@weasel-js/routing 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- import { RESERVED_ID_PREFIXES, RESERVED_ID_NAMES, parseTargetSpec, matchSpec, isKnownGestureName, getGestureDescriptor, formatRoute, canonicalModifiers } from '@weasel-js/gestures';
2
- export { GESTURE_DESCRIPTORS, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, canonicalModifiers, collapseShiftPairs, describeRoute, describeRouteParts, formatPhaseAtom, formatRoute, getGestureDescriptor, isKnownGestureName, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, parseRoute } from '@weasel-js/gestures';
3
- import { NORMAL, IMPLICIT_TAGS } from '@weasel-js/modes';
1
+ import { RESERVED_ID_PREFIXES, RESERVED_ID_NAMES, parseTargetSpec, matchSpec, routeGestureForSpecKind, getGestureDescriptor, formatRoute, canonicalModifiers, specificity } from '@weasel-js/gestures';
2
+ export { GESTURE_DESCRIPTORS, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, canonicalModifiers, collapseShiftPairs, describeRoute, describeRouteParts, formatPhaseAtom, formatRoute, getGestureDescriptor, isKnownGestureName, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, parseRoute, routeGestureForSpecKind, specKindForRouteGesture, specificity } from '@weasel-js/gestures';
3
+ import { activeModeOf, NORMAL, DEFAULT_MODES } from '@weasel-js/modes';
4
4
  import { resolveCursor } from '@weasel-js/cursor';
5
5
 
6
6
  // src/interactions/actions/binding.ts
@@ -44,6 +44,150 @@ function buildDepsFromRequires(action, depRegistry) {
44
44
  return deps;
45
45
  }
46
46
 
47
+ // src/interactions/actions/scopeNode.ts
48
+ var ScopeNode = class {
49
+ /** `descends`: whether reads made here answer from the active child down —
50
+ * true for a root or a yoke, which chrome reads through; false for a scope,
51
+ * which answers for itself even with a scope nested inside it. */
52
+ constructor(parent, descends) {
53
+ this.parent = parent;
54
+ this.descends = descends;
55
+ this.tree = parent ? parent.tree : { version: 0, listeners: /* @__PURE__ */ new Set() };
56
+ }
57
+ parent;
58
+ descends;
59
+ tree;
60
+ children = [];
61
+ activeChild = null;
62
+ /** Bump the tree's version and tell every listener in it. */
63
+ changed() {
64
+ this.tree.version++;
65
+ for (const listener of this.tree.listeners) {
66
+ try {
67
+ listener();
68
+ } catch (err) {
69
+ console.error("weasel input scope: subscriber threw", err);
70
+ }
71
+ }
72
+ }
73
+ /** Join the parent's children, newest last; returns the leave. */
74
+ mount() {
75
+ const parent = this.parent;
76
+ if (!parent) return () => {
77
+ };
78
+ parent.children.push(this);
79
+ this.changed();
80
+ let left = false;
81
+ return () => {
82
+ if (left) return;
83
+ left = true;
84
+ const i = parent.children.lastIndexOf(this);
85
+ if (i !== -1) parent.children.splice(i, 1);
86
+ if (parent.activeChild === this) parent.activeChild = null;
87
+ this.changed();
88
+ };
89
+ }
90
+ /** Make this node the one its ancestors resolve through. */
91
+ activate() {
92
+ let moved = false;
93
+ for (let n = this; n.parent; n = n.parent) {
94
+ if (n.parent.activeChild !== n) {
95
+ n.parent.activeChild = n;
96
+ moved = true;
97
+ }
98
+ }
99
+ if (moved) this.changed();
100
+ }
101
+ /** The node a lookup made here starts from: down the active children, or the
102
+ * newest where nothing has been activated yet. A yoke with no members yet
103
+ * is passed over — there is no canvas behind it to answer. */
104
+ leaf() {
105
+ let n = this;
106
+ for (; ; ) {
107
+ const next = n.activeChild ?? n.newestMember();
108
+ if (!next) return n;
109
+ n = next;
110
+ }
111
+ }
112
+ newestMember() {
113
+ for (let i = this.children.length - 1; i >= 0; i--) {
114
+ const c = this.children[i];
115
+ if (!(c.descends && c.children.length === 0)) return c;
116
+ }
117
+ return null;
118
+ }
119
+ /** Where a read made through this node starts. */
120
+ start() {
121
+ return this.descends ? this.leaf() : this;
122
+ }
123
+ /** Whether this node is on the path a read from the root takes. */
124
+ isActive() {
125
+ let root = this;
126
+ while (root.parent) root = root.parent;
127
+ for (const n of root.leaf().chain()) if (n === this) return true;
128
+ return false;
129
+ }
130
+ /** This node, then each ancestor up to the root. */
131
+ *chain() {
132
+ for (let n = this; n; n = n.parent) yield n;
133
+ }
134
+ };
135
+ function pushOwner(stack, value) {
136
+ if (value === null) return () => {
137
+ };
138
+ stack.push(value);
139
+ let released = false;
140
+ return () => {
141
+ if (released) return;
142
+ released = true;
143
+ const i = stack.lastIndexOf(value);
144
+ if (i !== -1) stack.splice(i, 1);
145
+ };
146
+ }
147
+
148
+ // src/interactions/actions/depNode.ts
149
+ var DepNode = class extends ScopeNode {
150
+ sources = /* @__PURE__ */ new Map();
151
+ registry = {
152
+ register: (name, source) => {
153
+ const key = name;
154
+ const entry = source;
155
+ const stack = this.sources.get(key);
156
+ if (stack) stack.push(entry);
157
+ else this.sources.set(key, [entry]);
158
+ let released = false;
159
+ return () => {
160
+ if (released) return;
161
+ released = true;
162
+ const cur = this.sources.get(key);
163
+ if (!cur) return;
164
+ const i = cur.lastIndexOf(entry);
165
+ if (i === -1) return;
166
+ cur.splice(i, 1);
167
+ if (cur.length === 0) this.sources.delete(key);
168
+ };
169
+ },
170
+ get: (name) => {
171
+ for (const n of this.start().chain()) {
172
+ const source = n.sources.get(name)?.at(-1);
173
+ if (source) return source();
174
+ }
175
+ return void 0;
176
+ }
177
+ };
178
+ constructor(parent, descends) {
179
+ super(parent, descends);
180
+ NODES.set(this.registry, this);
181
+ }
182
+ };
183
+ var NODES = /* @__PURE__ */ new WeakMap();
184
+ function depNodeOf(registry) {
185
+ return registry ? NODES.get(registry) ?? null : null;
186
+ }
187
+ function createDepRegistry() {
188
+ return new DepNode(null, true).registry;
189
+ }
190
+
47
191
  // src/interactions/actions/registry.ts
