@weasel-js/routing 1.9.4 → 1.10.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,8 +1,18 @@
1
- import { RESERVED_ID_PREFIXES, RESERVED_ID_NAMES, parseTargetSpec, matchSpec, routeGestureForSpecKind, getGestureDescriptor, formatRoute, canonicalModifiers, VALID_MOD_NAMES, 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';
1
+ import { createEmitter } from '@weasel-js/registry';
2
+ import { RESERVED_ID_PREFIXES, RESERVED_ID_NAMES, routeGestureForSpecKind, getGestureDescriptor, formatRoute, parseTargetSpec, matchSpec, canonicalModifiers, UNKNOWN_MIME, specificity, VALID_MOD_NAMES, stylusOf } from '@weasel-js/gestures';
3
+ export { GESTURE_DESCRIPTORS, LONG_PRESS_MS, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, activatesFocusedControl, canonicalModifiers, collapseShiftPairs, describeRoute, describeRouteParts, focusedControlOwnsKey, formatPhaseAtom, formatRoute, getGestureDescriptor, isEditableTarget, isKnownGestureName, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, parseRoute, routeGestureForSpecKind, specKindForRouteGesture, specificity } from '@weasel-js/gestures';
4
+ import { activeModeOf, NORMAL, DEFAULT_MODES, capabilitiesAllowed } from '@weasel-js/modes';
4
5
  import { resolveCursor } from '@weasel-js/cursor';
5
6
 
7
+ // src/interactions/actions/action.ts
8
+ var ActionDisabledReason = {
9
+ SelectionRequired: "selection-required",
10
+ SceneEmpty: "scene-empty",
11
+ NotApplicable: "not-applicable",
12
+ /** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
13
+ PredicateThrew: "predicate-threw"
14
+ };
15
+
6
16
  // src/interactions/actions/binding.ts
7
17
  function actionBindings(action) {
8
18
  const gs = action.defaultBinding;
@@ -16,6 +26,53 @@ function actionBindings(action) {
16
26
  });
17
27
  }
18
28
 
29
+ // src/devFlag.ts
30
+ function isDev() {
31
+ return typeof process !== "undefined" ? process.env?.NODE_ENV !== "production" : true;
32
+ }
33
+
34
+ // src/interactions/actions/actionEnabled.ts
35
+ var enabledSlowWarned = /* @__PURE__ */ new Set();
36
+ var enabledThrewWarned = /* @__PURE__ */ new Set();
37
+ var ENABLED_BUDGET_MS = 4;
38
+ function evaluateEnabled(action, deps) {
39
+ if (!action.enabled) return { enabled: true };
40
+ const dev = isDev();
41
+ const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
42
+ const start = dev ? now() : 0;
43
+ try {
44
+ const result = action.enabled(deps);
45
+ if (dev) {
46
+ const elapsed = now() - start;
47
+ if (elapsed > ENABLED_BUDGET_MS && !enabledSlowWarned.has(action.id)) {
48
+ enabledSlowWarned.add(action.id);
49
+ console.warn(
50
+ `weasel actions: enabled() for action "${action.id}" took ${elapsed.toFixed(2)}ms (budget ${ENABLED_BUDGET_MS}ms). The predicate must be pure and fast \u2014 see Action.enabled JSDoc.`
51
+ );
52
+ }
53
+ }
54
+ if (result === true) return { enabled: true };
55
+ return { enabled: false, reason: result };
56
+ } catch (e) {
57
+ if (dev && !enabledThrewWarned.has(action.id)) {
58
+ enabledThrewWarned.add(action.id);
59
+ console.warn(
60
+ `weasel actions: enabled() for action "${action.id}" threw; treating as disabled. The predicate must not throw \u2014 see Action.enabled JSDoc.`,
61
+ e
62
+ );
63
+ }
64
+ return { enabled: false, reason: ActionDisabledReason.PredicateThrew };
65
+ }
66
+ }
67
+ function evaluateActive(action, deps) {
68
+ if (!action.active) return void 0;
69
+ try {
70
+ return action.active(deps) === true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
19
76
  // src/interactions/actions/buildDeps.ts
20
77
  var DEV = (() => {
21
78
  try {
@@ -43,8 +100,6 @@ function buildDepsFromRequires(action, depRegistry) {
43
100
  }
44
101
  return deps;
45
102
  }
46
-
47
- // src/interactions/actions/scopeNode.ts
48
103
  var ScopeNode = class {
49
104
  /** `descends`: whether reads made here answer from the active child down —
50
105
  * true for a root or a yoke, which chrome reads through; false for a scope,
@@ -52,7 +107,7 @@ var ScopeNode = class {
52
107
  constructor(parent, descends) {
53
108
  this.parent = parent;
54
109
  this.descends = descends;
55
- this.tree = parent ? parent.tree : { version: 0, listeners: /* @__PURE__ */ new Set() };
110
+ this.tree = parent ? parent.tree : { version: 0, listeners: createEmitter() };
56
111
  }
57
112
  parent;
58
113
  descends;
@@ -62,13 +117,7 @@ var ScopeNode = class {
62
117
  /** Bump the tree's version and tell every listener in it. */
63
118
  changed() {
64
119
  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
- }
120
+ this.tree.listeners.emit();
72
121
  }
73
122
  /** Join the parent's children, newest last; returns the leave. */
74
123
  mount() {
@@ -176,12 +225,7 @@ var DepNode = class extends ScopeNode {
176
225
  }
177
226
  return void 0;
178
227
  },
179
- subscribe: (listener) => {
180
- this.tree.listeners.add(listener);
181
- return () => {
182
- this.tree.listeners.delete(listener);
183
- };
184
- },
228
+ subscribe: (listener) => this.tree.listeners.subscribe(listener),
185
229
  changed: () => this.changed()
186
230
  };
187
231
  constructor(parent, descends) {
@@ -289,56 +333,94 @@ function makeToolOffhandAction(bindings) {
289
333
  requires: ["activeTool"]
290
334
  };
291
335
  }
292
-
293
- // src/contributions/eligibility.ts
294
- function liveScope(id, eligibility, state) {
295
- const tags = eligibility.capabilities;
296
- if (tags && tags.length > 0 && state.allows && !state.allows(tags)) return null;
297
- if (state.engagedIds?.has(id)) return "hotkey";
298
- if (eligibility.focus && state.focusedId === id) return "active";
299
- if (eligibility.always || eligibility.claimed) return "ambient";
300
- return null;
301
- }
302
-
303
- // src/contributions/assemble.ts
304
- function scopeBindings(entries, state) {
336
+ var PREDICATE_TARGET = "predicate";
337
+ function buildRouteRegistry(entries) {
305
338
  const out = [];
306
- for (const entry of entries) {
307
- const scope = liveScope(entry.id, entry.eligibility ?? {}, state);
308
- if (scope === null) continue;
309
- for (const binding of entry.bindings ?? []) {
310
- out.push({ binding, scope, ownerId: entry.id });
339
+ for (const owner of entries) {
340
+ for (const binding of owner.bindings ?? []) {
341
+ const row = entryFor(owner.id, binding.spec, binding.actionId);
342
+ if (!row) continue;
343
+ const { views, params, yields } = binding.opts ?? {};
344
+ out.push({
345
+ ...row,
346
+ ...views === void 0 ? {} : { views },
347
+ ...params === void 0 ? {} : { params },
348
+ ...yields ? { yields: true } : {}
349
+ });
311
350
  }
312
351
  }
313
352
  return out;
314
353
  }
315
- function assembleEntries(entries, inputs) {
316
- const all = [...entries];
317
- const ordered = [];
318
- for (let i = inputs.hotkeyStack.length - 1; i >= 0; i--) {
319
- const entry = all.find((e) => e.id === inputs.hotkeyStack[i]);
320
- if (entry && !ordered.includes(entry)) ordered.push(entry);
321
- }
322
- for (const entry of all) {
323
- if (!ordered.includes(entry)) ordered.push(entry);
324
- }
325
- const allowed = inputs.allowedCapabilities;
354
+ function routesForSpec(spec) {
355
+ const gesture = routeGestureForSpecKind(spec.kind);
356
+ if (!gesture) return [];
357
+ const descriptor = getGestureDescriptor(gesture);
358
+ const modifiers = parseModSpec("mods" in spec ? spec.mods : void 0);
359
+ const phases = phaseAtomsForSpec("phase" in spec ? spec.phase : void 0);
360
+ const target = descriptor.hasTarget ? routeTargetForSpec("target" in spec ? spec.target : void 0) ?? "*" : void 0;
361
+ const args = descriptor.arg ? routeArgsForSpec(spec, descriptor.arg.default) : [void 0];
362
+ return args.map((arg) => formatRoute({ phases, gesture, arg, target, modifiers }));
363
+ }
364
+ function entryFor(ownerId, spec, actionId) {
365
+ const gesture = routeGestureForSpecKind(spec.kind);
366
+ if (!gesture) return null;
367
+ const descriptor = getGestureDescriptor(gesture);
326
368
  return {
327
- ordered,
328
- state: {
329
- focusedId: inputs.focusedId,
330
- engagedIds: new Set(inputs.hotkeyStack),
331
- ...allowed ? { allows: (tags) => tags.every((tag) => allowed.has(tag)) } : {}
332
- }
369
+ ownerId,
370
+ actionId,
371
+ gesture,
372
+ phase: phaseOf("phase" in spec ? spec.phase : void 0),
373
+ modifiers: parseModSpec("mods" in spec ? spec.mods : void 0),
374
+ arg: descriptor.arg ? argOf(spec, descriptor.arg.default) : void 0,
375
+ ...descriptor.arg ? argAlternativesOf(spec, descriptor.arg.default) : {},
376
+ target: descriptor.hasTarget ? routeTargetForSpec("target" in spec ? spec.target : void 0) : void 0,
377
+ spec
333
378
  };
334
379
  }
335
-
336
- // src/devFlag.ts
337
- function isDev() {
338
- return typeof process !== "undefined" ? process.env?.NODE_ENV !== "production" : true;
380
+ function phaseOf(phase) {
381
+ if (phase === void 0 || phase === "*") return "any";
382
+ if (phase === "initial" || phase === "engaged") return phase;
383
+ const distinct = new Set(phase.map((atom) => atom.phase));
384
+ if (distinct.size !== 1) return "any";
385
+ const only = [...distinct][0];
386
+ return only === "initial" || only === "engaged" ? only : "any";
387
+ }
388
+ function parseModSpec(mods) {
389
+ if (!mods) return {};
390
+ const out = {};
391
+ for (const [name, req] of Object.entries(mods)) {
392
+ if (req === true) out[name] = "required";
393
+ else if (req === "optional") out[name] = "optional";
394
+ }
395
+ return out;
396
+ }
397
+ function phaseAtomsForSpec(phase) {
398
+ if (phase === void 0) return [{ channel: "*", phase: "*" }];
399
+ if (phase === "initial" || phase === "engaged" || phase === "*") {
400
+ return [{ channel: "&", phase }];
401
+ }
402
+ return phase;
403
+ }
404
+ function routeTargetForSpec(target) {
405
+ if (target === void 0) return void 0;
406
+ return typeof target === "string" ? target : PREDICATE_TARGET;
407
+ }
408
+ function routeArgsForSpec(spec, fallback) {
409
+ if ("key" in spec) return Array.isArray(spec.key) ? spec.key : [spec.key];
410
+ if ("fingers" in spec) return [String(spec.fingers)];
411
+ if ("direction" in spec) return [spec.direction ?? fallback];
412
+ if ("types" in spec) return [spec.types?.length ? spec.types.join("|") : fallback];
413
+ return [fallback];
414
+ }
415
+ function argOf(spec, fallback) {
416
+ const args = routeArgsForSpec(spec, fallback);
417
+ return args.length > 1 ? args.join("|") : args[0];
418
+ }
419
+ function argAlternativesOf(spec, fallback) {
420
+ const types = "types" in spec ? spec.types : void 0;
421
+ const alts = types?.length ? types : routeArgsForSpec(spec, fallback).filter((a) => a !== void 0);
422
+ return alts.length > 1 ? { argAlternatives: alts } : {};
339
423
  }
340
-
341
- // src/interactions/dispatcher/matcher.ts
342
424
  var SCOPE_PRIORITY = ["hotkey", "active", "ambient"];
343
425
  function targetConsultsAffordance(specTarget) {
344
426
  if (specTarget === void 0) return false;
@@ -446,89 +528,6 @@ function matchSortedWithBarred(e, bindings, isMac, engagedChannels, opts = {}) {
446
528
  return { matches: out, barred, ...owner !== void 0 ? { claimOwner: owner } : {} };
447
529
  }
448
530
  var EMPTY_ENGAGED = /* @__PURE__ */ new Set();
449
- var PREDICATE_TARGET = "predicate";
450
- function buildRouteRegistry(entries) {
451
- const out = [];
452
- for (const owner of entries) {
453
- for (const binding of owner.bindings ?? []) {
454
- const row = entryFor(owner.id, binding.spec, binding.actionId);
455
- if (!row) continue;
456
- const views = binding.opts?.views;
457
- out.push(views === void 0 ? row : { ...row, views });
458
- }
459
- }
460
- return out;
461
- }
462
- function routesForSpec(spec) {
463
- const gesture = routeGestureForSpecKind(spec.kind);
464
- if (!gesture) return [];
465
- const descriptor = getGestureDescriptor(gesture);
466
- const modifiers = parseModSpec("mods" in spec ? spec.mods : void 0);
467
- const phases = phaseAtomsForSpec("phase" in spec ? spec.phase : void 0);
468
- const target = descriptor.hasTarget ? routeTargetForSpec("target" in spec ? spec.target : void 0) ?? "*" : void 0;
469
- const args = descriptor.arg ? routeArgsForSpec(spec, descriptor.arg.default) : [void 0];
470
- return args.map((arg) => formatRoute({ phases, gesture, arg, target, modifiers }));
471
- }
472
- function entryFor(ownerId, spec, actionId) {
473
- const gesture = routeGestureForSpecKind(spec.kind);
474
- if (!gesture) return null;
475
- const descriptor = getGestureDescriptor(gesture);
476
- return {
477
- ownerId,
478
- actionId,
479
- gesture,
480
- phase: phaseOf("phase" in spec ? spec.phase : void 0),
481
- modifiers: parseModSpec("mods" in spec ? spec.mods : void 0),
482
- arg: descriptor.arg ? argOf(spec, descriptor.arg.default) : void 0,
483
- ...descriptor.arg ? argAlternativesOf(spec, descriptor.arg.default) : {},
484
- target: descriptor.hasTarget ? routeTargetForSpec("target" in spec ? spec.target : void 0) : void 0,
485
- spec
486
- };
487
- }
488
- function phaseOf(phase) {
489
- if (phase === void 0 || phase === "*") return "any";
490
- if (phase === "initial" || phase === "engaged") return phase;
491
- const distinct = new Set(phase.map((atom) => atom.phase));
492
- if (distinct.size !== 1) return "any";
493
- const only = [...distinct][0];
494
- return only === "initial" || only === "engaged" ? only : "any";
495
- }
496
- function parseModSpec(mods) {
497
- if (!mods) return {};
498
- const out = {};
499
- for (const [name, req] of Object.entries(mods)) {
500
- if (req === true) out[name] = "required";
501
- else if (req === "optional") out[name] = "optional";
502
- }
503
- return out;
504
- }
505
- function phaseAtomsForSpec(phase) {
506
- if (phase === void 0) return [{ channel: "*", phase: "*" }];
507
- if (phase === "initial" || phase === "engaged" || phase === "*") {
508
- return [{ channel: "&", phase }];
509
- }
510
- return phase;
511
- }
512
- function routeTargetForSpec(target) {
513
- if (target === void 0) return void 0;
514
- return typeof target === "string" ? target : PREDICATE_TARGET;
515
- }
516
- function routeArgsForSpec(spec, fallback) {
517
- if ("key" in spec) return Array.isArray(spec.key) ? spec.key : [spec.key];
518
- if ("fingers" in spec) return [String(spec.fingers)];
519
- if ("direction" in spec) return [spec.direction ?? fallback];
520
- if ("types" in spec) return [spec.types?.length ? spec.types.join("|") : fallback];
521
- return [fallback];
522
- }
523
- function argOf(spec, fallback) {
524
- const args = routeArgsForSpec(spec, fallback);
525
- return args.length > 1 ? args.join("|") : args[0];
526
- }
527
- function argAlternativesOf(spec, fallback) {
528
- const types = "types" in spec ? spec.types : void 0;
529
- const alts = types?.length ? types : routeArgsForSpec(spec, fallback).filter((a) => a !== void 0);
530
- return alts.length > 1 ? { argAlternatives: alts } : {};
531
- }
532
531
 
533
532
  // src/eligibility/deviceProfile.ts
534
533
  var COARSE_TARGET_SCALE = 1.75;
@@ -788,9 +787,9 @@ function recordTrace(entry) {
788
787
  traceLog.push(entry);
789
788
  if (traceLog.length > TRACE_LIMIT) traceLog.shift();
790
789
  }
791
- function publishLiveDispatch(explain) {
790
+ function publishLiveDispatch(explain2) {
792
791
  if (!DEV2 || typeof window === "undefined") return;
793
- window.__weaselDispatchLive__ = explain;
792
+ window.__weaselDispatchLive__ = explain2;
794
793
  }
795
794
  function recordModeSwitch(mode, from, to, detail) {
796
795
  recordTrace({
@@ -803,44 +802,179 @@ function recordModeSwitch(mode, from, to, detail) {
803
802
  });
804
803
  }
805
804
 
806
- // src/interactions/dispatcher/inputFacts.ts
807
- function modifiersOf(e) {
808
- return { alt: e.altKey, ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey };
809
- }
810
- function worldPointOf(event) {
811
- switch (event.kind) {
812
- case "pointerdown":
813
- case "longpress":
814
- case "click":
815
- case "doubleclick":
816
- case "contextmenu":
817
- return event.x !== void 0 && event.y !== void 0 ? { x: event.x, y: event.y } : void 0;
818
- default:
819
- return void 0;
820
- }
805
+ // src/interactions/dispatcher/gestureIds.ts
806
+ function pointerGestureId(pointerId) {
807
+ return `pointer-${pointerId ?? "mouse"}`;
821
808
  }
822
-
823
- // src/interactions/dispatcher/ranking.ts
824
- function liveInView(binding, view) {
825
- const views = binding.opts?.views;
826
- return views === void 0 || views.includes(view);
809
+ function multiTouchGestureId(fingers) {
810
+ return `multitouch-${fingers}`;
827
811
  }
828
- function namesView(binding, view) {
829
- return binding.opts?.views?.includes(view) ?? false;
812
+ function isMultiTouchGestureId(gestureId) {
813
+ return gestureId.startsWith("multitouch-");
830
814
  }
831
- function preferViewScoped(matches, view) {
832
- const named = [];
833
- const rest = [];
834
- for (const m of matches) {
835
- (namesView(m.binding, view) ? named : rest).push(m);
815
+ function gestureIdFor(event) {
816
+ if (event.kind === "key-held") {
817
+ return `key-held-${event.code ?? event.key}`;
836
818
  }
837
- return named.length === 0 ? [...matches] : [...named, ...rest];
838
- }
839
- function ruleCtxOf(ctx) {
840
- const r = ctx.getRuleCtx?.();
841
- return r ? { ...r, viewId: ctx.viewId ?? null } : void 0;
819
+ if (event.kind === "pointerdown" || event.kind === "pointermove" || event.kind === "pointerup" || event.kind === "pointercancel") {
820
+ return pointerGestureId(event.pointerId);
821
+ }
822
+ if (event.kind === "multitouch") {
823
+ return multiTouchGestureId(event.fingers);
824
+ }
825
+ return `ongoing-${event.kind}`;
842
826
  }
843
- function eligibleToRule(eligible) {
827
+
828
+ // src/contributions/eligibility.ts
829
+ function liveScope(id, eligibility, state) {
830
+ const tags = eligibility.capabilities;
831
+ if (tags && state.allows && !state.allows(tags)) return null;
832
+ if (state.engagedIds?.has(id)) return "hotkey";
833
+ if (eligibility.focus && state.focusedId === id) return "active";
834
+ if (eligibility.always || eligibility.claimed) return "ambient";
835
+ return null;
836
+ }
837
+ function scopeBindings(entries, state) {
838
+ const out = [];
839
+ for (const entry of entries) {
840
+ const scope = liveScope(entry.id, entry.eligibility ?? {}, state);
841
+ if (scope === null) continue;
842
+ for (const binding of entry.bindings ?? []) {
843
+ out.push({ binding, scope, ownerId: entry.id });
844
+ }
845
+ }
846
+ return out;
847
+ }
848
+ function assembleEntries(entries, inputs) {
849
+ const all = [...entries];
850
+ const ordered = [];
851
+ for (let i = inputs.hotkeyStack.length - 1; i >= 0; i--) {
852
+ const entry = all.find((e) => e.id === inputs.hotkeyStack[i]);
853
+ if (entry && !ordered.includes(entry)) ordered.push(entry);
854
+ }
855
+ for (const entry of all) {
856
+ if (!ordered.includes(entry)) ordered.push(entry);
857
+ }
858
+ const allowed = inputs.allowedCapabilities;
859
+ return {
860
+ ordered,
861
+ state: {
862
+ focusedId: inputs.focusedId,
863
+ engagedIds: new Set(inputs.hotkeyStack),
864
+ ...allowed ? { allows: (tags) => capabilitiesAllowed(allowed, tags) } : {}
865
+ }
866
+ };
867
+ }
868
+
869
+ // src/interactions/dispatcher/inputFacts.ts
870
+ function modifiersOf(e) {
871
+ return { alt: e.altKey, ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey };
872
+ }
873
+ function worldPointOf(event) {
874
+ switch (event.kind) {
875
+ case "pointerdown":
876
+ case "hover":
877
+ case "longpress":
878
+ case "click":
879
+ case "doubleclick":
880
+ case "contextmenu":
881
+ return event.x !== void 0 && event.y !== void 0 ? { x: event.x, y: event.y } : void 0;
882
+ default:
883
+ return void 0;
884
+ }
885
+ }
886
+ function carriedBy(event) {
887
+ return event.kind === "drop" || event.kind === "paste" ? { items: event.items } : void 0;
888
+ }
889
+ function immediateParams(event, resolved) {
890
+ switch (event.kind) {
891
+ case "wheel":
892
+ return {
893
+ deltaX: event.deltaX,
894
+ deltaY: event.deltaY,
895
+ clientX: event.clientX,
896
+ clientY: event.clientY,
897
+ affordance: event.affordance,
898
+ ...resolved
899
+ };
900
+ case "pinch":
901
+ return {
902
+ scale: event.scale,
903
+ rotation: event.rotation,
904
+ clientX: event.clientX,
905
+ clientY: event.clientY,
906
+ affordance: event.affordance,
907
+ ...resolved
908
+ };
909
+ case "click":
910
+ case "doubleclick":
911
+ return {
912
+ worldX: event.x,
913
+ worldY: event.y,
914
+ affordance: event.affordance,
915
+ // The press point too: an action that places geometry at the click wants it.
916
+ ...event.kind === "click" ? { pressX: event.pressX, pressY: event.pressY } : {},
917
+ mods: modifiersOf(event),
918
+ ...resolved
919
+ };
920
+ // Only a `stage: 'press'` pointerdown reaches an immediate invoker: the
921
+ // buffered copy matches `drag` specs, which are ongoing.
922
+ case "pointerdown":
923
+ case "contextmenu":
924
+ case "longpress":
925
+ return {
926
+ worldX: event.x,
927
+ worldY: event.y,
928
+ affordance: event.affordance,
929
+ bodyTarget: event.bodyTarget,
930
+ mods: modifiersOf(event),
931
+ ...resolved
932
+ };
933
+ case "hover":
934
+ return {
935
+ worldX: event.x,
936
+ worldY: event.y,
937
+ clientX: event.clientX,
938
+ clientY: event.clientY,
939
+ affordance: event.affordance,
940
+ bodyTarget: event.bodyTarget,
941
+ mods: modifiersOf(event),
942
+ ...resolved
943
+ };
944
+ case "drop":
945
+ case "paste":
946
+ return {
947
+ items: event.items,
948
+ via: event.kind,
949
+ ...event.kind === "drop" && event.x !== void 0 ? { worldX: event.x, worldY: event.y } : {},
950
+ ...resolved
951
+ };
952
+ default:
953
+ return resolved;
954
+ }
955
+ }
956
+
957
+ // src/interactions/dispatcher/ranking.ts
958
+ function liveInView(binding, view) {
959
+ const views = binding.opts?.views;
960
+ return views === void 0 || views.includes(view);
961
+ }
962
+ function namesView(binding, view) {
963
+ return binding.opts?.views?.includes(view) ?? false;
964
+ }
965
+ function preferViewScoped(matches, view) {
966
+ const named = [];
967
+ const rest = [];
968
+ for (const m of matches) {
969
+ (namesView(m.binding, view) ? named : rest).push(m);
970
+ }
971
+ return named.length === 0 ? [...matches] : [...named, ...rest];
972
+ }
973
+ function ruleCtxOf(ctx) {
974
+ const r = ctx.getRuleCtx?.();
975
+ return r ? { ...r, viewId: ctx.viewId ?? null } : void 0;
976
+ }
977
+ function eligibleToRule(eligible) {
844
978
  return typeof eligible === "function" ? eligible.rule : eligible;
845
979
  }
846
980
  function isEligible(action, ruleCtx) {
@@ -948,609 +1082,331 @@ function buildDispatchRecord(r, walked, event, ctx, predicted, outcome) {
948
1082
  outcome
949
1083
  };
950
1084
  }
951
-
952
- // src/interactions/dispatcher/dispatcher.ts
953
- function pointerGestureId(pointerId) {
954
- return `pointer-${pointerId ?? "mouse"}`;
955
- }
956
- var EMPTY_ENGAGED2 = /* @__PURE__ */ new Set();
957
- function createDispatcher(opts) {
958
- const inFlightHandles = /* @__PURE__ */ new Map();
959
- const inFlightOwners = /* @__PURE__ */ new Map();
960
- const inFlightActions = /* @__PURE__ */ new Map();
961
- const dragOrigins = /* @__PURE__ */ new Map();
962
- const subscribers = /* @__PURE__ */ new Set();
963
- let version = 0;
964
- function notify() {
965
- version++;
966
- for (const fn of subscribers) fn();
967
- }
968
- const dragPoints = /* @__PURE__ */ new Map();
969
- const pinchStartSpreads = /* @__PURE__ */ new Map();
970
- let uiOngoingSeq = 0;
971
- const uiOngoingByAction = /* @__PURE__ */ new Map();
972
- function isEmptyOngoingHandle(h) {
973
- return !h.onMove && !h.onEnd && !h.overlay && !h.previewIds && !h.previewPose;
974
- }
975
- let routedView;
976
- function buildUiInvocationCtx(deps, params) {
977
- return {
978
- world: { x: 0, y: 0 },
979
- modifiers: { alt: false, ctrl: false, meta: false, shift: false },
980
- deps,
981
- ...params !== void 0 ? { params } : {}
982
- };
983
- }
984
- function buildInvocationCtx(event, deps, gestureId) {
985
- const modifiers = {
986
- alt: event.altKey,
987
- ctrl: event.ctrlKey,
988
- meta: event.metaKey,
989
- shift: event.shiftKey
990
- };
991
- const base = {
992
- world: { x: 0, y: 0 },
993
- modifiers,
994
- deps,
995
- ...routedView !== void 0 ? { viewId: routedView } : {}
996
- };
997
- const clientPoint = (ev) => {
998
- const cx = ev.clientX;
999
- const cy = ev.clientY;
1000
- return cx !== void 0 && cy !== void 0 ? { x: cx, y: cy } : void 0;
1001
- };
1002
- const screen = clientPoint(event);
1003
- if (screen) base.screen = screen;
1004
- if (event.kind === "key") {
1005
- base.key = { key: event.key, repeat: event.repeat ?? false };
1006
- } else if (event.kind === "key-held") {
1007
- base.key = { key: event.key, repeat: false };
1008
- } else if (event.kind === "wheel") {
1009
- base.wheel = { deltaX: event.deltaX, deltaY: event.deltaY, deltaZ: 0 };
1010
- } else if (event.kind === "pinch") {
1011
- base.pinch = { scale: event.scale, rotation: event.rotation };
1012
- } else if (event.kind === "pointerdown" || event.kind === "click" || event.kind === "doubleclick" || event.kind === "contextmenu") {
1013
- const sx = event.x ?? (event.kind === "click" ? event.pressX : void 0) ?? 0;
1014
- const sy = event.y ?? (event.kind === "click" ? event.pressY : void 0) ?? 0;
1015
- base.world = { x: sx, y: sy };
1016
- const affordance = event.kind === "pointerdown" ? event.affordance : void 0;
1017
- base.drag = {
1018
- start: { x: sx, y: sy },
1019
- current: { x: sx, y: sy },
1020
- delta: { x: 0, y: 0 },
1021
- ...affordance !== void 0 ? { affordance } : {}
1022
- };
1023
- } else if (event.kind === "pointermove" || event.kind === "pointerup") {
1024
- const cx = event.x;
1025
- const cy = event.y;
1026
- base.world = { x: cx, y: cy };
1027
- const origin = gestureId ? dragOrigins.get(gestureId) : void 0;
1028
- const ox = origin?.x ?? cx;
1029
- const oy = origin?.y ?? cy;
1030
- const live = gestureId ? dragPoints.get(gestureId) : void 0;
1031
- const points = live && event.kind === "pointerup" ? [...live] : live;
1032
- const eventClientX = event.clientX;
1033
- const eventClientY = event.clientY;
1034
- const hasClient = eventClientX !== void 0 && eventClientY !== void 0 && origin?.clientX !== void 0 && origin?.clientY !== void 0;
1035
- base.drag = {
1036
- start: { x: ox, y: oy },
1037
- current: { x: cx, y: cy },
1038
- delta: { x: cx - ox, y: cy - oy },
1039
- ...hasClient ? { screenDelta: { x: eventClientX - origin.clientX, y: eventClientY - origin.clientY } } : {},
1040
- ...points !== void 0 ? { points } : {}
1041
- };
1042
- } else if (event.kind === "multitouch") {
1043
- const centroid = event.centroid ?? { x: 0, y: 0 };
1044
- const spread = event.spread ?? 1;
1045
- const startSpread = gestureId ? pinchStartSpreads.get(gestureId) : void 0;
1046
- base.multiTouch = {
1047
- centroid,
1048
- spread,
1049
- rotation: 0,
1050
- ...startSpread !== void 0 && event.spread !== void 0 ? { pinch: { startSpread, currentSpread: spread, centroid } } : {}
1051
- };
1052
- }
1053
- return base;
1054
- }
1055
- function gestureIdFor(event) {
1056
- if (event.kind === "key-held") {
1057
- return `key-held-${event.code ?? event.key}`;
1058
- }
1059
- if (event.kind === "pointerdown" || event.kind === "pointermove" || event.kind === "pointerup" || event.kind === "pointercancel") {
1060
- return pointerGestureId(event.pointerId);
1061
- }
1062
- if (event.kind === "multitouch") {
1063
- return `multitouch-${event.fingers}`;
1064
- }
1065
- return `ongoing-${event.kind}`;
1066
- }
1067
- function assembleScopedBindings(ctx) {
1068
- const allowedCapabilities = ctx.getRuleCtx?.()?.allowedCapabilities;
1069
- const { ordered, state } = assembleEntries(ctx.entriesById.values(), {
1070
- focusedId: ctx.activeToolId,
1071
- hotkeyStack: ctx.hotkeyStack,
1072
- ...allowedCapabilities ? { allowedCapabilities } : {}
1073
- });
1074
- const view = ctx.viewId ?? null;
1075
- const result = scopeBindings(ordered, state).filter((sb) => liveInView(sb.binding, view));
1076
- for (const action of ctx.actions.list()) {
1077
- const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
1078
- for (const binding of actionBindings(action)) {
1079
- if (!liveInView(binding, view)) continue;
1080
- result.push({ binding, scope: targetScope, ownerId: null });
1081
- }
1082
- }
1083
- return result;
1084
- }
1085
- function snapshotEngagedChannels() {
1086
- if (inFlightOwners.size === 0) return EMPTY_ENGAGED2;
1087
- const out = /* @__PURE__ */ new Set();
1088
- for (const owner of inFlightOwners.values()) {
1089
- if (owner != null) out.add(owner);
1090
- }
1091
- return out;
1092
- }
1093
- function buildActionMap(registry) {
1094
- const map = /* @__PURE__ */ new Map();
1095
- for (const action of registry.list()) {
1096
- map.set(action.id, action);
1097
- }
1098
- return map;
1099
- }
1100
- function runOnEnd(gestureId, handle, endCtx, reason) {
1101
- try {
1102
- handle.onEnd?.(endCtx, reason);
1103
- } catch (err) {
1104
- console.error(`weasel dispatcher: gesture "${gestureId}" threw on onEnd`, err);
1105
- }
1106
- }
1107
- function endGesture(gestureId, event, reason) {
1108
- const handle = inFlightHandles.get(gestureId);
1109
- if (!handle) return false;
1110
- const endCtx = buildInvocationCtx(event, {}, gestureId);
1111
- inFlightHandles.delete(gestureId);
1112
- inFlightOwners.delete(gestureId);
1113
- inFlightActions.delete(gestureId);
1114
- dragOrigins.delete(gestureId);
1115
- dragPoints.delete(gestureId);
1116
- pinchStartSpreads.delete(gestureId);
1117
- runOnEnd(gestureId, handle, endCtx, reason);
1118
- return true;
1119
- }
1120
- function handleInput(event, ctx) {
1121
- routedView = ctx.viewId;
1122
- if (event.kind === "key-held" && event.phase === "up") {
1123
- const gestureId = gestureIdFor(event);
1124
- return endGesture(gestureId, event, "commit") ? "handled" : "unhandled";
1125
- }
1126
- if (event.kind === "key-held" && event.phase === "down") {
1127
- const gestureId = gestureIdFor(event);
1128
- if (inFlightHandles.has(gestureId)) {
1129
- return "handled";
1130
- }
1131
- }
1132
- if (event.kind === "pointermove") {
1133
- const gestureId = gestureIdFor(event);
1134
- const handle = inFlightHandles.get(gestureId);
1135
- if (!handle) return "unhandled";
1136
- const pts = dragPoints.get(gestureId);
1137
- if (pts) {
1138
- pts.push({
1139
- x: event.x,
1140
- y: event.y,
1141
- ...event.pressure !== void 0 ? { pressure: event.pressure } : {},
1142
- ...event.tiltX !== void 0 ? { tiltX: event.tiltX } : {},
1143
- ...event.tiltY !== void 0 ? { tiltY: event.tiltY } : {}
1144
- });
1145
- }
1146
- if (handle.onMove) {
1147
- const moveCtx = buildInvocationCtx(event, {}, gestureId);
1148
- handle.onMove(moveCtx);
1149
- return "handled";
1150
- }
1151
- return "unhandled";
1152
- }
1153
- if (event.kind === "pointerup") {
1154
- const gestureId = gestureIdFor(event);
1155
- return endGesture(gestureId, event, "commit") ? "handled" : "unhandled";
1156
- }
1157
- if (event.kind === "pointercancel") {
1158
- const gestureId = gestureIdFor(event);
1159
- return endGesture(gestureId, event, "cancel") ? "handled" : "unhandled";
1160
- }
1161
- if (event.kind === "multitouch" && event.end !== void 0) {
1162
- let ended = false;
1163
- for (const id of [...inFlightHandles.keys()]) {
1164
- if (id.startsWith("multitouch-")) ended = endGesture(id, event, event.end) || ended;
1165
- }
1166
- return ended ? "handled" : "unhandled";
1167
- }
1168
- if (event.kind === "multitouch" && event.centroid !== void 0) {
1169
- const gestureId = gestureIdFor(event);
1170
- const handle = inFlightHandles.get(gestureId);
1171
- if (handle?.onMove) {
1172
- const moveCtx = buildInvocationCtx(event, {}, gestureId);
1173
- handle.onMove(moveCtx);
1174
- return "handled";
1175
- }
1176
- for (const id of [...inFlightHandles.keys()]) {
1177
- if (id.startsWith("multitouch-") && id !== gestureId) endGesture(id, event, "cancel");
1178
- }
1179
- }
1180
- const r = rank(event, ctx, DEV2);
1181
- const walked = walk(r, event, ctx, (match, action, deps) => invoke(event, match, action, deps), false);
1182
- const outcome = walked.stop === "fired" ? "handled" : "unhandled";
1183
- if (DEV2) recordTrace(buildDispatchRecord(r, walked, event, ctx, false, outcome));
1184
- return outcome;
1185
- }
1186
- function invoke(event, match, action, deps) {
1187
- if (action.invoker?.timing === "immediate") {
1188
- try {
1189
- const resolved = resolveParams(match.binding.opts?.params);
1190
- let params;
1191
- if (event.kind === "wheel") {
1192
- params = {
1193
- deltaX: event.deltaX,
1194
- deltaY: event.deltaY,
1195
- clientX: event.clientX,
1196
- clientY: event.clientY,
1197
- affordance: event.affordance,
1198
- ...resolved
1199
- };
1200
- } else if (event.kind === "pinch") {
1201
- params = {
1202
- scale: event.scale,
1203
- rotation: event.rotation,
1204
- clientX: event.clientX,
1205
- clientY: event.clientY,
1206
- affordance: event.affordance,
1207
- ...resolved
1208
- };
1209
- } else if (event.kind === "click" || event.kind === "doubleclick") {
1210
- params = {
1211
- worldX: event.x,
1212
- worldY: event.y,
1213
- affordance: event.affordance,
1214
- // Press point as well as release point — an action that places
1215
- // geometry at the click wants the former. See `ClickEvent`.
1216
- ...event.kind === "click" ? { pressX: event.pressX, pressY: event.pressY } : {},
1217
- mods: modifiersOf(event),
1218
- ...resolved
1219
- };
1220
- } else if (event.kind === "contextmenu" || event.kind === "longpress") {
1221
- params = {
1222
- worldX: event.x,
1223
- worldY: event.y,
1224
- affordance: event.affordance,
1225
- bodyTarget: event.bodyTarget,
1226
- mods: modifiersOf(event),
1227
- ...resolved
1228
- };
1229
- } else if (event.kind === "pointerdown") {
1230
- params = {
1231
- worldX: event.x,
1232
- worldY: event.y,
1233
- affordance: event.affordance,
1234
- bodyTarget: event.bodyTarget,
1235
- mods: modifiersOf(event),
1236
- ...resolved
1237
- };
1238
- } else if (event.kind === "drop" || event.kind === "paste") {
1239
- params = {
1240
- items: event.items,
1241
- via: event.kind,
1242
- ...event.kind === "drop" && event.x !== void 0 ? { worldX: event.x, worldY: event.y } : {},
1243
- ...resolved
1244
- };
1245
- } else {
1246
- params = resolved;
1247
- }
1248
- action.invoker.run(deps, params);
1249
- } catch (err) {
1250
- console.error(`weasel dispatcher: action "${action.id}" invoker threw`, err);
1251
- }
1252
- return "fired";
1253
- }
1254
- if (action.invoker?.timing === "ongoing") {
1255
- if (event.kind === "pointerdown" && event.stage === "press") {
1256
- console.error(
1257
- `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.`
1258
- );
1259
- return "misbound";
1260
- }
1261
- const gestureId = gestureIdFor(event);
1085
+ function createGestureFacts() {
1086
+ const origins = /* @__PURE__ */ new Map();
1087
+ const points = /* @__PURE__ */ new Map();
1088
+ const startSpreads = /* @__PURE__ */ new Map();
1089
+ const turns = /* @__PURE__ */ new Map();
1090
+ return {
1091
+ begin(gestureId, event) {
1262
1092
  if (event.kind === "pointerdown") {
1263
- dragOrigins.set(gestureId, {
1093
+ origins.set(gestureId, {
1264
1094
  x: event.x ?? 0,
1265
1095
  y: event.y ?? 0,
1266
1096
  ...event.clientX !== void 0 ? { clientX: event.clientX } : {},
1267
1097
  ...event.clientY !== void 0 ? { clientY: event.clientY } : {}
1268
1098
  });
1269
- dragPoints.set(gestureId, [{
1270
- x: event.x ?? 0,
1271
- y: event.y ?? 0,
1272
- ...event.pressure !== void 0 ? { pressure: event.pressure } : {},
1273
- ...event.tiltX !== void 0 ? { tiltX: event.tiltX } : {},
1274
- ...event.tiltY !== void 0 ? { tiltY: event.tiltY } : {}
1275
- }]);
1099
+ points.set(gestureId, [{ x: event.x ?? 0, y: event.y ?? 0, ...stylusOf(event) }]);
1276
1100
  }
1277
1101
  if (event.kind === "multitouch" && event.spread !== void 0) {
1278
- pinchStartSpreads.set(gestureId, event.spread);
1279
- }
1280
- const invCtx = buildInvocationCtx(event, deps, gestureId);
1281
- const handle = action.invoker.start(invCtx, match.binding.opts);
1282
- if (isEmptyOngoingHandle(handle)) {
1283
- if (event.kind === "pointerdown") {
1284
- dragOrigins.delete(gestureId);
1285
- dragPoints.delete(gestureId);
1286
- }
1287
- if (event.kind === "multitouch") {
1288
- pinchStartSpreads.delete(gestureId);
1289
- }
1290
- return "empty-handle";
1102
+ startSpreads.set(gestureId, event.spread);
1291
1103
  }
1292
- inFlightHandles.set(gestureId, handle);
1293
- inFlightOwners.set(gestureId, match.ownerId);
1294
- inFlightActions.set(gestureId, action);
1295
- return "fired";
1296
- }
1297
- return "fired";
1298
- }
1299
- function rank(event, ctx, collectBarred) {
1300
- const view = ctx.viewId ?? null;
1301
- const outcome = matchSortedWithBarred(
1302
- event,
1303
- assembleScopedBindings(ctx),
1304
- ctx.isMac,
1305
- snapshotEngagedChannels(),
1306
- { collectBarred }
1307
- );
1308
- const sorted = preferViewScoped(outcome.matches, view);
1309
- const actionMap = buildActionMap(ctx.actions);
1310
- const ruleCtx = ruleCtxOf(ctx);
1311
- const held = /* @__PURE__ */ new Map();
1312
- const holds = (a) => {
1313
- if (ruleCtx === void 0) return true;
1314
- let h = held.get(a);
1315
- if (h === void 0) held.set(a, h = isEligible(a, ruleCtx));
1316
- return h;
1317
- };
1318
- const ordered = ruleCtx ? preferContextual(sorted, (id) => actionMap.get(id), holds, view) : sorted;
1319
- return {
1320
- ordered,
1321
- barred: outcome.barred,
1322
- ...outcome.claimOwner !== void 0 ? { claimOwner: outcome.claimOwner } : {},
1323
- actionMap,
1324
- ruleCtx,
1325
- eligible: (m) => {
1326
- const a = actionMap.get(m.binding.actionId);
1327
- return a === void 0 || holds(a);
1104
+ if (event.kind === "multitouch" && event.angle !== void 0) {
1105
+ turns.set(gestureId, { last: event.angle, total: 0 });
1328
1106
  }
1329
- };
1330
- }
1331
- function walk(r, event, ctx, attempt, evaluateShadowed) {
1332
- const steps = [];
1333
- const tried = /* @__PURE__ */ new Set();
1334
- let stop = "exhausted";
1335
- let winner = null;
1336
- for (const match of r.ordered) {
1337
- if (!r.eligible(match)) continue;
1338
- const push = (step) => {
1339
- steps.push({ match, step });
1107
+ },
1108
+ sample(gestureId, event) {
1109
+ points.get(gestureId)?.push({ x: event.x, y: event.y, ...stylusOf(event) });
1110
+ },
1111
+ turn(gestureId, event) {
1112
+ const turn = turns.get(gestureId);
1113
+ if (!turn || event.angle === void 0) return;
1114
+ const step = event.angle - turn.last;
1115
+ turn.total += step - 360 * Math.round(step / 360);
1116
+ turn.last = event.angle;
1117
+ },
1118
+ of(gestureId) {
1119
+ if (gestureId === void 0) return {};
1120
+ const origin = origins.get(gestureId);
1121
+ const trail = points.get(gestureId);
1122
+ const startSpread = startSpreads.get(gestureId);
1123
+ const turn = turns.get(gestureId);
1124
+ return {
1125
+ ...origin !== void 0 ? { origin } : {},
1126
+ ...trail !== void 0 ? { points: trail } : {},
1127
+ ...startSpread !== void 0 ? { startSpread } : {},
1128
+ ...turn !== void 0 ? { rotation: turn.total } : {}
1340
1129
  };
1341
- const actionId = match.binding.actionId;
1342
- if (tried.has(actionId)) {
1343
- push({ kind: "duplicate" });
1344
- continue;
1345
- }
1346
- if (stop !== "exhausted" && !evaluateShadowed) {
1347
- push({ kind: "not-asked" });
1348
- continue;
1349
- }
1350
- tried.add(actionId);
1351
- const action = r.actionMap.get(actionId);
1352
- if (!action) {
1353
- if (attempt) {
1354
- console.warn(
1355
- `weasel dispatcher: binding resolved actionId "${actionId}" which has no registered action. Skipping. (misconfiguration)`
1356
- );
1357
- }
1358
- push({ kind: "no-such-action" });
1359
- continue;
1360
- }
1361
- const deps = buildDepsFromRequires(action, ctx.depRegistry);
1362
- const enabled = action.enabled ? action.enabled(deps, worldPointOf(event)) : true;
1363
- if (enabled !== true) {
1364
- push({ kind: "declined", reason: String(enabled) });
1365
- continue;
1366
- }
1367
- if (stop !== "exhausted") {
1368
- push({ kind: "outranked" });
1369
- continue;
1130
+ },
1131
+ drop(gestureId) {
1132
+ origins.delete(gestureId);
1133
+ points.delete(gestureId);
1134
+ startSpreads.delete(gestureId);
1135
+ turns.delete(gestureId);
1136
+ },
1137
+ clear() {
1138
+ origins.clear();
1139
+ points.clear();
1140
+ startSpreads.clear();
1141
+ turns.clear();
1142
+ }
1143
+ };
1144
+ }
1145
+ var EMPTY_ENGAGED2 = /* @__PURE__ */ new Set();
1146
+ function createInFlightGestures() {
1147
+ const handles = /* @__PURE__ */ new Map();
1148
+ const owners = /* @__PURE__ */ new Map();
1149
+ const actions = /* @__PURE__ */ new Map();
1150
+ const forget = (gestureId) => {
1151
+ handles.delete(gestureId);
1152
+ owners.delete(gestureId);
1153
+ actions.delete(gestureId);
1154
+ };
1155
+ return {
1156
+ handles,
1157
+ open(gestureId, handle, ownerId, action) {
1158
+ handles.set(gestureId, handle);
1159
+ owners.set(gestureId, ownerId);
1160
+ if (action) actions.set(gestureId, action);
1161
+ },
1162
+ take(gestureId) {
1163
+ const handle = handles.get(gestureId);
1164
+ forget(gestureId);
1165
+ return handle;
1166
+ },
1167
+ takeAll() {
1168
+ const all = [...handles];
1169
+ handles.clear();
1170
+ owners.clear();
1171
+ actions.clear();
1172
+ return all;
1173
+ },
1174
+ engagedOwners() {
1175
+ if (owners.size === 0) return EMPTY_ENGAGED2;
1176
+ const out = /* @__PURE__ */ new Set();
1177
+ for (const owner of owners.values()) {
1178
+ if (owner != null) out.add(owner);
1370
1179
  }
1371
- if (!attempt) {
1372
- push({ kind: "would-fire" });
1373
- stop = "fired";
1374
- winner = { match, action };
1375
- continue;
1180
+ return out;
1181
+ },
1182
+ cursor() {
1183
+ let cursor = null;
1184
+ for (const action of actions.values()) {
1185
+ const spec = action.activeCursor ?? action.cursor;
1186
+ if (spec !== void 0 && resolveCursor(spec)) cursor = spec;
1376
1187
  }
1377
- const result = attempt(match, action, deps);
1378
- if (result === "empty-handle") {
1379
- push({ kind: "empty-handle" });
1380
- continue;
1188
+ return cursor;
1189
+ },
1190
+ latest() {
1191
+ let lastId = null;
1192
+ let lastHandle = null;
1193
+ for (const [id, handle] of handles) {
1194
+ lastId = id;
1195
+ lastHandle = handle;
1381
1196
  }
1382
- push({ kind: result === "fired" ? "fired" : "misbound" });
1383
- stop = result;
1384
- winner = { match, action };
1197
+ if (lastHandle === null) return { kind: null, id: null };
1198
+ return { kind: lastHandle.kind ?? null, id: lastId };
1385
1199
  }
1386
- return { steps, stop, winner };
1387
- }
1388
- function resolveAll(event, ctx, resolveOpts) {
1389
- const evaluateShadowed = resolveOpts?.evaluateShadowed ?? false;
1390
- const r = rank(event, ctx, false);
1391
- if (r.ordered.length === 0) return [];
1392
- const walked = walk(r, event, ctx, null, evaluateShadowed);
1393
- const stepOf = new Map(walked.steps.map((s) => [s.match, s.step]));
1394
- const verdictByAction = /* @__PURE__ */ new Map();
1395
- let fired = false;
1396
- const out = [];
1397
- for (const match of r.ordered) {
1398
- const actionId = match.binding.actionId;
1399
- const action = r.actionMap.get(actionId);
1400
- if (!action) continue;
1401
- let verdict = verdictByAction.get(actionId);
1402
- if (verdict === void 0) {
1403
- if (!r.eligible(match)) {
1404
- verdict = fired && !evaluateShadowed ? { kind: "shadowed" } : { kind: "ineligible", reason: describeEligible(action.eligible) };
1405
- } else {
1406
- const step = stepOf.get(match);
1407
- if (step?.kind === "would-fire") {
1408
- verdict = { kind: "would-fire" };
1409
- fired = true;
1410
- } else if (step?.kind === "declined") {
1411
- verdict = { kind: "disabled", reason: step.reason };
1412
- } else {
1413
- verdict = { kind: "shadowed" };
1414
- }
1415
- }
1416
- verdictByAction.set(actionId, verdict);
1417
- } else if (verdict.kind === "would-fire") {
1418
- verdict = { kind: "shadowed" };
1419
- }
1420
- out.push({
1421
- actionId,
1422
- action,
1423
- binding: match.binding,
1424
- scope: match.scope,
1425
- ownerId: match.ownerId,
1426
- specificity: specificity(match.binding.spec),
1427
- verdict
1428
- });
1200
+ };
1201
+ }
1202
+ function isEmptyOngoingHandle(h) {
1203
+ return !h.onMove && !h.onEnd && !h.overlay && !h.previewIds && !h.previewPose;
1204
+ }
1205
+
1206
+ // src/interactions/dispatcher/invocationCtx.ts
1207
+ function uiInvocationCtx(deps, params) {
1208
+ return {
1209
+ world: { x: 0, y: 0 },
1210
+ modifiers: { alt: false, ctrl: false, meta: false, shift: false },
1211
+ deps,
1212
+ ...params !== void 0 ? { params } : {}
1213
+ };
1214
+ }
1215
+ function clientPoint(ev) {
1216
+ const cx = ev.clientX;
1217
+ const cy = ev.clientY;
1218
+ return cx !== void 0 && cy !== void 0 ? { x: cx, y: cy } : void 0;
1219
+ }
1220
+ function pressDrag(event) {
1221
+ const sx = event.x ?? (event.kind === "click" ? event.pressX : void 0) ?? 0;
1222
+ const sy = event.y ?? (event.kind === "click" ? event.pressY : void 0) ?? 0;
1223
+ const affordance = event.kind === "pointerdown" ? event.affordance : void 0;
1224
+ return {
1225
+ world: { x: sx, y: sy },
1226
+ drag: {
1227
+ start: { x: sx, y: sy },
1228
+ current: { x: sx, y: sy },
1229
+ delta: { x: 0, y: 0 },
1230
+ ...affordance !== void 0 ? { affordance } : {}
1429
1231
  }
1430
- return out;
1431
- }
1432
- function resolveOnly(event, ctx) {
1433
- const r = rank(event, ctx, false);
1434
- const { winner } = walk(r, event, ctx, null, false);
1435
- if (!winner) return null;
1436
- return {
1437
- actionId: winner.action.id,
1438
- action: winner.action,
1439
- scope: winner.match.scope,
1440
- ownerId: winner.match.ownerId
1441
- };
1442
- }
1443
- function explain(event, ctx, explainOpts) {
1444
- const r = rank(event, ctx, true);
1445
- const walked = walk(r, event, ctx, null, explainOpts?.evaluateShadowed ?? false);
1446
- return buildDispatchRecord(r, walked, event, ctx, true, walked.winner ? "handled" : "unhandled");
1447
- }
1448
- function cancelAll(reason) {
1449
- const stubCtx = {
1450
- world: { x: 0, y: 0 },
1451
- modifiers: { alt: false, ctrl: false, meta: false, shift: false },
1452
- deps: {}
1453
- };
1454
- const ending = [...inFlightHandles];
1455
- inFlightHandles.clear();
1456
- inFlightOwners.clear();
1457
- inFlightActions.clear();
1458
- uiOngoingByAction.clear();
1459
- dragOrigins.clear();
1460
- dragPoints.clear();
1461
- pinchStartSpreads.clear();
1462
- for (const [gestureId, handle] of ending) runOnEnd(gestureId, handle, stubCtx, reason);
1463
- }
1464
- function inFlight() {
1465
- return inFlightHandles;
1466
- }
1467
- function inFlightCursor() {
1468
- let cursor = null;
1469
- for (const action of inFlightActions.values()) {
1470
- const spec = action.activeCursor ?? action.cursor;
1471
- if (spec !== void 0 && resolveCursor(spec)) cursor = spec;
1232
+ };
1233
+ }
1234
+ function pumpDrag(event, facts) {
1235
+ const cx = event.x;
1236
+ const cy = event.y;
1237
+ const origin = facts.origin;
1238
+ const ox = origin?.x ?? cx;
1239
+ const oy = origin?.y ?? cy;
1240
+ const live = facts.points;
1241
+ const points = live && event.kind === "pointerup" ? [...live] : live;
1242
+ const eventClientX = event.clientX;
1243
+ const eventClientY = event.clientY;
1244
+ const hasClient = eventClientX !== void 0 && eventClientY !== void 0 && origin?.clientX !== void 0 && origin?.clientY !== void 0;
1245
+ return {
1246
+ world: { x: cx, y: cy },
1247
+ drag: {
1248
+ start: { x: ox, y: oy },
1249
+ current: { x: cx, y: cy },
1250
+ delta: { x: cx - ox, y: cy - oy },
1251
+ ...hasClient ? { screenDelta: { x: eventClientX - origin.clientX, y: eventClientY - origin.clientY } } : {},
1252
+ ...points !== void 0 ? { points } : {}
1472
1253
  }
1473
- return cursor;
1474
- }
1475
- function getInFlightHandles() {
1476
- return inFlightHandles.values();
1477
- }
1478
- function subscribe(fn) {
1479
- subscribers.add(fn);
1480
- return () => {
1481
- subscribers.delete(fn);
1482
- };
1483
- }
1484
- function getVersion() {
1485
- return version;
1254
+ };
1255
+ }
1256
+ function multiTouchOf(event, facts) {
1257
+ const centroid = event.centroid ?? { x: 0, y: 0 };
1258
+ const spread = event.spread ?? 1;
1259
+ const { startSpread } = facts;
1260
+ return {
1261
+ centroid,
1262
+ spread,
1263
+ rotation: facts.rotation ?? 0,
1264
+ // Pinch geometry, on a move of a running gesture.
1265
+ ...startSpread !== void 0 && event.spread !== void 0 ? { pinch: { startSpread, currentSpread: spread, centroid } } : {}
1266
+ };
1267
+ }
1268
+ function buildInvocationCtx(event, deps, viewId, facts = {}) {
1269
+ const base = {
1270
+ world: { x: 0, y: 0 },
1271
+ modifiers: { alt: event.altKey, ctrl: event.ctrlKey, meta: event.metaKey, shift: event.shiftKey },
1272
+ deps,
1273
+ ...viewId !== void 0 ? { viewId } : {}
1274
+ };
1275
+ const screen = clientPoint(event);
1276
+ if (screen) base.screen = screen;
1277
+ if (event.kind === "key") {
1278
+ base.key = { key: event.key, repeat: event.repeat ?? false };
1279
+ } else if (event.kind === "key-held") {
1280
+ base.key = { key: event.key, repeat: false };
1281
+ } else if (event.kind === "wheel") {
1282
+ base.wheel = { deltaX: event.deltaX, deltaY: event.deltaY, deltaZ: 0 };
1283
+ } else if (event.kind === "pinch") {
1284
+ base.pinch = { scale: event.scale, rotation: event.rotation };
1285
+ } else if (event.kind === "pointerdown" || event.kind === "click" || event.kind === "doubleclick" || event.kind === "contextmenu") {
1286
+ Object.assign(base, pressDrag(event));
1287
+ } else if (event.kind === "pointermove" || event.kind === "pointerup") {
1288
+ Object.assign(base, pumpDrag(event, facts));
1289
+ } else if (event.kind === "multitouch") {
1290
+ base.multiTouch = multiTouchOf(event, facts);
1291
+ }
1292
+ return base;
1293
+ }
1294
+
1295
+ // src/interactions/dispatcher/gestureRun.ts
1296
+ var ctxFor = (run, event, deps, gestureId) => buildInvocationCtx(event, deps, run.viewId(), run.facts.of(gestureId));
1297
+ function runOnEnd(gestureId, handle, endCtx, reason) {
1298
+ try {
1299
+ handle.onEnd?.(endCtx, reason);
1300
+ } catch (err) {
1301
+ console.error(`weasel dispatcher: gesture "${gestureId}" threw on onEnd`, err);
1486
1302
  }
1487
- function getActiveAction() {
1488
- let lastId = null;
1489
- let lastHandle = null;
1490
- for (const [id, handle] of inFlightHandles) {
1491
- lastId = id;
1492
- lastHandle = handle;
1303
+ }
1304
+ function endGesture(run, gestureId, event, reason) {
1305
+ if (!run.inFlight.handles.has(gestureId)) return false;
1306
+ const endCtx = ctxFor(run, event, {}, gestureId);
1307
+ const handle = run.inFlight.take(gestureId);
1308
+ run.facts.drop(gestureId);
1309
+ runOnEnd(gestureId, handle, endCtx, reason);
1310
+ return true;
1311
+ }
1312
+ function endMultiTouch(run, event, reason, except) {
1313
+ let ended = false;
1314
+ for (const id of [...run.inFlight.handles.keys()]) {
1315
+ if (isMultiTouchGestureId(id) && id !== except) ended = endGesture(run, id, event, reason) || ended;
1316
+ }
1317
+ return ended;
1318
+ }
1319
+ function endAllGestures(run, reason) {
1320
+ const stubCtx = uiInvocationCtx({});
1321
+ const ending = run.inFlight.takeAll();
1322
+ run.facts.clear();
1323
+ for (const [gestureId, handle] of ending) runOnEnd(gestureId, handle, stubCtx, reason);
1324
+ }
1325
+ function pumpGesture(run, event) {
1326
+ const { inFlight } = run;
1327
+ const ended = (reason) => endGesture(run, gestureIdFor(event), event, reason) ? "handled" : "unhandled";
1328
+ if (event.kind === "key-held" && event.phase === "up") return ended("commit");
1329
+ if (event.kind === "key-held" && event.phase === "down") {
1330
+ return inFlight.handles.has(gestureIdFor(event)) ? "handled" : void 0;
1331
+ }
1332
+ if (event.kind === "pointermove") {
1333
+ const gestureId = gestureIdFor(event);
1334
+ const handle = inFlight.handles.get(gestureId);
1335
+ if (!handle) return "unhandled";
1336
+ run.facts.sample(gestureId, event);
1337
+ if (!handle.onMove) return "unhandled";
1338
+ handle.onMove(ctxFor(run, event, {}, gestureId));
1339
+ return "handled";
1340
+ }
1341
+ if (event.kind === "pointerup") return ended("commit");
1342
+ if (event.kind === "pointercancel") return ended("cancel");
1343
+ if (event.kind === "multitouch" && event.end !== void 0) {
1344
+ return endMultiTouch(run, event, event.end) ? "handled" : "unhandled";
1345
+ }
1346
+ if (event.kind === "multitouch" && event.centroid !== void 0) {
1347
+ const gestureId = gestureIdFor(event);
1348
+ const handle = inFlight.handles.get(gestureId);
1349
+ if (handle?.onMove) {
1350
+ run.facts.turn(gestureId, event);
1351
+ handle.onMove(ctxFor(run, event, {}, gestureId));
1352
+ return "handled";
1493
1353
  }
1494
- if (lastHandle === null) return { kind: null, id: null };
1495
- return { kind: lastHandle.kind ?? null, id: lastId };
1354
+ endMultiTouch(run, event, "cancel", gestureId);
1496
1355
  }
1497
- const handleInputWithNotify = (event, ctx) => {
1498
- const before = inFlightHandles.size;
1499
- try {
1500
- return handleInput(event, ctx);
1501
- } finally {
1502
- if (before > 0 || inFlightHandles.size > 0) notify();
1503
- }
1504
- };
1505
- const cancelAllWithNotify = (reason) => {
1506
- const before = inFlightHandles.size;
1356
+ return void 0;
1357
+ }
1358
+ function invokeAction(run, event, match, action, deps) {
1359
+ if (action.invoker?.timing === "immediate") {
1507
1360
  try {
1508
- cancelAll(reason);
1509
- } finally {
1510
- if (before > 0) notify();
1361
+ const params = immediateParams(event, resolveParams(match.binding.opts?.params));
1362
+ action.invoker.run(deps, params);
1363
+ } catch (err) {
1364
+ console.error(`weasel dispatcher: action "${action.id}" invoker threw`, err);
1511
1365
  }
1512
- };
1513
- function beginUiOngoing(actionId, deps, params) {
1514
- const getAction = opts?.getAction;
1515
- if (!getAction) {
1516
- console.warn("weasel dispatcher: beginUiOngoing called but no getAction was provided to createDispatcher");
1517
- return null;
1366
+ return "fired";
1367
+ }
1368
+ if (action.invoker?.timing === "ongoing") {
1369
+ if (event.kind === "hover" || event.kind === "pointerdown" && event.stage === "press") {
1370
+ const spec = event.kind === "hover" ? "hover spec, which has no press to own" : "pointerDown spec, which fires at press time";
1371
+ console.error(
1372
+ `weasel dispatcher: action "${action.id}" has an ongoing invoker but is bound to a ${spec}. Bind it to a drag spec, or give the action an immediate invoker.`
1373
+ );
1374
+ return "misbound";
1518
1375
  }
1519
- const action = getAction(actionId);
1520
- if (!action || !action.invoker || action.invoker.timing !== "ongoing") {
1521
- return null;
1376
+ const gestureId = gestureIdFor(event);
1377
+ run.facts.begin(gestureId, event);
1378
+ const handle = action.invoker.start(ctxFor(run, event, deps, gestureId), match.binding.opts);
1379
+ if (isEmptyOngoingHandle(handle)) {
1380
+ run.facts.drop(gestureId);
1381
+ return "empty-handle";
1522
1382
  }
1523
- const prevGestureId = uiOngoingByAction.get(actionId);
1524
- if (prevGestureId !== void 0) {
1525
- const prevHandle = inFlightHandles.get(prevGestureId);
1526
- if (prevHandle?.onEnd) {
1527
- try {
1528
- prevHandle.onEnd(buildUiInvocationCtx(deps), "commit");
1529
- } catch (e) {
1530
- console.error(`weasel dispatcher: prior UI handle for "${actionId}" threw on auto-commit`, e);
1531
- }
1383
+ run.inFlight.open(gestureId, handle, match.ownerId, action);
1384
+ return "fired";
1385
+ }
1386
+ return "fired";
1387
+ }
1388
+
1389
+ // src/interactions/dispatcher/uiOngoing.ts
1390
+ function createUiOngoing(inFlight, notify, getAction) {
1391
+ let seq = 0;
1392
+ const byAction = /* @__PURE__ */ new Map();
1393
+ function commitPrior(actionId, deps) {
1394
+ const prevGestureId = byAction.get(actionId);
1395
+ if (prevGestureId === void 0) return;
1396
+ const prevHandle = inFlight.handles.get(prevGestureId);
1397
+ if (prevHandle?.onEnd) {
1398
+ try {
1399
+ prevHandle.onEnd(uiInvocationCtx(deps), "commit");
1400
+ } catch (e) {
1401
+ console.error(`weasel dispatcher: prior UI handle for "${actionId}" threw on auto-commit`, e);
1532
1402
  }
1533
- inFlightHandles.delete(prevGestureId);
1534
- inFlightOwners.delete(prevGestureId);
1535
- uiOngoingByAction.delete(actionId);
1536
1403
  }
1537
- const gestureId = `ui-${actionId}-${++uiOngoingSeq}`;
1538
- let handle;
1539
- try {
1540
- handle = action.invoker.start(buildUiInvocationCtx(deps, params), { params });
1541
- } catch (e) {
1542
- console.error(`weasel dispatcher: action "${actionId}" threw on start`, e);
1543
- return null;
1544
- }
1545
- if (isEmptyOngoingHandle(handle)) {
1546
- return null;
1547
- }
1548
- inFlightHandles.set(gestureId, handle);
1549
- inFlightOwners.set(gestureId, null);
1550
- uiOngoingByAction.set(actionId, gestureId);
1551
- notify();
1404
+ inFlight.take(prevGestureId);
1405
+ byAction.delete(actionId);
1406
+ }
1407
+ function control(actionId, gestureId, handle, deps) {
1552
1408
  let ended = false;
1553
- const live = () => !ended && inFlightHandles.get(gestureId) === handle;
1409
+ const live = () => !ended && inFlight.handles.get(gestureId) === handle;
1554
1410
  return {
1555
1411
  gestureId,
1556
1412
  get active() {
@@ -1560,7 +1416,7 @@ function createDispatcher(opts) {
1560
1416
  if (!live()) return;
1561
1417
  if (!handle.onMove) return;
1562
1418
  try {
1563
- handle.onMove(buildUiInvocationCtx(deps, nextParams));
1419
+ handle.onMove(uiInvocationCtx(deps, nextParams));
1564
1420
  } catch (e) {
1565
1421
  console.error(`weasel dispatcher: action "${actionId}" threw on onMove`, e);
1566
1422
  }
@@ -1574,43 +1430,302 @@ function createDispatcher(opts) {
1574
1430
  ended = true;
1575
1431
  if (handle.onEnd) {
1576
1432
  try {
1577
- handle.onEnd(buildUiInvocationCtx(deps), reason);
1433
+ handle.onEnd(uiInvocationCtx(deps), reason);
1578
1434
  } catch (e) {
1579
1435
  console.error(`weasel dispatcher: action "${actionId}" threw on onEnd`, e);
1580
1436
  }
1581
1437
  }
1582
- inFlightHandles.delete(gestureId);
1583
- inFlightOwners.delete(gestureId);
1584
- inFlightActions.delete(gestureId);
1585
- if (uiOngoingByAction.get(actionId) === gestureId) {
1586
- uiOngoingByAction.delete(actionId);
1587
- }
1438
+ inFlight.take(gestureId);
1439
+ if (byAction.get(actionId) === gestureId) byAction.delete(actionId);
1588
1440
  notify();
1589
1441
  }
1590
1442
  };
1591
1443
  }
1444
+ return {
1445
+ begin(actionId, deps, params) {
1446
+ if (!getAction) {
1447
+ console.warn("weasel dispatcher: beginUiOngoing called but no getAction was provided to createDispatcher");
1448
+ return null;
1449
+ }
1450
+ const action = getAction(actionId);
1451
+ if (!action || !action.invoker || action.invoker.timing !== "ongoing") {
1452
+ return null;
1453
+ }
1454
+ commitPrior(actionId, deps);
1455
+ const gestureId = `ui-${actionId}-${++seq}`;
1456
+ let handle;
1457
+ try {
1458
+ handle = action.invoker.start(uiInvocationCtx(deps, params), { params });
1459
+ } catch (e) {
1460
+ console.error(`weasel dispatcher: action "${actionId}" threw on start`, e);
1461
+ return null;
1462
+ }
1463
+ if (isEmptyOngoingHandle(handle)) return null;
1464
+ inFlight.open(gestureId, handle, null);
1465
+ byAction.set(actionId, gestureId);
1466
+ notify();
1467
+ return control(actionId, gestureId, handle, deps);
1468
+ },
1469
+ clear: () => {
1470
+ byAction.clear();
1471
+ }
1472
+ };
1473
+ }
1474
+
1475
+ // src/interactions/dispatcher/walk.ts
1476
+ function assembleScopedBindings(ctx) {
1477
+ const allowedCapabilities = ctx.getRuleCtx?.()?.allowedCapabilities;
1478
+ const { ordered, state } = assembleEntries(ctx.entriesById.values(), {
1479
+ focusedId: ctx.activeToolId,
1480
+ hotkeyStack: ctx.hotkeyStack,
1481
+ ...allowedCapabilities ? { allowedCapabilities } : {}
1482
+ });
1483
+ const view = ctx.viewId ?? null;
1484
+ const result = scopeBindings(ordered, state).filter((sb) => liveInView(sb.binding, view));
1485
+ for (const action of ctx.actions.list()) {
1486
+ const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
1487
+ for (const binding of actionBindings(action)) {
1488
+ if (!liveInView(binding, view)) continue;
1489
+ result.push({ binding, scope: targetScope, ownerId: null });
1490
+ }
1491
+ }
1492
+ return result;
1493
+ }
1494
+ function buildActionMap(registry) {
1495
+ const map = /* @__PURE__ */ new Map();
1496
+ for (const action of registry.list()) {
1497
+ map.set(action.id, action);
1498
+ }
1499
+ return map;
1500
+ }
1501
+ function rank(event, ctx, engaged, collectBarred) {
1502
+ const view = ctx.viewId ?? null;
1503
+ const outcome = matchSortedWithBarred(
1504
+ event,
1505
+ assembleScopedBindings(ctx),
1506
+ ctx.isMac,
1507
+ engaged,
1508
+ { collectBarred }
1509
+ );
1510
+ const sorted = preferViewScoped(outcome.matches, view);
1511
+ const actionMap = buildActionMap(ctx.actions);
1512
+ const ruleCtx = ruleCtxOf(ctx);
1513
+ const held = /* @__PURE__ */ new Map();
1514
+ const holds = (a) => {
1515
+ if (ruleCtx === void 0) return true;
1516
+ let h = held.get(a);
1517
+ if (h === void 0) held.set(a, h = isEligible(a, ruleCtx));
1518
+ return h;
1519
+ };
1520
+ const ordered = ruleCtx ? preferContextual(sorted, (id) => actionMap.get(id), holds, view) : sorted;
1521
+ return {
1522
+ ordered,
1523
+ barred: outcome.barred,
1524
+ ...outcome.claimOwner !== void 0 ? { claimOwner: outcome.claimOwner } : {},
1525
+ actionMap,
1526
+ ruleCtx,
1527
+ eligible: (m) => {
1528
+ const a = actionMap.get(m.binding.actionId);
1529
+ return a === void 0 || holds(a);
1530
+ }
1531
+ };
1532
+ }
1533
+ function walk(r, event, ctx, attempt, evaluateShadowed) {
1534
+ const steps = [];
1535
+ const tried = /* @__PURE__ */ new Set();
1536
+ let stop = "exhausted";
1537
+ let winner = null;
1538
+ for (const match of r.ordered) {
1539
+ if (!r.eligible(match)) continue;
1540
+ const push = (step) => {
1541
+ steps.push({ match, step });
1542
+ };
1543
+ const actionId = match.binding.actionId;
1544
+ if (tried.has(actionId)) {
1545
+ push({ kind: "duplicate" });
1546
+ continue;
1547
+ }
1548
+ if (stop !== "exhausted" && !evaluateShadowed) {
1549
+ push({ kind: "not-asked" });
1550
+ continue;
1551
+ }
1552
+ tried.add(actionId);
1553
+ const action = r.actionMap.get(actionId);
1554
+ if (!action) {
1555
+ if (attempt) {
1556
+ console.warn(
1557
+ `weasel dispatcher: binding resolved actionId "${actionId}" which has no registered action. Skipping. (misconfiguration)`
1558
+ );
1559
+ }
1560
+ push({ kind: "no-such-action" });
1561
+ continue;
1562
+ }
1563
+ const deps = buildDepsFromRequires(action, ctx.depRegistry);
1564
+ const enabled = action.enabled ? action.enabled(deps, worldPointOf(event), carriedBy(event)) : true;
1565
+ if (enabled !== true) {
1566
+ push({ kind: "declined", reason: String(enabled) });
1567
+ continue;
1568
+ }
1569
+ if (stop !== "exhausted") {
1570
+ push({ kind: "outranked" });
1571
+ continue;
1572
+ }
1573
+ if (!attempt) {
1574
+ push({ kind: "would-fire" });
1575
+ stop = "fired";
1576
+ winner = { match, action };
1577
+ continue;
1578
+ }
1579
+ const result = attempt(match, action, deps);
1580
+ if (result === "empty-handle") {
1581
+ push({ kind: "empty-handle" });
1582
+ continue;
1583
+ }
1584
+ push({ kind: result === "fired" ? "fired" : "misbound" });
1585
+ stop = result;
1586
+ winner = { match, action };
1587
+ }
1588
+ return { steps, stop, winner };
1589
+ }
1590
+ function verdictsOf(r, walked, evaluateShadowed) {
1591
+ const stepOf = new Map(walked.steps.map((s) => [s.match, s.step]));
1592
+ const verdictByAction = /* @__PURE__ */ new Map();
1593
+ let fired = false;
1594
+ const out = [];
1595
+ for (const match of r.ordered) {
1596
+ const actionId = match.binding.actionId;
1597
+ const action = r.actionMap.get(actionId);
1598
+ if (!action) continue;
1599
+ let verdict = verdictByAction.get(actionId);
1600
+ if (verdict === void 0) {
1601
+ if (!r.eligible(match)) {
1602
+ verdict = fired && !evaluateShadowed ? { kind: "shadowed" } : { kind: "ineligible", reason: describeEligible(action.eligible) };
1603
+ } else {
1604
+ const step = stepOf.get(match);
1605
+ if (step?.kind === "would-fire") {
1606
+ verdict = { kind: "would-fire" };
1607
+ fired = true;
1608
+ } else if (step?.kind === "declined") {
1609
+ verdict = { kind: "disabled", reason: step.reason };
1610
+ } else {
1611
+ verdict = { kind: "shadowed" };
1612
+ }
1613
+ }
1614
+ verdictByAction.set(actionId, verdict);
1615
+ } else if (verdict.kind === "would-fire") {
1616
+ verdict = { kind: "shadowed" };
1617
+ }
1618
+ out.push({
1619
+ actionId,
1620
+ action,
1621
+ binding: match.binding,
1622
+ scope: match.scope,
1623
+ ownerId: match.ownerId,
1624
+ specificity: specificity(match.binding.spec),
1625
+ verdict
1626
+ });
1627
+ }
1628
+ return out;
1629
+ }
1630
+
1631
+ // src/interactions/dispatcher/predict.ts
1632
+ function resolveAll(event, ctx, engaged, opts) {
1633
+ const evaluateShadowed = opts?.evaluateShadowed ?? false;
1634
+ const r = rank(event, ctx, engaged, false);
1635
+ if (r.ordered.length === 0) return [];
1636
+ return verdictsOf(r, walk(r, event, ctx, null, evaluateShadowed), evaluateShadowed);
1637
+ }
1638
+ function resolveOnly(event, ctx, engaged) {
1639
+ const r = rank(event, ctx, engaged, false);
1640
+ const { winner } = walk(r, event, ctx, null, false);
1641
+ if (!winner) return null;
1642
+ return {
1643
+ actionId: winner.action.id,
1644
+ action: winner.action,
1645
+ scope: winner.match.scope,
1646
+ ownerId: winner.match.ownerId
1647
+ };
1648
+ }
1649
+ function explain(event, ctx, engaged, opts) {
1650
+ const r = rank(event, ctx, engaged, true);
1651
+ const walked = walk(r, event, ctx, null, opts?.evaluateShadowed ?? false);
1652
+ return buildDispatchRecord(r, walked, event, ctx, true, walked.winner ? "handled" : "unhandled");
1653
+ }
1654
+
1655
+ // src/interactions/dispatcher/dispatcher.ts
1656
+ function createDispatcher(opts) {
1657
+ const inFlight = createInFlightGestures();
1658
+ const facts = createGestureFacts();
1659
+ const changed = createEmitter();
1660
+ let version = 0;
1661
+ function notify() {
1662
+ version++;
1663
+ changed.emit();
1664
+ }
1665
+ const uiOngoing = createUiOngoing(inFlight, notify, opts?.getAction);
1666
+ let routedView;
1667
+ const run = { inFlight, facts, viewId: () => routedView };
1668
+ const invoking = (event) => (match, action, deps) => invokeAction(run, event, match, action, deps);
1669
+ function handleInput(event, ctx) {
1670
+ routedView = ctx.viewId;
1671
+ const pumped = pumpGesture(run, event);
1672
+ if (pumped !== void 0) return pumped;
1673
+ if (event.kind === "hover") return hover(event, ctx);
1674
+ const r = rank(event, ctx, inFlight.engagedOwners(), DEV2);
1675
+ const walked = walk(r, event, ctx, invoking(event), false);
1676
+ const outcome = walked.stop === "fired" ? "handled" : "unhandled";
1677
+ if (DEV2) recordTrace(buildDispatchRecord(r, walked, event, ctx, false, outcome));
1678
+ return outcome;
1679
+ }
1680
+ function hover(event, ctx) {
1681
+ let fired = false;
1682
+ const invoke = invoking(event);
1683
+ walk(rank(event, ctx, inFlight.engagedOwners(), false), event, ctx, (match, action, deps) => {
1684
+ if (invoke(match, action, deps) === "fired") fired = true;
1685
+ return "empty-handle";
1686
+ }, false);
1687
+ return fired ? "handled" : "unhandled";
1688
+ }
1689
+ function cancelAll(reason) {
1690
+ uiOngoing.clear();
1691
+ endAllGestures(run, reason);
1692
+ }
1693
+ const handleInputWithNotify = (event, ctx) => {
1694
+ const before = inFlight.handles.size;
1695
+ try {
1696
+ return handleInput(event, ctx);
1697
+ } finally {
1698
+ if (before > 0 || inFlight.handles.size > 0) notify();
1699
+ }
1700
+ };
1701
+ const cancelAllWithNotify = (reason) => {
1702
+ const before = inFlight.handles.size;
1703
+ try {
1704
+ cancelAll(reason);
1705
+ } finally {
1706
+ if (before > 0) notify();
1707
+ }
1708
+ };
1592
1709
  return {
1593
1710
  handleInput: handleInputWithNotify,
1594
- resolveOnly,
1595
- resolveAll,
1596
- explain,
1711
+ resolveOnly: (event, ctx) => resolveOnly(event, ctx, inFlight.engagedOwners()),
1712
+ resolveAll: (event, ctx, resolveOpts) => resolveAll(event, ctx, inFlight.engagedOwners(), resolveOpts),
1713
+ explain: (event, ctx, explainOpts) => explain(event, ctx, inFlight.engagedOwners(), explainOpts),
1597
1714
  cancelAll: cancelAllWithNotify,
1598
- inFlight,
1599
- inFlightCursor,
1600
- getInFlightHandles,
1601
- subscribe,
1602
- getVersion,
1603
- getActiveAction,
1604
- beginUiOngoing
1715
+ inFlight: () => inFlight.handles,
1716
+ inFlightCursor: inFlight.cursor,
1717
+ getInFlightHandles: () => inFlight.handles.values(),
1718
+ subscribe: changed.subscribe,
1719
+ getVersion: () => version,
1720
+ getActiveAction: inFlight.latest,
1721
+ beginUiOngoing: uiOngoing.begin
1605
1722
  };
1606
1723
  }
1607
-
1608
- // src/interactions/dispatcher/longPressState.ts
1609
- var LONG_PRESS_MS = 500;
1724
+ var LONG_PRESS_HAPTIC_MS = 15;
1610
1725
  var nowMs = () => typeof performance !== "undefined" ? performance.now() : Date.now();
1611
1726
  function createLongPressStore() {
1612
1727
  let current = null;
1613
- const listeners = /* @__PURE__ */ new Set();
1728
+ const changed = createEmitter();
1614
1729
  return {
1615
1730
  get: () => current,
1616
1731
  progress: (now = nowMs()) => {
@@ -1619,16 +1734,11 @@ function createLongPressStore() {
1619
1734
  const t = (now - current.startedAt) / current.duration;
1620
1735
  return t <= 0 ? 0 : t >= 1 ? 1 : t;
1621
1736
  },
1622
- subscribe: (fn) => {
1623
- listeners.add(fn);
1624
- return () => {
1625
- listeners.delete(fn);
1626
- };
1627
- },
1737
+ subscribe: changed.subscribe,
1628
1738
  set: (next) => {
1629
1739
  if (next === current) return;
1630
1740
  current = next;
1631
- for (const fn of [...listeners]) fn();
1741
+ changed.emit();
1632
1742
  }
1633
1743
  };
1634
1744
  }
@@ -1716,47 +1826,29 @@ function openPointerSession(origin, down, cb, opts = {}) {
1716
1826
  function isTool(entry) {
1717
1827
  return !!(entry.eligibility?.focus || entry.eligibility?.offhand);
1718
1828
  }
1719
- function findConflicts(entries) {
1720
- return findConflictsKeyed(entries).map((k) => k.conflict);
1721
- }
1722
- function findConflictsKeyed(entries, rules, modes = KIT_MODES) {
1723
- const rows = buildRouteRegistry(entries);
1724
- const groups = /* @__PURE__ */ new Map();
1725
- for (const entry of rows) {
1726
- for (const arg of argKeysOf(entry)) {
1727
- const base = `${entry.phase}|${entry.gesture}|${arg}|${targetKey(entry)}`;
1728
- for (const state of modifierTieKeys(entry)) {
1729
- const key = `${base}|${state}`;
1730
- const bucket = groups.get(key);
1731
- if (bucket) {
1732
- if (!bucket.members.includes(entry)) bucket.members.push(entry);
1733
- } else groups.set(key, { base, members: [entry] });
1734
- }
1735
- }
1736
- }
1737
- const ruleOf = (entry) => rules?.get(entry.ownerId) ?? ALWAYS;
1738
- const tie = (e, f) => viewsTie(e.views, f.views) && rulesCanHoldTogether(ruleOf(e), ruleOf(f), modes);
1739
- const conflicts = /* @__PURE__ */ new Map();
1740
- for (const { base, members } of groups.values()) {
1741
- const bucket = members.filter((e) => members.some((f) => f !== e && tie(e, f)));
1742
- if (bucket.length < 2) continue;
1743
- const first = bucket[0];
1744
- const key = `${base}|${bucket.map((e) => `${objectId(e.spec)}:${canonicalModifiers(e.modifiers)}`).join(";")}`;
1745
- if (conflicts.has(key)) continue;
1746
- conflicts.set(key, {
1747
- phase: first.phase,
1748
- gesture: first.gesture,
1749
- arg: first.arg,
1750
- target: first.target,
1751
- modifiers: first.modifiers,
1752
- ownerIds: bucket.map((e) => e.ownerId)
1753
- });
1754
- }
1755
- return [...conflicts].map(([key, conflict]) => ({ key, conflict }));
1829
+
1830
+ // src/tools/routing/reflection/conflictKeys.ts
1831
+ var PLATFORMS = ["mac", "other"];
1832
+ function resolveMod(mods, platform) {
1833
+ if (mods.mod === void 0) return mods;
1834
+ const { mod, meta: _meta, ctrl: _ctrl, ...rest } = mods;
1835
+ return { ...rest, [platform === "mac" ? "meta" : "ctrl"]: mod };
1836
+ }
1837
+ function sameOutcome(entries) {
1838
+ const first = entries[0];
1839
+ return entries.every((e) => e.ownerId === first.ownerId && e.actionId === first.actionId && sameParams(e.params, first.params));
1840
+ }
1841
+ function sameParams(a, b) {
1842
+ if (a === b) return true;
1843
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
1844
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
1845
+ const keys = Object.keys(a);
1846
+ return keys.length === Object.keys(b).length && keys.every((k) => sameParams(a[k], b[k]));
1756
1847
  }
1757
- function modifierTieKeys(entry) {
1758
- const optional = VALID_MOD_NAMES.filter((k) => entry.modifiers[k] === "optional");
1759
- const required = VALID_MOD_NAMES.filter((k) => entry.modifiers[k] === "required");
1848
+ function modifierTieKeys(entry, platform) {
1849
+ const mods = resolveMod(entry.modifiers, platform);
1850
+ const optional = VALID_MOD_NAMES.filter((k) => mods[k] === "optional");
1851
+ const required = VALID_MOD_NAMES.filter((k) => mods[k] === "required");
1760
1852
  const keys = [];
1761
1853
  for (let bits = 0; bits < 1 << optional.length; bits++) {
1762
1854
  const held = [...required, ...optional.filter((_, i) => (bits & 1 << i) !== 0)].sort();
@@ -1790,6 +1882,57 @@ function objectId(obj) {
1790
1882
  }
1791
1883
  return id;
1792
1884
  }
1885
+
1886
+ // src/tools/routing/reflection/conflicts.ts
1887
+ function findConflicts(entries) {
1888
+ return findConflictsKeyed(entries).map((k) => k.conflict);
1889
+ }
1890
+ function findConflictsKeyed(entries, rules, modes = KIT_MODES) {
1891
+ const rows = buildRouteRegistry(entries);
1892
+ const groups = /* @__PURE__ */ new Map();
1893
+ for (const entry of rows) {
1894
+ for (const arg of argKeysOf(entry)) {
1895
+ const base = `${entry.phase}|${entry.gesture}|${arg}|${targetKey(entry)}`;
1896
+ for (const platform of PLATFORMS) {
1897
+ for (const state of modifierTieKeys(entry, platform)) {
1898
+ const key = `${platform}|${base}|${state}`;
1899
+ const bucket = groups.get(key);
1900
+ if (bucket) {
1901
+ if (!bucket.members.includes(entry)) bucket.members.push(entry);
1902
+ } else groups.set(key, { base, platform, members: [entry] });
1903
+ }
1904
+ }
1905
+ }
1906
+ }
1907
+ const ruleOf = (entry) => rules?.get(entry.ownerId) ?? ALWAYS;
1908
+ const tie = (e, f) => viewsTie(e.views, f.views) && rulesCanHoldTogether(ruleOf(e), ruleOf(f), modes);
1909
+ const conflicts = /* @__PURE__ */ new Map();
1910
+ for (const { base, platform, members } of groups.values()) {
1911
+ const ties = (e, i, f, j) => i !== j && !(i < j ? e : f).yields && tie(e, f);
1912
+ const bucket = members.filter((e, i) => members.some((f, j) => ties(e, i, f, j)));
1913
+ if (bucket.length < 2) continue;
1914
+ const first = bucket[0];
1915
+ const key = `${base}|${bucket.map((e) => `${objectId(e.spec)}:${canonicalModifiers(e.modifiers)}`).join(";")}`;
1916
+ const found = conflicts.get(key);
1917
+ if (found) {
1918
+ found.platforms.add(platform);
1919
+ continue;
1920
+ }
1921
+ conflicts.set(key, {
1922
+ conflict: {
1923
+ phase: first.phase,
1924
+ gesture: first.gesture,
1925
+ arg: first.arg,
1926
+ target: first.target,
1927
+ modifiers: first.modifiers,
1928
+ ownerIds: bucket.map((e) => e.ownerId)
1929
+ },
1930
+ bucket,
1931
+ platforms: /* @__PURE__ */ new Set([platform])
1932
+ });
1933
+ }
1934
+ return [...conflicts].filter(([, c]) => c.platforms.size === PLATFORMS.length || !sameOutcome(c.bucket)).map(([key, { conflict }]) => ({ key, conflict }));
1935
+ }
1793
1936
  function rulesCanHoldTogether(a, b, modes) {
1794
1937
  if (rulesExclusive(a, b)) return false;
1795
1938
  return modes.some((m) => ruleCanHoldIn(a, m) && ruleCanHoldIn(b, m));
@@ -1862,38 +2005,6 @@ function reportRouteConflicts(scopes, warn = (m) => console.warn(m)) {
1862
2005
  }
1863
2006
  return conflicts;
1864
2007
  }
1865
-
1866
- // src/interactions/keyHelpers.ts
1867
- function isEditableTarget(target) {
1868
- if (!target || !(target instanceof Element)) return false;
1869
- const tag = target.tagName;
1870
- if (tag === "INPUT" || tag === "TEXTAREA") return true;
1871
- if (target.isContentEditable) return true;
1872
- const ce = target.getAttribute("contenteditable");
1873
- return ce === "true" || ce === "";
1874
- }
1875
- var ACTIVATED_BY_KEY = [
1876
- "button",
1877
- "a[href]",
1878
- "summary",
1879
- "select",
1880
- 'input[type="button"]',
1881
- 'input[type="submit"]',
1882
- 'input[type="reset"]',
1883
- 'input[type="checkbox"]',
1884
- 'input[type="radio"]',
1885
- '[role="button"]',
1886
- '[role="link"]',
1887
- '[role="checkbox"]',
1888
- '[role="switch"]',
1889
- '[role="menuitem"]',
1890
- '[role="tab"]',
1891
- '[role="option"]'
1892
- ].join(", ");
1893
- function activatesFocusedControl(e) {
1894
- if (e.key !== " " && e.key !== "Enter") return false;
1895
- return e.target instanceof Element && e.target.matches(ACTIVATED_BY_KEY);
1896
- }
1897
2008
  function keyMatches(eventKey, spec) {
1898
2009
  const want = typeof spec === "string" ? [spec] : spec;
1899
2010
  const ek = eventKey.toLowerCase();
@@ -1921,8 +2032,6 @@ function historyKey(e) {
1921
2032
  return "redo";
1922
2033
  return null;
1923
2034
  }
1924
-
1925
- // src/ingestion/ingestItems.ts
1926
2035
  var INGEST_STRING_MIMES = /* @__PURE__ */ new Set([
1927
2036
  "text/plain",
1928
2037
  "text/html",
@@ -1967,6 +2076,16 @@ function itemsFromDataTransfer(dt) {
1967
2076
  }
1968
2077
  return Promise.all(reads).then(() => [...files, ...strings]);
1969
2078
  }
2079
+ function itemTypesFromDataTransfer(dt) {
2080
+ const mimes = [];
2081
+ for (const item of Array.from(dt.items ?? [])) {
2082
+ const mime = item.type ? normalizeMime(item.type) : "";
2083
+ if (item.kind === "file") mimes.push(mime && mime !== "application/x-moz-file" ? mime : UNKNOWN_MIME);
2084
+ else if (INGEST_STRING_MIMES.has(mime)) mimes.push(mime);
2085
+ }
2086
+ if (mimes.length === 0 && Array.from(dt.types ?? []).includes("Files")) mimes.push(UNKNOWN_MIME);
2087
+ return mimes.map((mime) => ({ mime }));
2088
+ }
1970
2089
  function itemsFromClipboardData(cd) {
1971
2090
  const out = [];
1972
2091
  for (const file of Array.from(cd.files ?? [])) {
@@ -2025,6 +2144,6 @@ function dwarn(namespace, ...args) {
2025
2144
  console.warn(`[${namespace}]`, ...args);
2026
2145
  }
2027
2146
 
2028
- export { ALWAYS, COARSE_TARGET_SCALE, DEFAULT_ALLOWED_CAPABILITIES, DEFAULT_DEVICE_PROFILE, DepNode, INGEST_STRING_MIMES, LONG_PRESS_MS, NEVER, PREDICATE_TARGET, ScopeNode, TOOL_OFFHAND_ID, actionBindings, activatesFocusedControl, assembleEntries, buildDepsFromRequires, buildRouteRegistry, buildRuleCtx, buildToolOffhandBindings, clientToCanvas, clientToCanvasRect, createDepRegistry, createDispatcher, createLongPressStore, depNodeOf, describeRule, dlog, dwarn, evaluate, findConflicts, heldKeySpec, historyKey, isDebugEnabled, isDev, isEditableTarget, isTool, itemsFromClipboardData, itemsFromDataTransfer, itemsFromFiles, liveScope, makeToolOffhandAction, matchBest, matchSorted, matchSortedWithBarred, matchesKeyBinding, offhandKeyFor, openPointerSession, pointerGestureId, publishLiveDispatch, pushOwner, recordModeSwitch, reportRouteConflicts, resolveDeviceProfile, resolveParams, routesForSpec, ruleCanHoldIn, rulesExclusive, scopeBindings, targetConsultsAffordance, validateActionDefaultBinding, validateActionId };
2029
- //# sourceMappingURL=chunk-Y5RURZBH.js.map
2030
- //# sourceMappingURL=chunk-Y5RURZBH.js.map
2147
+ export { ALWAYS, ActionDisabledReason, COARSE_TARGET_SCALE, DEFAULT_ALLOWED_CAPABILITIES, DEFAULT_DEVICE_PROFILE, DEV2 as DEV, DepNode, INGEST_STRING_MIMES, LONG_PRESS_HAPTIC_MS, NEVER, PREDICATE_TARGET, ScopeNode, TOOL_OFFHAND_ID, actionBindings, assembleEntries, buildDepsFromRequires, buildRouteRegistry, buildRuleCtx, buildToolOffhandBindings, clientToCanvas, clientToCanvasRect, createDepRegistry, createDispatcher, createLongPressStore, depNodeOf, describeRule, dlog, dwarn, evaluate, evaluateActive, evaluateEnabled, findConflicts, heldKeySpec, historyKey, isDebugEnabled, isDev, isTool, itemTypesFromDataTransfer, itemsFromClipboardData, itemsFromDataTransfer, itemsFromFiles, liveScope, makeToolOffhandAction, matchBest, matchSorted, matchSortedWithBarred, matchesKeyBinding, multiTouchGestureId, offhandKeyFor, openPointerSession, pointerGestureId, publishLiveDispatch, pushOwner, recordModeSwitch, reportRouteConflicts, resolveDeviceProfile, resolveParams, routesForSpec, ruleCanHoldIn, rulesExclusive, scopeBindings, targetConsultsAffordance, validateActionDefaultBinding, validateActionId };
2148
+ //# sourceMappingURL=chunk-4DR6AJMN.js.map
2149
+ //# sourceMappingURL=chunk-4DR6AJMN.js.map