48
192
  function validateActionId(id) {
49
193
  if (id.length === 0) {
@@ -99,6 +243,8 @@ function offhandKeyFor(trigger) {
99
243
  return "Meta";
100
244
  case "shift":
101
245
  return "Shift";
246
+ default:
247
+ return trigger;
102
248
  }
103
249
  }
104
250
  function buildToolOffhandBindings(specs) {
@@ -148,7 +294,7 @@ function scopeBindings(entries, state) {
148
294
  const scope = liveScope(entry.id, entry.eligibility ?? {}, state);
149
295
  if (scope === null) continue;
150
296
  for (const binding of entry.bindings ?? []) {
151
- out.push({ binding, scope, ownerToolId: entry.id });
297
+ out.push({ binding, scope, ownerId: entry.id });
152
298
  }
153
299
  }
154
300
  return out;
@@ -161,34 +307,6 @@ function isDev() {
161
307
 
162
308
  // src/interactions/dispatcher/matcher.ts
163
309
  var SCOPE_PRIORITY = ["hotkey", "active", "ambient"];
164
- function modsCount(mods) {
165
- if (!mods) return 0;
166
- let n = 0;
167
- if (mods.alt === true) n++;
168
- if (mods.ctrl === true) n++;
169
- if (mods.meta === true) n++;
170
- if (mods.mod === true) n++;
171
- if (mods.shift === true) n++;
172
- return n;
173
- }
174
- function targetRank(target) {
175
- if (target === void 0) return 0;
176
- const form = parseTargetSpec(target);
177
- if (form === null) return 1;
178
- switch (form.form) {
179
- case "kind":
180
- return form.requireSelected ? 3 : 2;
181
- case "affordance":
182
- return 2;
183
- case "body":
184
- return 1;
185
- case "predicate":
186
- return 1;
187
- default: {
188
- return 1;
189
- }
190
- }
191
- }
192
310
  function targetConsultsAffordance(specTarget) {
193
311
  if (specTarget === void 0) return false;
194
312
  const form = parseTargetSpec(specTarget);
@@ -249,25 +367,6 @@ function reportDeadClaim(owner, warn) {
249
367
  `[weasel] exclusive claim by "${key}" matched no binding: no \`affordance:\` or \`kindOf\` target resolved against it, so the press was dropped.`
250
368
  );
251
369
  }
252
- function phaseRank(spec) {
253
- if (spec === void 0) return 0;
254
- const atoms = typeof spec === "string" ? [{ channel: "&", phase: spec }] : spec;
255
- if (atoms.length === 0) return 0;
256
- let min = 2;
257
- for (const a of atoms) {
258
- const rank = (a.channel === "*" ? 0 : 1) + (a.phase === "*" ? 0 : 1);
259
- if (rank < min) min = rank;
260
- }
261
- return min;
262
- }
263
- function specificity(spec) {
264
- const t = targetRank(specTargetOf(spec));
265
- const mods = "mods" in spec ? spec.mods : void 0;
266
- const m = modsCount(mods);
267
- const p = phaseRank("phase" in spec ? spec.phase : void 0);
268
- const typed = (spec.kind === "drop" || spec.kind === "paste") && spec.types !== void 0 && spec.types.length > 0 ? 2 : 1;
269
- return [t, m, p, typed];
270
- }
271
370
  function compareSpecificity(a, b) {
272
371
  const sa = specificity(a);
273
372
  const sb = specificity(b);
@@ -281,28 +380,122 @@ function matchBest(e, bindings, isMac, engagedChannels) {
281
380
  return sorted.length > 0 ? sorted[0] : null;
282
381
  }
283
382
  function matchSorted(e, bindings, isMac, engagedChannels, warn = (m) => console.warn(m)) {
284
- const out = [];
383
+ return matchSortedWithBarred(e, bindings, isMac, engagedChannels, { warn }).matches;
384
+ }
385
+ function matchSortedWithBarred(e, bindings, isMac, engagedChannels, opts = {}) {
386
+ const warn = opts.warn ?? ((m) => console.warn(m));
285
387
  const engaged = engagedChannels ?? EMPTY_ENGAGED;
286
388
  const exclusive = isExclusiveClaim(e);
287
- const pool = exclusive ? bindings.filter((sb) => targetConsultsAffordance(specTargetOf(sb.binding.spec))) : bindings;
389
+ const admits = (sb) => !exclusive || targetConsultsAffordance(specTargetOf(sb.binding.spec));
390
+ const out = [];
391
+ const barred = [];
288
392
  for (const scope of SCOPE_PRIORITY) {
289
393
  const scopeMatches = [];
290
- for (const sb of pool) {
394
+ const scopeBarred = [];
395
+ for (const sb of bindings) {
291
396
  if (sb.scope !== scope) continue;
292
- const phaseCtx = { selfChannel: sb.ownerToolId, engagedChannels: engaged };
397
+ const admitted = admits(sb);
398
+ if (!admitted && !opts.collectBarred) continue;
399
+ const phaseCtx = { selfChannel: sb.ownerId, engagedChannels: engaged };
293
400
  if (matchSpec(e, sb.binding.spec, isMac, phaseCtx)) {
294
- scopeMatches.push({ binding: sb.binding, scope, ownerToolId: sb.ownerToolId });
401
+ (admitted ? scopeMatches : scopeBarred).push({ binding: sb.binding, scope, ownerId: sb.ownerId });
295
402
  }
296
403
  }
297
404
  scopeMatches.sort((a, b) => compareSpecificity(a.binding.spec, b.binding.spec));
405
+ scopeBarred.sort((a, b) => compareSpecificity(a.binding.spec, b.binding.spec));
298
406
  out.push(...scopeMatches);
407
+ barred.push(...scopeBarred);
299
408
  }
300
409
  if (exclusive && out.length === 0 && bindings.length > 0) {
301
410
  reportDeadClaim(claimOf(e)?.owner, warn);
302
411
  }
303
- return out;
412
+ const owner = exclusive ? claimOf(e)?.owner : void 0;
413
+ return { matches: out, barred, ...owner !== void 0 ? { claimOwner: owner } : {} };
304
414
  }
305
415
  var EMPTY_ENGAGED = /* @__PURE__ */ new Set();
416
+ var PREDICATE_TARGET = "predicate";
417
+ function buildRouteRegistry(entries) {
418
+ const out = [];
419
+ for (const owner of entries) {
420
+ for (const binding of owner.bindings ?? []) {
421
+ const row = entryFor(owner.id, binding.spec, binding.actionId);
422
+ if (!row) continue;
423
+ const views = binding.opts?.views;
424
+ out.push(views === void 0 ? row : { ...row, views });
425
+ }
426
+ }
427
+ return out;
428
+ }
429
+ function routesForSpec(spec) {
430
+ const gesture = routeGestureForSpecKind(spec.kind);
431
+ if (!gesture) return [];
432
+ const descriptor = getGestureDescriptor(gesture);
433
+ const modifiers = parseModSpec("mods" in spec ? spec.mods : void 0);
434
+ const phases = phaseAtomsForSpec("phase" in spec ? spec.phase : void 0);
435
+ const target = descriptor.hasTarget ? routeTargetForSpec("target" in spec ? spec.target : void 0) ?? "*" : void 0;
436
+ const args = descriptor.arg ? routeArgsForSpec(spec, descriptor.arg.default) : [void 0];
437
+ return args.map((arg) => formatRoute({ phases, gesture, arg, target, modifiers }));
438
+ }
439
+ function entryFor(ownerId, spec, actionId) {
440
+ const gesture = routeGestureForSpecKind(spec.kind);
441
+ if (!gesture) return null;
442
+ const descriptor = getGestureDescriptor(gesture);
443
+ return {
444
+ ownerId,
445
+ actionId,
446
+ gesture,
447
+ phase: phaseOf("phase" in spec ? spec.phase : void 0),
448
+ modifiers: parseModSpec("mods" in spec ? spec.mods : void 0),
449
+ arg: descriptor.arg ? argOf(spec, descriptor.arg.default) : void 0,
450
+ ...descriptor.arg ? argAlternativesOf(spec, descriptor.arg.default) : {},
451
+ target: descriptor.hasTarget ? routeTargetForSpec("target" in spec ? spec.target : void 0) : void 0,
452
+ spec
453
+ };
454
+ }
455
+ function phaseOf(phase) {
456
+ if (phase === void 0 || phase === "*") return "any";
457
+ if (phase === "initial" || phase === "engaged") return phase;
458
+ const distinct = new Set(phase.map((atom) => atom.phase));
459
+ if (distinct.size !== 1) return "any";
460
+ const only = [...distinct][0];
461
+ return only === "initial" || only === "engaged" ? only : "any";
462
+ }
463
+ function parseModSpec(mods) {
464
+ if (!mods) return {};
465
+ const out = {};
466
+ for (const [name, req] of Object.entries(mods)) {
467
+ if (req === true) out[name] = "required";
468
+ else if (req === "optional") out[name] = "optional";
469
+ }
470
+ return out;
471
+ }
472
+ function phaseAtomsForSpec(phase) {
473
+ if (phase === void 0) return [{ channel: "*", phase: "*" }];
474
+ if (phase === "initial" || phase === "engaged" || phase === "*") {
475
+ return [{ channel: "&", phase }];
476
+ }
477
+ return phase;
478
+ }
479
+ function routeTargetForSpec(target) {
480
+ if (target === void 0) return void 0;
481
+ return typeof target === "string" ? target : PREDICATE_TARGET;
482
+ }
483
+ function routeArgsForSpec(spec, fallback) {
484
+ if ("key" in spec) return Array.isArray(spec.key) ? spec.key : [spec.key];
485
+ if ("fingers" in spec) return [String(spec.fingers)];
486
+ if ("direction" in spec) return [spec.direction ?? fallback];
487
+ if ("types" in spec) return [spec.types?.length ? spec.types.join("|") : fallback];
488
+ return [fallback];
489
+ }
490
+ function argOf(spec, fallback) {
491
+ const args = routeArgsForSpec(spec, fallback);
492
+ return args.length > 1 ? args.join("|") : args[0];
493
+ }
494
+ function argAlternativesOf(spec, fallback) {
495
+ const types = "types" in spec ? spec.types : void 0;
496
+ const alts = types?.length ? types : routeArgsForSpec(spec, fallback).filter((a) => a !== void 0);
497
+ return alts.length > 1 ? { argAlternatives: alts } : {};
498
+ }
306
499
 
307
500
  // src/eligibility/deviceProfile.ts
308
501
  var COARSE_TARGET_SCALE = 1.75;
@@ -418,10 +611,118 @@ function evaluate(rule, ctx) {
418
611
  if (isWhenRule(rule)) return rule.when(ctx);
419
612
  return evaluateSelector(rule, ctx);
420
613
  }
421
- var DEFAULT_ALLOWED_CAPABILITIES = /* @__PURE__ */ new Set([
422
- ...NORMAL.allows,
423
- ...IMPLICIT_TAGS
424
- ]);
614
+
615
+ // src/eligibility/exclusive.ts
616
+ function rulesExclusive(a, b) {
617
+ if (isAllRule(a)) return a.all.some((r) => rulesExclusive(r, b));
618
+ if (isAllRule(b)) return b.all.some((r) => rulesExclusive(a, r));
619
+ if (isAnyRule(a)) return a.any.every((r) => rulesExclusive(r, b));
620
+ if (isAnyRule(b)) return b.any.every((r) => rulesExclusive(a, r));
621
+ if (isNotRule(a)) return rulesEqual(a.not, b);
622
+ if (isNotRule(b)) return rulesEqual(b.not, a);
623
+ if (isWhenRule(a) || isWhenRule(b)) return false;
624
+ return selectorsExclusive(a, b);
625
+ }
626
+ function ruleCanHoldIn(rule, mode) {
627
+ return decideIn(rule, mode) !== false;
628
+ }
629
+ function decideIn(rule, mode) {
630
+ if (isAllRule(rule)) return all3(rule.all.map((r) => decideIn(r, mode)));
631
+ if (isAnyRule(rule)) {
632
+ const parts = rule.any.map((r) => decideIn(r, mode));
633
+ if (parts.includes(true)) return true;
634
+ return parts.every((p) => p === false) ? false : void 0;
635
+ }
636
+ if (isNotRule(rule)) {
637
+ const inner = decideIn(rule.not, mode);
638
+ return inner === void 0 ? void 0 : !inner;
639
+ }
640
+ if (isWhenRule(rule)) return void 0;
641
+ const ctx = { mode: mode.id, allowedCapabilities: mode.allowedCapabilities };
642
+ const settled = [
643
+ rule.mode === void 0 ? true : checkMode(rule.mode, ctx),
644
+ rule.capability === void 0 ? true : checkCapability(rule.capability, ctx)
645
+ ];
646
+ const { mode: _m, capability: _c, ...rest } = rule;
647
+ return all3([...settled, Object.keys(rest).length > 0 ? void 0 : true]);
648
+ }
649
+ function all3(parts) {
650
+ if (parts.includes(false)) return false;
651
+ return parts.every((p) => p === true) ? true : void 0;
652
+ }
653
+ var BOOLEAN_KEYS = [
654
+ "gesturing",
655
+ "focused",
656
+ "hovering",
657
+ "hoveringSelected",
658
+ "editingAnchors",
659
+ "resizable",
660
+ "coarsePointer",
661
+ "canHover"
662
+ ];
663
+ function selectorsExclusive(a, b) {
664
+ for (const key of BOOLEAN_KEYS) {
665
+ if (a[key] !== void 0 && b[key] !== void 0 && a[key] !== b[key]) return true;
666
+ }
667
+ if (a.actionIs !== void 0 && b.actionIs !== void 0 && a.actionIs !== b.actionIs) return true;
668
+ if (a.actionIs !== void 0 && b.gesturing === false) return true;
669
+ if (b.actionIs !== void 0 && a.gesturing === false) return true;
670
+ if (a.mode !== void 0 && b.mode !== void 0 && modesExclusive(a.mode, b.mode)) return true;
671
+ if (a.selection && b.selection && selectionsExclusive(a.selection, b.selection)) return true;
672
+ if (a.capability !== void 0 && b.capability !== void 0 && capabilitiesExclusive(a.capability, b.capability)) return true;
673
+ return false;
674
+ }
675
+ function modeSet(m) {
676
+ if (typeof m === "string") return { in: /* @__PURE__ */ new Set([m]) };
677
+ if ("not" in m) return { notIn: /* @__PURE__ */ new Set([m.not]) };
678
+ return { in: new Set(m.in) };
679
+ }
680
+ function modesExclusive(a, b) {
681
+ const x = modeSet(a);
682
+ const y = modeSet(b);
683
+ if ("in" in x && "in" in y) return ![...x.in].some((m) => y.in.has(m));
684
+ if ("in" in x && "notIn" in y) return [...x.in].every((m) => y.notIn.has(m));
685
+ if ("notIn" in x && "in" in y) return [...y.in].every((m) => x.notIn.has(m));
686
+ return false;
687
+ }
688
+ function selectionRange(s) {
689
+ let lo = 0;
690
+ let hi = Infinity;
691
+ if (s.empty === true) hi = 0;
692
+ if (s.empty === false) lo = 1;
693
+ if (s.is !== void 0) {
694
+ lo = Math.max(lo, s.is);
695
+ hi = Math.min(hi, s.is);
696
+ }
697
+ if (s.atLeast !== void 0) lo = Math.max(lo, s.atLeast);
698
+ return [lo, hi];
699
+ }
700
+ function selectionsExclusive(a, b) {
701
+ const [alo, ahi] = selectionRange(a);
702
+ const [blo, bhi] = selectionRange(b);
703
+ return Math.max(alo, blo) > Math.min(ahi, bhi);
704
+ }
705
+ function capabilityTerms(c) {
706
+ if (typeof c === "string") return { requires: [c] };
707
+ if (Array.isArray(c)) return { requires: c };
708
+ if ("not" in c) return { requires: [], forbids: c.not };
709
+ return { requires: [] };
710
+ }
711
+ function capabilitiesExclusive(a, b) {
712
+ const x = capabilityTerms(a);
713
+ const y = capabilityTerms(b);
714
+ return x.forbids !== void 0 && y.requires.includes(x.forbids) || y.forbids !== void 0 && x.requires.includes(y.forbids);
715
+ }
716
+ function rulesEqual(a, b) {
717
+ if (a === b) return true;
718
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
719
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
720
+ const ka = Object.keys(a);
721
+ const kb = Object.keys(b);
722
+ if (ka.length !== kb.length) return false;
723
+ return ka.every((k) => rulesEqual(a[k], b[k]));
724
+ }
725
+ var DEFAULT_ALLOWED_CAPABILITIES = activeModeOf(NORMAL).allowedCapabilities;
425
726
  function buildRuleCtx(args) {
426
727
  return {
427
728
  focused: args.focused,
@@ -455,6 +756,10 @@ function recordTrace(entry) {
455
756
  traceLog.push(entry);
456
757
  if (traceLog.length > TRACE_LIMIT) traceLog.shift();
457
758
  }
759
+ function publishLiveDispatch(explain) {
760
+ if (!DEV2 || typeof window === "undefined") return;
761
+ window.__weaselDispatchLive__ = explain;
762
+ }
458
763
  function recordModeSwitch(mode, from, to, detail) {
459
764
  recordTrace({
460
765
  kind: "mode",
@@ -469,11 +774,14 @@ function liveInView(binding, view) {
469
774
  const views = binding.opts?.views;
470
775
  return views === void 0 || views.includes(view);
471
776
  }
777
+ function namesView(binding, view) {
778
+ return binding.opts?.views?.includes(view) ?? false;
779
+ }
472
780
  function preferViewScoped(matches, view) {
473
781
  const named = [];
474
782
  const rest = [];
475
783
  for (const m of matches) {
476
- (m.binding.opts?.views?.includes(view) ? named : rest).push(m);
784
+ (namesView(m.binding, view) ? named : rest).push(m);
477
785
  }
478
786
  return named.length === 0 ? [...matches] : [...named, ...rest];
479
787
  }
@@ -498,6 +806,40 @@ function filterEligible(matches, actionLookup, ruleCtx) {
498
806
  return isEligible(action, ruleCtx);
499
807
  });
500
808
  }
809
+ function preferContextual(matches, actionLookup, holds, view) {
810
+ const out = [];
811
+ let run = [];
812
+ const flush = () => {
813
+ const gated = run.filter((m) => {
814
+ const action = actionLookup(m.binding.actionId);
815
+ return action?.eligible !== void 0 && holds(action);
816
+ });
817
+ out.push(...gated, ...run.filter((m) => !gated.includes(m)));
818
+ run = [];
819
+ };
820
+ for (const m of matches) {
821
+ const head = run[0];
822
+ if (head && (head.scope !== m.scope || namesView(head.binding, view) !== namesView(m.binding, view) || !sameSpecificity(head.binding.spec, m.binding.spec))) flush();
823
+ run.push(m);
824
+ }
825
+ flush();
826
+ return out;
827
+ }
828
+ var SPECIFICITY_PARTS = ["target", "mods", "phase", "exact"];
829
+ function placedBy(prev, cur, view, contextual) {
830
+ if (namesView(prev.binding, view) !== namesView(cur.binding, view)) return { step: "view" };
831
+ if (prev.scope !== cur.scope) return { step: "tier" };
832
+ const sp = specificity(prev.binding.spec);
833
+ const sc = specificity(cur.binding.spec);
834
+ const i = sp.findIndex((v, k) => v !== sc[k]);
835
+ if (i >= 0) return { step: "specificity", part: SPECIFICITY_PARTS[i] };
836
+ return contextual ? { step: "context" } : { step: "order" };
837
+ }
838
+ function sameSpecificity(a, b) {
839
+ const sa = specificity(a);
840
+ const sb = specificity(b);
841
+ return sa.every((v, i) => v === sb[i]);
842
+ }
501
843
  var EMPTY_ENGAGED2 = /* @__PURE__ */ new Set();
502
844
  function modifiersOf(e) {
503
845
  return { alt: e.altKey, ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey };
@@ -628,11 +970,11 @@ function createDispatcher(opts) {
628
970
  const ordered = [];
629
971
  for (let i = ctx.hotkeyStack.length - 1; i >= 0; i--) {
630
972
  const id = ctx.hotkeyStack[i];
631
- const tool = ctx.toolsById.get(id);
632
- if (tool && !ordered.includes(tool)) ordered.push(tool);
973
+ const entry = ctx.entriesById.get(id);
974
+ if (entry && !ordered.includes(entry)) ordered.push(entry);
633
975
  }
634
- for (const tool of ctx.toolsById.values()) {
635
- if (!ordered.includes(tool)) ordered.push(tool);
976
+ for (const entry of ctx.entriesById.values()) {
977
+ if (!ordered.includes(entry)) ordered.push(entry);
636
978
  }
637
979
  const capsCtx = ctx.getRuleCtx?.();
638
980
  const allows = capsCtx ? (tags) => tags.every((tag) => capsCtx.allowedCapabilities.has(tag)) : void 0;
@@ -646,7 +988,7 @@ function createDispatcher(opts) {
646
988
  const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
647
989
  for (const binding of actionBindings(action)) {
648
990
  if (!liveInView(binding, view)) continue;
649
- result.push({ binding, scope: targetScope, ownerToolId: null });
991
+ result.push({ binding, scope: targetScope, ownerId: null });
650
992
  }
651
993
  }
652
994
  return result;
@@ -753,209 +1095,302 @@ function createDispatcher(opts) {
753
1095
  pinchStartSpreads.delete(id);
754
1096
  }
755
1097
  }
756
- const scopedBindings = assembleScopedBindings(ctx);
757
- const engagedChannels = snapshotEngagedChannels();
758
- const rawMatches = preferViewScoped(
759
- matchSorted(event, scopedBindings, ctx.isMac, engagedChannels),
760
- ctx.viewId ?? null
761
- );
762
- const traceCandidates = [];
763
- const eventKey = event.kind === "key" || event.kind === "key-held" ? event.key : void 0;
764
- const finishTrace = (fired, outcome) => {
765
- recordTrace({
766
- kind: "dispatch",
767
- ts: Date.now(),
768
- eventKind: event.kind,
769
- ...eventKey !== void 0 ? { key: eventKey } : {},
770
- candidates: traceCandidates,
771
- fired,
772
- outcome
773
- });
774
- };
775
- if (rawMatches.length === 0) {
776
- finishTrace(null, "unhandled");
777
- return "unhandled";
778
- }
779
- const actionMap = buildActionMap(ctx.actions);
780
- const ruleCtx = ruleCtxOf(ctx);
781
- const matches = ruleCtx ? filterEligible(rawMatches, (id) => actionMap.get(id), ruleCtx) : rawMatches;
782
- if (matches.length === 0) {
783
- finishTrace(null, "unhandled");
784
- return "unhandled";
1098
+ const r = rank(event, ctx, DEV2);
1099
+ const walked = walk(r, event, ctx, (match, action, deps) => invoke(event, match, action, deps), false);
1100
+ const outcome = walked.stop === "fired" ? "handled" : "unhandled";
1101
+ if (DEV2) recordTrace(buildRecord(r, walked, event, ctx, false, outcome));
1102
+ return outcome;
1103
+ }
1104
+ function invoke(event, match, action, deps) {
1105
+ if (action.invoker?.timing === "immediate") {
1106
+ try {
1107
+ const resolved = resolveParams(match.binding.opts?.params);
1108
+ let params;
1109
+ if (event.kind === "wheel") {
1110
+ params = {
1111
+ deltaX: event.deltaX,
1112
+ deltaY: event.deltaY,
1113
+ clientX: event.clientX,
1114
+ clientY: event.clientY,
1115
+ affordance: event.affordance,
1116
+ ...resolved
1117
+ };
1118
+ } else if (event.kind === "pinch") {
1119
+ params = {
1120
+ scale: event.scale,
1121
+ rotation: event.rotation,
1122
+ clientX: event.clientX,
1123
+ clientY: event.clientY,
1124
+ affordance: event.affordance,
1125
+ ...resolved
1126
+ };
1127
+ } else if (event.kind === "click" || event.kind === "doubleclick") {
1128
+ params = {
1129
+ worldX: event.x,
1130
+ worldY: event.y,
1131
+ affordance: event.affordance,
1132
+ // Press point as well as release point — an action that places
1133
+ // geometry at the click wants the former. See `ClickEvent`.
1134
+ ...event.kind === "click" ? { pressX: event.pressX, pressY: event.pressY } : {},
1135
+ mods: modifiersOf(event),
1136
+ ...resolved
1137
+ };
1138
+ } else if (event.kind === "contextmenu" || event.kind === "longpress") {
1139
+ params = {
1140
+ worldX: event.x,
1141
+ worldY: event.y,
1142
+ affordance: event.affordance,
1143
+ bodyTarget: event.bodyTarget,
1144
+ mods: modifiersOf(event),
1145
+ ...resolved
1146
+ };
1147
+ } else if (event.kind === "pointerdown") {
1148
+ params = {
1149
+ worldX: event.x,
1150
+ worldY: event.y,
1151
+ affordance: event.affordance,
1152
+ bodyTarget: event.bodyTarget,
1153
+ mods: modifiersOf(event),
1154
+ ...resolved
1155
+ };
1156
+ } else if (event.kind === "drop" || event.kind === "paste") {
1157
+ params = {
1158
+ items: event.items,
1159
+ via: event.kind,
1160
+ ...event.kind === "drop" && event.x !== void 0 ? { worldX: event.x, worldY: event.y } : {},
1161
+ ...resolved
1162
+ };
1163
+ } else {
1164
+ params = resolved;
1165
+ }
1166
+ action.invoker.run(deps, params);
1167
+ } catch (err) {
1168
+ console.error(`weasel dispatcher: action "${action.id}" invoker threw`, err);
1169
+ }
1170
+ return "fired";
785
1171
  }
786
- const triedActionIds = /* @__PURE__ */ new Set();
787
- for (const match of matches) {
788
- if (triedActionIds.has(match.binding.actionId)) continue;
789
- triedActionIds.add(match.binding.actionId);
790
- const action = actionMap.get(match.binding.actionId);
791
- if (!action) {
792
- traceCandidates.push({ actionId: match.binding.actionId, scope: match.scope, enabledResult: "no-such-action" });
793
- console.warn(
794
- `weasel dispatcher: binding resolved actionId "${match.binding.actionId}" which has no registered action. Skipping. (misconfiguration)`
1172
+ if (action.invoker?.timing === "ongoing") {
1173
+ if (event.kind === "pointerdown" && event.stage === "press") {
1174
+ console.error(
1175
+ `weasel dispatcher: action "${action.id}" has an ongoing invoker but is bound to a pointerDown spec, which fires at press time. Bind it to a drag spec, or give the action an immediate invoker.`
795
1176
  );
796
- continue;
1177
+ return "misbound";
797
1178
  }
798
- const deps = buildDepsFromRequires(action, ctx.depRegistry);
799
- if (action.enabled) {
800
- const result = action.enabled(deps, worldPointOf(event));
801
- if (result !== true) {
802
- traceCandidates.push({ actionId: action.id, scope: match.scope, enabledResult: String(result) });
803
- continue;
804
- }
1179
+ const gestureId = gestureIdFor(event);
1180
+ if (event.kind === "pointerdown") {
1181
+ dragOrigins.set(gestureId, {
1182
+ x: event.x ?? 0,
1183
+ y: event.y ?? 0,
1184
+ ...event.clientX !== void 0 ? { clientX: event.clientX } : {},
1185
+ ...event.clientY !== void 0 ? { clientY: event.clientY } : {}
1186
+ });
1187
+ dragPoints.set(gestureId, [{
1188
+ x: event.x ?? 0,
1189
+ y: event.y ?? 0,
1190
+ ...event.pressure !== void 0 ? { pressure: event.pressure } : {},
1191
+ ...event.tiltX !== void 0 ? { tiltX: event.tiltX } : {},
1192
+ ...event.tiltY !== void 0 ? { tiltY: event.tiltY } : {}
1193
+ }]);
805
1194
  }
806
- traceCandidates.push({ actionId: action.id, scope: match.scope, enabledResult: true });
807
- if (action.invoker?.timing === "immediate") {
808
- try {
809
- const resolved = resolveParams(match.binding.opts?.params);
810
- let params;
811
- if (event.kind === "wheel") {
812
- params = {
813
- deltaX: event.deltaX,
814
- deltaY: event.deltaY,
815
- clientX: event.clientX,
816
- clientY: event.clientY,
817
- affordance: event.affordance,
818
- ...resolved
819
- };
820
- } else if (event.kind === "pinch") {
821
- params = {
822
- scale: event.scale,
823
- rotation: event.rotation,
824
- clientX: event.clientX,
825
- clientY: event.clientY,
826
- affordance: event.affordance,
827
- ...resolved
828
- };
829
- } else if (event.kind === "click" || event.kind === "doubleclick") {
830
- params = {
831
- worldX: event.x,
832
- worldY: event.y,
833
- affordance: event.affordance,
834
- // Press point as well as release point — an action that places
835
- // geometry at the click wants the former. See `ClickEvent`.
836
- ...event.kind === "click" ? { pressX: event.pressX, pressY: event.pressY } : {},
837
- mods: modifiersOf(event),
838
- ...resolved
839
- };
840
- } else if (event.kind === "contextmenu" || event.kind === "longpress") {
841
- params = {
842
- worldX: event.x,
843
- worldY: event.y,
844
- affordance: event.affordance,
845
- bodyTarget: event.bodyTarget,
846
- mods: modifiersOf(event),
847
- ...resolved
848
- };
849
- } else if (event.kind === "pointerdown") {
850
- params = {
851
- worldX: event.x,
852
- worldY: event.y,
853
- affordance: event.affordance,
854
- bodyTarget: event.bodyTarget,
855
- mods: modifiersOf(event),
856
- ...resolved
857
- };
858
- } else if (event.kind === "drop" || event.kind === "paste") {
859
- params = {
860
- items: event.items,
861
- via: event.kind,
862
- ...event.kind === "drop" && event.x !== void 0 ? { worldX: event.x, worldY: event.y } : {},
863
- ...resolved
864
- };
865
- } else {
866
- params = resolved;
867
- }
868
- action.invoker.run(deps, params);
869
- } catch (err) {
870
- console.error(`weasel dispatcher: action "${action.id}" invoker threw`, err);
871
- }
872
- finishTrace(action.id, "handled");
873
- return "handled";
1195
+ if (event.kind === "multitouch" && event.spread !== void 0) {
1196
+ pinchStartSpreads.set(gestureId, event.spread);
874
1197
  }
875
- if (action.invoker?.timing === "ongoing") {
876
- if (event.kind === "pointerdown" && event.stage === "press") {
877
- console.error(
878
- `weasel dispatcher: action "${action.id}" has an ongoing invoker but is bound to a pointerDown spec, which fires at press time. Bind it to a drag spec, or give the action an immediate invoker.`
879
- );
880
- finishTrace(action.id, "unhandled");
881
- return "unhandled";
882
- }
883
- const gestureId = gestureIdFor(event);
1198
+ const invCtx = buildInvocationCtx(event, deps, gestureId);
1199
+ const handle = action.invoker.start(invCtx, match.binding.opts);
1200
+ if (isEmptyOngoingHandle(handle)) {
884
1201
  if (event.kind === "pointerdown") {
885
- dragOrigins.set(gestureId, {
886
- x: event.x ?? 0,
887
- y: event.y ?? 0,
888
- ...event.clientX !== void 0 ? { clientX: event.clientX } : {},
889
- ...event.clientY !== void 0 ? { clientY: event.clientY } : {}
890
- });
891
- dragPoints.set(gestureId, [{
892
- x: event.x ?? 0,
893
- y: event.y ?? 0,
894
- ...event.pressure !== void 0 ? { pressure: event.pressure } : {},
895
- ...event.tiltX !== void 0 ? { tiltX: event.tiltX } : {},
896
- ...event.tiltY !== void 0 ? { tiltY: event.tiltY } : {}
897
- }]);
898
- }
899
- if (event.kind === "multitouch" && event.spread !== void 0) {
900
- pinchStartSpreads.set(gestureId, event.spread);
1202
+ dragOrigins.delete(gestureId);
1203
+ dragPoints.delete(gestureId);
901
1204
  }
902
- const invCtx = buildInvocationCtx(event, deps, gestureId);
903
- const handle = action.invoker.start(invCtx, match.binding.opts);
904
- if (isEmptyOngoingHandle(handle)) {
905
- if (event.kind === "pointerdown") {
906
- dragOrigins.delete(gestureId);
907
- dragPoints.delete(gestureId);
908
- }
909
- if (event.kind === "multitouch") {
910
- pinchStartSpreads.delete(gestureId);
911
- }
912
- traceCandidates.push({ actionId: action.id, scope: match.scope, enabledResult: "empty-handle" });
913
- continue;
1205
+ if (event.kind === "multitouch") {
1206
+ pinchStartSpreads.delete(gestureId);
914
1207
  }
915
- inFlightHandles.set(gestureId, handle);
916
- inFlightOwners.set(gestureId, match.ownerToolId);
917
- inFlightActions.set(gestureId, action);
918
- finishTrace(action.id, "handled");
919
- return "handled";
1208
+ return "empty-handle";
920
1209
  }
921
- finishTrace(action.id, "handled");
922
- return "handled";
1210
+ inFlightHandles.set(gestureId, handle);
1211
+ inFlightOwners.set(gestureId, match.ownerId);
1212
+ inFlightActions.set(gestureId, action);
1213
+ return "fired";
923
1214
  }
924
- finishTrace(null, "unhandled");
925
- return "unhandled";
1215
+ return "fired";
926
1216
  }
927
- function resolveAll(event, ctx, resolveOpts) {
928
- const scopedBindings = assembleScopedBindings(ctx);
929
- const engagedChannels = snapshotEngagedChannels();
930
- const matches = preferViewScoped(
931
- matchSorted(event, scopedBindings, ctx.isMac, engagedChannels),
932
- ctx.viewId ?? null
1217
+ function rank(event, ctx, collectBarred) {
1218
+ const view = ctx.viewId ?? null;
1219
+ const outcome = matchSortedWithBarred(
1220
+ event,
1221
+ assembleScopedBindings(ctx),
1222
+ ctx.isMac,
1223
+ snapshotEngagedChannels(),
1224
+ { collectBarred }
933
1225
  );
934
- if (matches.length === 0) return [];
1226
+ const sorted = preferViewScoped(outcome.matches, view);
935
1227
  const actionMap = buildActionMap(ctx.actions);
936
1228
  const ruleCtx = ruleCtxOf(ctx);
1229
+ const held = /* @__PURE__ */ new Map();
1230
+ const holds = (a) => {
1231
+ if (ruleCtx === void 0) return true;
1232
+ let h = held.get(a);
1233
+ if (h === void 0) held.set(a, h = isEligible(a, ruleCtx));
1234
+ return h;
1235
+ };
1236
+ const ordered = ruleCtx ? preferContextual(sorted, (id) => actionMap.get(id), holds, view) : sorted;
1237
+ return {
1238
+ ordered,
1239
+ barred: outcome.barred,
1240
+ ...outcome.claimOwner !== void 0 ? { claimOwner: outcome.claimOwner } : {},
1241
+ actionMap,
1242
+ ruleCtx,
1243
+ eligible: (m) => {
1244
+ const a = actionMap.get(m.binding.actionId);
1245
+ return a === void 0 || holds(a);
1246
+ }
1247
+ };
1248
+ }
1249
+ function walk(r, event, ctx, attempt, evaluateShadowed) {
1250
+ const steps = [];
1251
+ const tried = /* @__PURE__ */ new Set();
1252
+ let stop = "exhausted";
1253
+ let winner = null;
1254
+ for (const match of r.ordered) {
1255
+ if (!r.eligible(match)) continue;
1256
+ const push = (step) => {
1257
+ steps.push({ match, step });
1258
+ };
1259
+ const actionId = match.binding.actionId;
1260
+ if (tried.has(actionId)) {
1261
+ push({ kind: "duplicate" });
1262
+ continue;
1263
+ }
1264
+ if (stop !== "exhausted" && !evaluateShadowed) {
1265
+ push({ kind: "not-asked" });
1266
+ continue;
1267
+ }
1268
+ tried.add(actionId);
1269
+ const action = r.actionMap.get(actionId);
1270
+ if (!action) {
1271
+ if (attempt) {
1272
+ console.warn(
1273
+ `weasel dispatcher: binding resolved actionId "${actionId}" which has no registered action. Skipping. (misconfiguration)`
1274
+ );
1275
+ }
1276
+ push({ kind: "no-such-action" });
1277
+ continue;
1278
+ }
1279
+ const deps = buildDepsFromRequires(action, ctx.depRegistry);
1280
+ const enabled = action.enabled ? action.enabled(deps, worldPointOf(event)) : true;
1281
+ if (enabled !== true) {
1282
+ push({ kind: "declined", reason: String(enabled) });
1283
+ continue;
1284
+ }
1285
+ if (stop !== "exhausted") {
1286
+ push({ kind: "outranked" });
1287
+ continue;
1288
+ }
1289
+ if (!attempt) {
1290
+ push({ kind: "would-fire" });
1291
+ stop = "fired";
1292
+ winner = { match, action };
1293
+ continue;
1294
+ }
1295
+ const result = attempt(match, action, deps);
1296
+ if (result === "empty-handle") {
1297
+ push({ kind: "empty-handle" });
1298
+ continue;
1299
+ }
1300
+ push({ kind: result === "fired" ? "fired" : "misbound" });
1301
+ stop = result;
1302
+ winner = { match, action };
1303
+ }
1304
+ return { steps, stop, winner };
1305
+ }
1306
+ function buildRecord(r, walked, event, ctx, predicted, outcome) {
1307
+ const view = ctx.viewId ?? null;
1308
+ const candidates = /* @__PURE__ */ new Map();
1309
+ const candidateOf = (m) => {
1310
+ let c = candidates.get(m);
1311
+ if (!c) {
1312
+ const eligible = r.actionMap.get(m.binding.actionId)?.eligible;
1313
+ c = {
1314
+ actionId: m.binding.actionId,
1315
+ routes: routesForSpec(m.binding.spec),
1316
+ scope: m.scope,
1317
+ ownerId: m.ownerId,
1318
+ namesView: namesView(m.binding, view),
1319
+ specificity: specificity(m.binding.spec),
1320
+ ...eligible !== void 0 ? { eligible: describeEligible(eligible) } : {}
1321
+ };
1322
+ candidates.set(m, c);
1323
+ }
1324
+ return c;
1325
+ };
1326
+ const dropped = [
1327
+ ...r.barred.map((m) => ({
1328
+ candidate: candidateOf(m),
1329
+ filter: "claim",
1330
+ ...r.claimOwner !== void 0 ? { owner: r.claimOwner } : {}
1331
+ }))
1332
+ ];
1333
+ for (const m of r.ordered) {
1334
+ if (r.eligible(m)) continue;
1335
+ const c = candidateOf(m);
1336
+ dropped.push({ candidate: c, filter: "ineligible", rule: c.eligible ?? "" });
1337
+ }
1338
+ const gated = (m) => r.actionMap.get(m.binding.actionId)?.eligible !== void 0;
1339
+ const ranked = walked.steps.map(({ match, step }, i) => {
1340
+ const prev = walked.steps[i - 1]?.match;
1341
+ return {
1342
+ candidate: candidateOf(match),
1343
+ placedBy: prev ? placedBy(prev, match, view, r.ruleCtx !== void 0 && gated(prev) && !gated(match)) : { step: "first" },
1344
+ walk: step
1345
+ };
1346
+ });
1347
+ const eventKey = event.kind === "key" || event.kind === "key-held" ? event.key : void 0;
1348
+ const world = worldPointOf(event);
1349
+ return {
1350
+ kind: "dispatch",
1351
+ ts: Date.now(),
1352
+ input: {
1353
+ eventKind: event.kind,
1354
+ ...eventKey !== void 0 ? { key: eventKey } : {},
1355
+ modifiers: modifiersOf(event),
1356
+ viewId: view,
1357
+ ...world !== void 0 ? { world } : {},
1358
+ ...r.ruleCtx !== void 0 ? { mode: r.ruleCtx.mode } : {}
1359
+ },
1360
+ matched: [...r.ordered, ...r.barred].map(candidateOf),
1361
+ dropped,
1362
+ ranked,
1363
+ predicted,
1364
+ fired: walked.winner?.action.id ?? null,
1365
+ outcome
1366
+ };
1367
+ }
1368
+ function resolveAll(event, ctx, resolveOpts) {
1369
+ const evaluateShadowed = resolveOpts?.evaluateShadowed ?? false;
1370
+ const r = rank(event, ctx, false);
1371
+ if (r.ordered.length === 0) return [];
1372
+ const walked = walk(r, event, ctx, null, evaluateShadowed);
1373
+ const stepOf = new Map(walked.steps.map((s) => [s.match, s.step]));
937
1374
  const verdictByAction = /* @__PURE__ */ new Map();
938
1375
  let fired = false;
939
1376
  const out = [];
940
- for (const match of matches) {
1377
+ for (const match of r.ordered) {
941
1378
  const actionId = match.binding.actionId;
942
- const action = actionMap.get(actionId);
1379
+ const action = r.actionMap.get(actionId);
943
1380
  if (!action) continue;
944
1381
  let verdict = verdictByAction.get(actionId);
945
1382
  if (verdict === void 0) {
946
- if (fired && !resolveOpts?.evaluateShadowed) {
947
- verdict = { kind: "shadowed" };
948
- } else if (ruleCtx && action.eligible && !isEligible(action, ruleCtx)) {
949
- verdict = { kind: "ineligible", reason: describeEligible(action.eligible) };
1383
+ if (!r.eligible(match)) {
1384
+ verdict = fired && !evaluateShadowed ? { kind: "shadowed" } : { kind: "ineligible", reason: describeEligible(action.eligible) };
950
1385
  } else {
951
- const disabled = action.enabled ? action.enabled(buildDepsFromRequires(action, ctx.depRegistry), worldPointOf(event)) : true;
952
- if (disabled !== true) {
953
- verdict = { kind: "disabled", reason: String(disabled) };
954
- } else if (fired) {
955
- verdict = { kind: "shadowed" };
956
- } else {
1386
+ const step = stepOf.get(match);
1387
+ if (step?.kind === "would-fire") {
957
1388
  verdict = { kind: "would-fire" };
958
1389
  fired = true;
1390
+ } else if (step?.kind === "declined") {
1391
+ verdict = { kind: "disabled", reason: step.reason };
1392
+ } else {
1393
+ verdict = { kind: "shadowed" };
959
1394
  }
960
1395
  }
961
1396
  verdictByAction.set(actionId, verdict);
@@ -967,7 +1402,7 @@ function createDispatcher(opts) {
967
1402
  action,
968
1403
  binding: match.binding,
969
1404
  scope: match.scope,
970
- ownerToolId: match.ownerToolId,
1405
+ ownerId: match.ownerId,
971
1406
  specificity: specificity(match.binding.spec),
972
1407
  verdict
973
1408
  });
@@ -975,15 +1410,21 @@ function createDispatcher(opts) {
975
1410
  return out;
976
1411
  }
977
1412
  function resolveOnly(event, ctx) {
978
- const winner = resolveAll(event, ctx).find((c) => c.verdict.kind === "would-fire");
1413
+ const r = rank(event, ctx, false);
1414
+ const { winner } = walk(r, event, ctx, null, false);
979
1415
  if (!winner) return null;
980
1416
  return {
981
- actionId: winner.actionId,
1417
+ actionId: winner.action.id,
982
1418
  action: winner.action,
983
- scope: winner.scope,
984
- ownerToolId: winner.ownerToolId
1419
+ scope: winner.match.scope,
1420
+ ownerId: winner.match.ownerId
985
1421
  };
986
1422
  }
1423
+ function explain(event, ctx, explainOpts) {
1424
+ const r = rank(event, ctx, true);
1425
+ const walked = walk(r, event, ctx, null, explainOpts?.evaluateShadowed ?? false);
1426
+ return buildRecord(r, walked, event, ctx, true, walked.winner ? "handled" : "unhandled");
1427
+ }
987
1428
  function cancelAll(reason) {
988
1429
  const stubCtx = {
989
1430
  world: { x: 0, y: 0 },
@@ -1123,6 +1564,7 @@ function createDispatcher(opts) {
1123
1564
  handleInput: handleInputWithNotify,
1124
1565
  resolveOnly,
1125
1566
  resolveAll,
1567
+ explain,
1126
1568
  cancelAll: cancelAllWithNotify,
1127
1569
  inFlight,
1128
1570
  inFlightCursor,
@@ -1213,117 +1655,17 @@ function openPointerSession(origin, down, cb, opts = {}) {
1213
1655
  };
1214
1656
  }
1215
1657
 
1216
- // src/tools/routing/reflection/registry.ts
1217
- var PREDICATE_TARGET = "predicate";
1218
- var SPEC_KIND_TO_GESTURE = {
1219
- key: "keyDown",
1220
- "key-held": "keyHeld",
1221
- wheel: "wheel",
1222
- pinch: "pinch",
1223
- click: "click",
1224
- doubleClick: "dblTap",
1225
- contextMenu: "contextMenu",
1226
- longPress: "longPress",
1227
- drag: "drag",
1228
- pointerDown: "pointerDown",
1229
- multiTouch: void 0,
1230
- multiTouchTap: "multiTouchTap",
1231
- drop: "drop",
1232
- paste: "paste"
1233
- };
1234
- function buildRouteRegistry(tools) {
1235
- const out = [];
1236
- for (const tool of tools) {
1237
- for (const binding of tool.bindings ?? []) {
1238
- const entry = entryFor(tool.id, binding.spec, binding.actionId);
1239
- if (entry) out.push(entry);
1240
- }
1241
- }
1242
- return out;
1243
- }
1244
- function routeGestureForSpecKind(kind) {
1245
- const gesture = SPEC_KIND_TO_GESTURE[kind];
1246
- return gesture && isKnownGestureName(gesture) ? gesture : void 0;
1247
- }
1248
- function routesForSpec(spec) {
1249
- const gesture = routeGestureForSpecKind(spec.kind);
1250
- if (!gesture) return [];
1251
- const descriptor = getGestureDescriptor(gesture);
1252
- const modifiers = parseModSpec("mods" in spec ? spec.mods : void 0);
1253
- const phases = phaseAtomsForSpec("phase" in spec ? spec.phase : void 0);
1254
- const target = descriptor.hasTarget ? routeTargetForSpec("target" in spec ? spec.target : void 0) ?? "*" : void 0;
1255
- const args = descriptor.arg ? routeArgsForSpec(spec, descriptor.arg.default) : [void 0];
1256
- return args.map((arg) => formatRoute({ phases, gesture, arg, target, modifiers }));
1257
- }
1258
- function entryFor(toolId, spec, actionId) {
1259
- const gesture = SPEC_KIND_TO_GESTURE[spec.kind];
1260
- if (!gesture || !isKnownGestureName(gesture)) return null;
1261
- const descriptor = getGestureDescriptor(gesture);
1262
- return {
1263
- toolId,
1264
- actionId,
1265
- gesture,
1266
- phase: phaseOf("phase" in spec ? spec.phase : void 0),
1267
- modifiers: parseModSpec("mods" in spec ? spec.mods : void 0),
1268
- arg: descriptor.arg ? argOf(spec, descriptor.arg.default) : void 0,
1269
- ...descriptor.arg ? argAlternativesOf(spec, descriptor.arg.default) : {},
1270
- target: descriptor.hasTarget ? routeTargetForSpec("target" in spec ? spec.target : void 0) : void 0,
1271
- spec
1272
- };
1273
- }
1274
- function phaseOf(phase) {
1275
- if (phase === void 0 || phase === "*") return "any";
1276
- if (phase === "initial" || phase === "engaged") return phase;
1277
- const distinct = new Set(phase.map((atom) => atom.phase));
1278
- if (distinct.size !== 1) return "any";
1279
- const only = [...distinct][0];
1280
- return only === "initial" || only === "engaged" ? only : "any";
1281
- }
1282
- function parseModSpec(mods) {
1283
- if (!mods) return {};
1284
- const out = {};
1285
- for (const [name, req] of Object.entries(mods)) {
1286
- if (req === true) out[name] = "required";
1287
- else if (req === "optional") out[name] = "optional";
1288
- }
1289
- return out;
1290
- }
1291
- function phaseAtomsForSpec(phase) {
1292
- if (phase === void 0) return [{ channel: "&", phase: "*" }];
1293
- if (phase === "initial" || phase === "engaged" || phase === "*") {
1294
- return [{ channel: "&", phase }];
1295
- }
1296
- return phase;
1658
+ // src/tools/types.ts
1659
+ function isTool(entry) {
1660
+ return !!(entry.eligibility?.focus || entry.eligibility?.offhand);
1297
1661
  }
1298
- function routeTargetForSpec(target) {
1299
- if (target === void 0) return void 0;
1300
- return typeof target === "string" ? target : PREDICATE_TARGET;
1301
- }
1302
- function routeArgsForSpec(spec, fallback) {
1303
- if ("key" in spec) return Array.isArray(spec.key) ? spec.key : [spec.key];
1304
- if ("fingers" in spec) return [String(spec.fingers)];
1305
- if ("direction" in spec) return [spec.direction ?? fallback];
1306
- if ("types" in spec) return [spec.types?.length ? spec.types.join("|") : fallback];
1307
- return [fallback];
1662
+ function findConflicts(entries) {
1663
+ return findConflictsKeyed(entries).map((k) => k.conflict);
1308
1664
  }
1309
- function argOf(spec, fallback) {
1310
- const args = routeArgsForSpec(spec, fallback);
1311
- return args.length > 1 ? args.join("|") : args[0];
1312
- }
1313
- function argAlternativesOf(spec, fallback) {
1314
- const types = "types" in spec ? spec.types : void 0;
1315
- const alts = types?.length ? types : routeArgsForSpec(spec, fallback).filter((a) => a !== void 0);
1316
- return alts.length > 1 ? { argAlternatives: alts } : {};
1317
- }
1318
-
1319
- // src/tools/routing/reflection/conflicts.ts
1320
- function findConflicts(tools) {
1321
- return findConflictsKeyed(tools).map((k) => k.conflict);
1322
- }
1323
- function findConflictsKeyed(tools) {
1324
- const entries = buildRouteRegistry(tools);
1665
+ function findConflictsKeyed(entries, rules, modes = KIT_MODES) {
1666
+ const rows = buildRouteRegistry(entries);
1325
1667
  const groups = /* @__PURE__ */ new Map();
1326
- for (const entry of entries) {
1668
+ for (const entry of rows) {
1327
1669
  for (const arg of argKeysOf(entry)) {
1328
1670
  const key = `${entry.phase}|${entry.gesture}|${arg}|${targetKey(entry)}|${canonicalModifiers(entry.modifiers)}`;
1329
1671
  const bucket = groups.get(key);
@@ -1332,8 +1674,11 @@ function findConflictsKeyed(tools) {
1332
1674
  } else groups.set(key, [entry]);
1333
1675
  }
1334
1676
  }
1677
+ const ruleOf = (entry) => rules?.get(entry.ownerId) ?? ALWAYS;
1678
+ const tie = (e, f) => viewsTie(e.views, f.views) && rulesCanHoldTogether(ruleOf(e), ruleOf(f), modes);
1335
1679
  const conflicts = [];
1336
- for (const [key, bucket] of groups) {
1680
+ for (const [key, all] of groups) {
1681
+ const bucket = all.filter((e) => all.some((f) => f !== e && tie(e, f)));
1337
1682
  if (bucket.length < 2) continue;
1338
1683
  const first = bucket[0];
1339
1684
  conflicts.push({
@@ -1344,12 +1689,16 @@ function findConflictsKeyed(tools) {
1344
1689
  arg: first.arg,
1345
1690
  target: first.target,
1346
1691
  modifiers: first.modifiers,
1347
- toolIds: bucket.map((e) => e.toolId)
1692
+ ownerIds: bucket.map((e) => e.ownerId)
1348
1693
  }
1349
1694
  });
1350
1695
  }
1351
1696
  return conflicts;
1352
1697
  }
1698
+ function viewsTie(a, b) {
1699
+ if (a === void 0 || b === void 0) return a === b;
1700
+ return a.some((v) => b.includes(v));
1701
+ }
1353
1702
  function argKeysOf(entry) {
1354
1703
  const alts = entry.argAlternatives;
1355
1704
  if (alts && alts.length > 0) return [...new Set(alts.map((a) => a.toLowerCase()))];
@@ -1372,43 +1721,67 @@ function predicateId(pred) {
1372
1721
  }
1373
1722
  return id;
1374
1723
  }
1375
- function actionAsTool(action) {
1724
+ function rulesCanHoldTogether(a, b, modes) {
1725
+ if (rulesExclusive(a, b)) return false;
1726
+ return modes.some((m) => ruleCanHoldIn(a, m) && ruleCanHoldIn(b, m));
1727
+ }
1728
+ var KIT_MODES = DEFAULT_MODES.map(activeModeOf);
1729
+ function actionAsEntry(action) {
1376
1730
  return { id: action.id, eligibility: {}, bindings: actionBindings(action) };
1377
1731
  }
1732
+ function eligibleRule(action) {
1733
+ const eligible = action.eligible;
1734
+ if (eligible === void 0) return void 0;
1735
+ return typeof eligible === "function" ? eligible.rule : eligible;
1736
+ }
1378
1737
  function findScopedConflicts(scopes) {
1379
1738
  const registry = Array.isArray(scopes.registry) ? scopes.registry : Object.values(scopes.registry);
1380
1739
  const actions = scopes.actions ?? [];
1381
- const ambientActions = actions.filter((a) => a.scope !== "hotkey").map(actionAsTool);
1382
- const hotkeyActions = actions.filter((a) => a.scope === "hotkey").map(actionAsTool);
1383
- const ambient = [...scopes.ambient ?? [], ...ambientActions];
1740
+ const rules = /* @__PURE__ */ new Map();
1741
+ for (const a of actions) {
1742
+ const rule = eligibleRule(a);
1743
+ if (rule) rules.set(a.id, rule);
1744
+ }
1745
+ const hotkeyActions = actions.filter((a) => a.scope === "hotkey").map(actionAsEntry);
1746
+ const gatedActions = [];
1747
+ const ungated = [];
1748
+ for (const a of actions) {
1749
+ if (a.scope === "hotkey") continue;
1750
+ (a.eligible !== void 0 ? gatedActions : ungated).push(actionAsEntry(a));
1751
+ }
1752
+ const modes = scopes.modes ?? KIT_MODES;
1753
+ const ambient = [...scopes.ambient ?? [], ...ungated];
1384
1754
  const out = [];
1385
1755
  const seen = /* @__PURE__ */ new Set();
1386
1756
  const add = (keyed) => {
1387
1757
  for (const { key, conflict } of keyed) {
1388
- const dedupeKey = `${key}|${conflict.toolIds.join(",")}`;
1758
+ const dedupeKey = `${key}|${conflict.ownerIds.join(",")}`;
1389
1759
  if (seen.has(dedupeKey)) continue;
1390
1760
  seen.add(dedupeKey);
1391
1761
  out.push(conflict);
1392
1762
  }
1393
1763
  };
1394
- for (const tool of [...registry, ...ambient, ...hotkeyActions]) add(findConflictsKeyed([tool]));
1395
- if (ambient.length > 1) add(findConflictsKeyed(ambient));
1764
+ for (const tool of [...registry, ...ambient, ...gatedActions, ...hotkeyActions]) {
1765
+ add(findConflictsKeyed([tool], rules, modes));
1766
+ }
1767
+ if (ambient.length > 1) add(findConflictsKeyed(ambient, rules, modes));
1768
+ if (gatedActions.length > 1) add(findConflictsKeyed(gatedActions, rules, modes));
1396
1769
  const hotkey = [
1397
1770
  ...registry.filter((t) => t.def?.hotkey !== void 0),
1398
1771
  ...hotkeyActions
1399
1772
  ];
1400
- if (hotkey.length > 1) add(findConflictsKeyed(hotkey));
1773
+ if (hotkey.length > 1) add(findConflictsKeyed(hotkey, rules, modes));
1401
1774
  return out;
1402
1775
  }
1403
1776
  function formatConflict(conflict) {
1404
1777
  const route = formatRoute({
1405
- phases: [{ channel: "&", phase: conflict.phase === "any" ? "*" : conflict.phase }],
1778
+ phases: [conflict.phase === "any" ? { channel: "*", phase: "*" } : { channel: "&", phase: conflict.phase }],
1406
1779
  gesture: conflict.gesture,
1407
1780
  arg: conflict.arg,
1408
1781
  target: conflict.target,
1409
1782
  modifiers: conflict.modifiers
1410
1783
  });
1411
- return `${route} \u2014 declared by ${conflict.toolIds.join(", ")}`;
1784
+ return `${route} \u2014 declared by ${conflict.ownerIds.join(", ")}`;
1412
1785
  }
1413
1786
  function reportRouteConflicts(scopes, warn = (m) => console.warn(m)) {
1414
1787
  const conflicts = findScopedConflicts(scopes);
@@ -1550,6 +1923,6 @@ function dlog(namespace, ...args) {
1550
1923
  console.debug(`[${namespace}]`, ...args);
1551
1924
  }
1552
1925
 
1553
- export { ALWAYS, COARSE_TARGET_SCALE, DEFAULT_ALLOWED_CAPABILITIES, DEFAULT_DEVICE_PROFILE, INGEST_STRING_MIMES, NEVER, PREDICATE_TARGET, TOOL_OFFHAND_ID, actionBindings, buildDepsFromRequires, buildRouteRegistry, buildRuleCtx, buildToolOffhandBindings, clientToCanvas, clientToCanvasRect, createDispatcher, describeRule, dlog, evaluate, filterEligible, findConflicts, isDev, isEditableTarget, itemsFromClipboardData, itemsFromDataTransfer, itemsFromFiles, liveScope, makeToolOffhandAction, matchBest, matchSorted, matchesKeyBinding, offhandKeyFor, openPointerSession, pointerGestureId, recordModeSwitch, reportRouteConflicts, resolveDeviceProfile, resolveParams, routeGestureForSpecKind, routesForSpec, scopeBindings, specificity, targetConsultsAffordance, validateActionDefaultBinding, validateActionId };
1554
- //# sourceMappingURL=chunk-VEC3ACV5.js.map
1555
- //# sourceMappingURL=chunk-VEC3ACV5.js.map
1926
+ export { ALWAYS, COARSE_TARGET_SCALE, DEFAULT_ALLOWED_CAPABILITIES, DEFAULT_DEVICE_PROFILE, DepNode, INGEST_STRING_MIMES, NEVER, PREDICATE_TARGET, ScopeNode, TOOL_OFFHAND_ID, actionBindings, buildDepsFromRequires, buildRouteRegistry, buildRuleCtx, buildToolOffhandBindings, clientToCanvas, clientToCanvasRect, createDepRegistry, createDispatcher, depNodeOf, describeRule, dlog, evaluate, filterEligible, findConflicts, isDev, isEditableTarget, isTool, itemsFromClipboardData, itemsFromDataTransfer, itemsFromFiles, liveScope, makeToolOffhandAction, matchBest, matchSorted, matchSortedWithBarred, matchesKeyBinding, offhandKeyFor, openPointerSession, pointerGestureId, preferContextual, publishLiveDispatch, pushOwner, recordModeSwitch, reportRouteConflicts, resolveDeviceProfile, resolveParams, routesForSpec, ruleCanHoldIn, rulesExclusive, scopeBindings, targetConsultsAffordance, validateActionDefaultBinding, validateActionId };
1927
+ //# sourceMappingURL=chunk-Q3Y3DPKH.js.map
1928
+ //# sourceMappingURL=chunk-Q3Y3DPKH.js.map