@weasel-js/routing 1.10.1 → 1.11.1

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.
package/dist/react.js CHANGED
@@ -1,10 +1,10 @@
1
- import { createDepRegistry, createDispatcher, assembleEntries, liveScope, isDev, reportRouteConflicts, offhandKeyFor, makeToolOffhandAction, buildToolOffhandBindings, dlog, ScopeNode, isTool, tuneAction, pushOwner, evaluateEnabled, validateActionId, validateActionDefaultBinding, depNodeOf, DepNode, clientToCanvas, DEV, publishLiveDispatch, LONG_PRESS_HAPTIC_MS, multiTouchGestureId, openPointerSession, buildDepsFromRequires, isEditableTarget, focusedControlOwnsKey, itemsFromClipboardData, itemsFromDataTransfer, itemTypesFromDataTransfer } from './chunk-VCEIQ64H.js';
2
- import { createContext, useState, useContext, useEffect, useRef, useCallback, useMemo, useSyncExternalStore } from 'react';
1
+ import { createDepRegistry, createDispatchTrace, createDispatcher, assembleEntries, liveScope, reportRouteConflicts, offhandKeyFor, makeToolOffhandAction, buildToolOffhandBindings, dlog, ScopeNode, tuneAction, createMenuHost, pushOwner, evaluateEnabled, tunedParams, validateActionId, validateActionDefaultBinding, isTool, depNodeOf, DepNode, buildDepsFromRequires, clientToCanvas, LONG_PRESS_HAPTIC_MS, multiTouchGestureId, openPointerSession, addCarrySurface, carryGestureId, anyBinding, isEditableTarget, focusedControlOwnsKey, itemsFromClipboardData, itemsFromDataTransfer, itemTypesFromDataTransfer, actionCursor } from './chunk-KLBH7VFF.js';
2
+ import { createContext, useState, useContext, useEffect, useMemo, useRef, useCallback, useSyncExternalStore } from 'react';
3
3
  import { isApplePlatform, heldPointers, createPointerRecognizer, isInControlWithin, longPressEvents, stylusOf } from '@weasel-js/gestures';
4
- import { useLatest, useStableByContent, sameList } from '@weasel-js/react';
4
+ import { isDev, useLatest, useKitSettings, useStableByContent, sameList } from '@weasel-js/react';
5
5
  import { jsx, Fragment } from 'react/jsx-runtime';
6
6
  import { createCursorSink, resolveCursorTier } from '@weasel-js/cursor';
7
- import { createEmitter } from '@weasel-js/registry';
7
+ import { deepEqual, createEmitter } from '@weasel-js/registry';
8
8
  import { activeModeOf } from '@weasel-js/modes';
9
9
 
10
10
  var ToolNode = class extends ScopeNode {
@@ -108,9 +108,280 @@ function useDepSource(name, source) {
108
108
  const registry = useDepRegistry();
109
109
  const sourceRef = useLatest(source);
110
110
  useEffect(() => {
111
- return registry.register(name, () => sourceRef.current());
111
+ return registry.register(name, (below) => sourceRef.current(below));
112
112
  }, [name, registry, sourceRef]);
113
113
  }
114
+ var MenuHostContext = createContext(null);
115
+ function useMenuHost() {
116
+ return useContext(MenuHostContext);
117
+ }
118
+ function useMenus(defs, owner = null) {
119
+ const host = useMenuHost();
120
+ useEffect(() => {
121
+ if (!host) return;
122
+ const releases = defs.map((d) => host.define(d, owner));
123
+ return () => {
124
+ for (const r of releases) r();
125
+ };
126
+ }, [host, defs, owner]);
127
+ }
128
+ var ActionsContext = createContext(null);
129
+ function depsFor(a, r) {
130
+ if (!r) return {};
131
+ if (a.requires) return buildDepsFromRequires(a, r);
132
+ return {
133
+ selection: r.get("selection"),
134
+ scene: r.get("scene"),
135
+ history: r.get("history"),
136
+ view: r.get("view"),
137
+ activeTool: r.get("activeTool"),
138
+ booleansAdapter: r.get("booleansAdapter")
139
+ };
140
+ }
141
+ var ActionsNode = class extends ScopeNode {
142
+ constructor(parent, descends, contextDeps) {
143
+ super(parent, descends);
144
+ this.contextDeps = contextDeps;
145
+ NODES.set(this.registry, this);
146
+ }
147
+ contextDeps;
148
+ actions = /* @__PURE__ */ new Map();
149
+ muted = /* @__PURE__ */ new Map();
150
+ tunings = /* @__PURE__ */ new Map();
151
+ dispatchers = [];
152
+ cache = null;
153
+ host = null;
154
+ /** Called on `activate`, so a scope's dep and tool nodes move with it. */
155
+ onActivate = [];
156
+ resolve(id) {
157
+ for (const n of this.chain()) {
158
+ if (n.muted.has(id)) return void 0;
159
+ const a = n.actions.get(id)?.at(-1);
160
+ if (a) return this.tuned(a);
161
+ }
162
+ return void 0;
163
+ }
164
+ /** Every tuning for `id` from the root down to here, root first. */
165
+ tuningsOf(id) {
166
+ const tunings = [];
167
+ for (const n of this.chain()) {
168
+ const stack = n.tunings.get(id);
169
+ if (stack) tunings.unshift(...stack);
170
+ }
171
+ return tunings;
172
+ }
173
+ /** `a` under every tuning for its id from the root down to here. */
174
+ tuned(a) {
175
+ const tunings = this.tuningsOf(a.id);
176
+ return tunings.length ? tuneAction(a, tunings) : a;
177
+ }
178
+ snapshot() {
179
+ const leaf = this.start();
180
+ const cached = this.cache;
181
+ if (cached && cached.version === this.tree.version && cached.leaf === leaf) return cached.list;
182
+ const ids = /* @__PURE__ */ new Set();
183
+ for (const n of leaf.chain()) for (const id of n.actions.keys()) ids.add(id);
184
+ const out = [];
185
+ for (const id of ids) {
186
+ const a = leaf.resolve(id);
187
+ if (a) out.push(a);
188
+ }
189
+ const list = Object.freeze(out);
190
+ this.cache = { version: this.tree.version, leaf, list };
191
+ return list;
192
+ }
193
+ deps() {
194
+ for (const n of this.chain()) {
195
+ const r = n.contextDeps();
196
+ if (r) return r;
197
+ }
198
+ return null;
199
+ }
200
+ /** The actions registered at this node itself, live, unmuted and tuned —
201
+ * none from the scopes below it. */
202
+ ownActions() {
203
+ const out = [];
204
+ for (const [id, stack] of this.actions) {
205
+ if (stack.length > 0 && !this.muted.has(id)) out.push(this.tuned(stack.at(-1)));
206
+ }
207
+ return out;
208
+ }
209
+ /** The deps `a` is invoked with from here: the active scope's, which is
210
+ * what `trigger` and `begin` run it with. */
211
+ depsOf(a) {
212
+ return depsFor(a, this.start().deps());
213
+ }
214
+ /** The menu host of this node's tree, made by its root on first ask. Rows
215
+ * resolve and run against the active scope, as chrome's `trigger` does. */
216
+ menuHost() {
217
+ const root = [...this.chain()].at(-1);
218
+ return root.host ??= createMenuHost({
219
+ actions: () => root.registry.list(),
220
+ depsFor: (a) => root.depsOf(a),
221
+ trigger: (id, params) => root.registry.trigger(id, params),
222
+ activeTool: () => root.start().deps()?.get("activeTool")?.active ?? null
223
+ });
224
+ }
225
+ dispatcher() {
226
+ for (const n of this.chain()) {
227
+ const d = n.dispatchers.at(-1);
228
+ if (d) return d;
229
+ }
230
+ return null;
231
+ }
232
+ activate() {
233
+ keyHolder = this;
234
+ super.activate();
235
+ for (const f of this.onActivate) f();
236
+ }
237
+ registry = {
238
+ register: (action) => {
239
+ validateActionId(action.id);
240
+ validateActionDefaultBinding(action);
241
+ const stack = this.actions.get(action.id);
242
+ if (stack) stack.push(action);
243
+ else this.actions.set(action.id, [action]);
244
+ this.changed();
245
+ let released = false;
246
+ return () => {
247
+ if (released) return;
248
+ released = true;
249
+ const cur = this.actions.get(action.id);
250
+ if (!cur) return;
251
+ const i = cur.lastIndexOf(action);
252
+ if (i === -1) return;
253
+ cur.splice(i, 1);
254
+ if (cur.length === 0) {
255
+ queueMicrotask(() => {
256
+ if (cur.length === 0 && this.actions.get(action.id) === cur) this.actions.delete(action.id);
257
+ });
258
+ }
259
+ this.changed();
260
+ };
261
+ },
262
+ unregister: (id) => {
263
+ if (this.actions.delete(id)) this.changed();
264
+ },
265
+ mute: (id) => {
266
+ this.muted.set(id, (this.muted.get(id) ?? 0) + 1);
267
+ this.changed();
268
+ let released = false;
269
+ return () => {
270
+ if (released) return;
271
+ released = true;
272
+ const held = this.muted.get(id);
273
+ if (held === void 0) return;
274
+ if (held > 1) this.muted.set(id, held - 1);
275
+ else this.muted.delete(id);
276
+ this.changed();
277
+ };
278
+ },
279
+ tune: (id, tuning) => {
280
+ let stack = this.tunings.get(id);
281
+ if (!stack) this.tunings.set(id, stack = []);
282
+ const release = pushOwner(stack, tuning);
283
+ this.changed();
284
+ let released = false;
285
+ return () => {
286
+ if (released) return;
287
+ released = true;
288
+ release();
289
+ if (stack.length === 0) this.tunings.delete(id);
290
+ this.changed();
291
+ };
292
+ },
293
+ paramsOf: (id, given) => tunedParams(this.tuningsOf(id), given),
294
+ list: () => this.snapshot(),
295
+ trigger: (id, params) => {
296
+ const leaf = this.start();
297
+ const a = leaf.resolve(id);
298
+ if (!a?.invoker || a.invoker.timing !== "immediate") return false;
299
+ const deps = leaf.depsOf(a);
300
+ if (!evaluateEnabled(a, deps, pointOf(params)).enabled) return false;
301
+ try {
302
+ a.invoker.run(deps, params);
303
+ } catch (err) {
304
+ console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
305
+ }
306
+ return true;
307
+ },
308
+ preview: (id, params) => {
309
+ const leaf = this.start();
310
+ const a = leaf.resolve(id);
311
+ if (!a?.preview) return NO_PREVIEW;
312
+ const deps = leaf.depsOf(a);
313
+ if (!evaluateEnabled(a, deps, pointOf(params)).enabled) return NO_PREVIEW;
314
+ let stop;
315
+ try {
316
+ stop = a.preview(deps, params);
317
+ } catch (err) {
318
+ console.error(`weasel ActionsRegistry: preview of "${id}" threw`, err);
319
+ return NO_PREVIEW;
320
+ }
321
+ if (typeof stop !== "function") return NO_PREVIEW;
322
+ let down = false;
323
+ return () => {
324
+ if (down) return;
325
+ down = true;
326
+ try {
327
+ stop();
328
+ } catch (err) {
329
+ console.error(`weasel ActionsRegistry: preview of "${id}" threw as it ended`, err);
330
+ }
331
+ };
332
+ },
333
+ subscribe: (listener) => this.tree.listeners.subscribe(listener),
334
+ setDispatcher: (d) => pushOwner(this.dispatchers, d),
335
+ begin: (id, params) => {
336
+ const leaf = this.start();
337
+ const a = leaf.resolve(id);
338
+ const disp = leaf.dispatcher();
339
+ if (!a || !disp) return null;
340
+ return disp.beginUiOngoing(id, leaf.depsOf(a), params);
341
+ },
342
+ activate: () => this.activate(),
343
+ isActive: () => this.isActive(),
344
+ holdsKeys: () => keyHolder === this,
345
+ dropKeys: () => {
346
+ if (keyHolder === this) keyHolder = null;
347
+ }
348
+ };
349
+ };
350
+ var NO_PREVIEW = () => {
351
+ };
352
+ function pointOf(params) {
353
+ const { worldX: x, worldY: y } = params ?? {};
354
+ return typeof x === "number" && typeof y === "number" ? { x, y } : void 0;
355
+ }
356
+ var keyHolder = null;
357
+ var NODES = /* @__PURE__ */ new WeakMap();
358
+ function actionsNodeOf(registry) {
359
+ return registry ? NODES.get(registry) ?? null : null;
360
+ }
361
+ function ActionsProvider({ children }) {
362
+ const depReg = useOptionalDepRegistry();
363
+ const depRegRef = useLatest(depReg);
364
+ const node = useMemo(() => new ActionsNode(null, true, () => depRegRef.current), [depRegRef]);
365
+ return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: node.registry, children: /* @__PURE__ */ jsx(MenuHostContext.Provider, { value: node.menuHost(), children }) });
366
+ }
367
+ var IS_DEV = isDev();
368
+ function useActionsRegistry() {
369
+ return useContext(ActionsContext);
370
+ }
371
+ function useAction(action) {
372
+ const reg = useActionsRegistry();
373
+ useEffect(() => {
374
+ if (!reg) {
375
+ if (IS_DEV) {
376
+ console.warn(
377
+ `useAction("${action.id}"): no <ActionsProvider> is in scope, so the action was not registered and its bindings will never fire.`
378
+ );
379
+ }
380
+ return;
381
+ }
382
+ return reg.register(action);
383
+ }, [reg, action]);
384
+ }
114
385
  function useToolLifecycle(opts) {
115
386
  const { active, hotkeyStack } = opts;
116
387
  const liveRef = useRef(/* @__PURE__ */ new Set());
@@ -148,6 +419,7 @@ function useToolLifecycle(opts) {
148
419
  function createDispatchCore(src) {
149
420
  const { canvas } = src;
150
421
  let routedId = null;
422
+ let over = false;
151
423
  const target = () => {
152
424
  if (routedId === null) return src.root();
153
425
  return src.views()?.targets().find((t) => t.id === routedId) ?? src.root();
@@ -184,16 +456,22 @@ function createDispatchCore(src) {
184
456
  return fn ? fn(cx, cy) : { x: cx, y: cy };
185
457
  },
186
458
  toCanvasLocal: (cx, cy) => {
459
+ const own = src.clientToLocal();
460
+ if (own) return own(cx, cy);
187
461
  if (!canvas) return { x: cx, y: cy };
188
462
  const [x, y] = clientToCanvas(canvas, cx, cy);
189
463
  return { x, y };
190
464
  },
191
- dispatch: (ev, on) => {
465
+ dispatch: (ev, on, reach) => {
192
466
  const d = on ?? dispatcherNow();
193
467
  src.uses(d);
194
- return d.handleInput(ev, ctxNow());
468
+ return d.handleInput(ev, reach ? { ...ctxNow(), keyReach: reach } : ctxNow());
195
469
  },
196
470
  holdsKeys: () => canvas ? src.actions().holdsKeys() : src.actions().isActive(),
471
+ pointerOver: () => over,
472
+ setPointerOver: (next) => {
473
+ over = canvas !== null && next;
474
+ },
197
475
  activate: () => src.actions().activate(),
198
476
  disposed: src.disposed
199
477
  };
@@ -243,8 +521,65 @@ function underPointer(view, mods, w) {
243
521
  shiftKey: mods.shiftKey,
244
522
  ...affordance !== void 0 ? { affordance } : {},
245
523
  ...body?.body !== void 0 ? { bodyTarget: body.body } : {},
246
- ...body?.kind !== void 0 ? { bodyKind: body.kind } : {}
524
+ ...body?.kind !== void 0 ? { bodyKind: body.kind } : {},
525
+ ...body?.doubleClick !== void 0 ? { doubleClickBody: doubleClickBodyOf(body.doubleClick) } : {}
526
+ };
527
+ }
528
+ function doubleClickBodyOf(named) {
529
+ return { bodyTarget: named.body, ...named.kind !== void 0 ? { bodyKind: named.kind } : {} };
530
+ }
531
+
532
+ // src/interactions/dispatcher/channels/carry.ts
533
+ function carryInput(core) {
534
+ const { canvas } = core;
535
+ if (!canvas) return () => {
247
536
  };
537
+ const pinned = /* @__PURE__ */ new Map();
538
+ return addCarrySurface({
539
+ element: canvas,
540
+ receive(stage, payload, at, pointerId) {
541
+ if (core.disposed()) return "unhandled";
542
+ const event = () => {
543
+ const w = core.toWorld(at.clientX, at.clientY);
544
+ const { affordance, bodyTarget, bodyKind } = underPointer(core.target(), at, w);
545
+ return {
546
+ kind: "carry",
547
+ stage,
548
+ pointerId,
549
+ type: payload.type,
550
+ value: payload.value,
551
+ x: w.x,
552
+ y: w.y,
553
+ clientX: at.clientX,
554
+ clientY: at.clientY,
555
+ altKey: at.altKey,
556
+ ctrlKey: at.ctrlKey,
557
+ metaKey: at.metaKey,
558
+ shiftKey: at.shiftKey,
559
+ ...affordance !== void 0 ? { affordance } : {},
560
+ ...bodyTarget !== void 0 ? { bodyTarget } : {},
561
+ ...bodyKind !== void 0 ? { bodyKind } : {}
562
+ };
563
+ };
564
+ const routedNow = core.routedView();
565
+ try {
566
+ if (stage === "enter") {
567
+ core.routeAt(null, at.clientX, at.clientY);
568
+ pinned.set(pointerId, core.routedView());
569
+ return core.dispatch(event());
570
+ }
571
+ if (pinned.has(pointerId)) core.setRoutedView(pinned.get(pointerId));
572
+ if (stage === "drop" || stage === "leave") pinned.delete(pointerId);
573
+ if (stage !== "leave" && !core.dispatcherNow().inFlight().has(carryGestureId(pointerId))) {
574
+ core.routeAt(null, at.clientX, at.clientY);
575
+ if (stage === "move") pinned.set(pointerId, core.routedView());
576
+ }
577
+ return core.dispatch(event());
578
+ } finally {
579
+ core.setRoutedView(routedNow);
580
+ }
581
+ }
582
+ });
248
583
  }
249
584
 
250
585
  // src/interactions/dispatcher/channels/contextMenu.ts
@@ -253,6 +588,7 @@ function contextMenuInput(core, takeNativeHold) {
253
588
  const onContextMenu = (e) => {
254
589
  e.preventDefault();
255
590
  if (takeNativeHold()) return;
591
+ core.activate();
256
592
  routeAt(null, e.clientX, e.clientY);
257
593
  const w = toWorld(e.clientX, e.clientY);
258
594
  const ev = {
@@ -290,6 +626,15 @@ function hoverCursorInput(core, paintedCursor, pointersHeld, cursorSink = () =>
290
626
  };
291
627
  let last = null;
292
628
  let facts = null;
629
+ let heldHeard = null;
630
+ const hears = () => {
631
+ if (pointersHeld() === 0) {
632
+ heldHeard = null;
633
+ return true;
634
+ }
635
+ heldHeard ??= anyBinding(ctxNow(), (s) => s.kind === "hover" && s.held !== void 0);
636
+ return heldHeard;
637
+ };
293
638
  const resolve = () => {
294
639
  if (!canvas || !last) return;
295
640
  if (pointersHeld() > 0 || dispatcherNow().inFlight().size > 0) {
@@ -307,9 +652,10 @@ function hoverCursorInput(core, paintedCursor, pointersHeld, cursorSink = () =>
307
652
  const ctx = ctxNow();
308
653
  const d = dispatcherNow();
309
654
  const press = { kind: "pointerdown", x: w.x, y: w.y, clientX: h.clientX, clientY: h.clientY, ...where };
310
- if (DEV) publishLiveDispatch(() => d.explain(press, ctx));
655
+ if (ctx.trace?.recording()) ctx.trace.setLive(() => d.explain(press, ctx));
311
656
  const click = { kind: "click", x: w.x, y: w.y, pressX: w.x, pressY: w.y, ...where };
312
- const cursor = d.resolveOnly(press, ctx)?.action.cursor ?? dispatcherNow().resolveOnly(click, ctx)?.action.cursor;
657
+ const cursorOf = (r) => r ? actionCursor(r.action, r.params) : void 0;
658
+ const cursor = cursorOf(d.resolveOnly(press, ctx)) ?? cursorOf(dispatcherNow().resolveOnly(click, ctx));
313
659
  override.apply(cursor ?? null);
314
660
  };
315
661
  const refresh = () => {
@@ -324,19 +670,22 @@ function hoverCursorInput(core, paintedCursor, pointersHeld, cursorSink = () =>
324
670
  if (!core.disposed()) refresh();
325
671
  }, 0);
326
672
  };
673
+ const hoverAt = (at) => {
674
+ const w = toWorld(at.clientX, at.clientY);
675
+ const where = underPointer(target(), at, w);
676
+ const from = facts ? { from: whereOf(facts.where) } : {};
677
+ const held = pointersHeld() > 0 ? { held: true } : {};
678
+ facts = { at, where };
679
+ dispatch({ kind: "hover", x: w.x, y: w.y, clientX: at.clientX, clientY: at.clientY, ...where, ...from, ...held });
680
+ };
327
681
  return {
328
682
  moved(e) {
329
683
  last = hoverOf(e);
330
- if (pointersHeld() === 0) {
331
- const w = toWorld(e.clientX, e.clientY);
332
- const where = underPointer(target(), last, w);
333
- const from = facts ? { from: whereOf(facts.where) } : {};
334
- facts = { at: last, where };
335
- dispatch({ kind: "hover", x: w.x, y: w.y, clientX: e.clientX, clientY: e.clientY, ...where, ...from });
336
- }
684
+ if (hears()) hoverAt(last);
337
685
  refresh();
338
686
  },
339
687
  released(e) {
688
+ heldHeard = null;
340
689
  last = hoverOf(e);
341
690
  refreshSoon();
342
691
  },
@@ -348,13 +697,16 @@ function hoverCursorInput(core, paintedCursor, pointersHeld, cursorSink = () =>
348
697
  }
349
698
  facts = null;
350
699
  last = null;
351
- if (DEV) publishLiveDispatch(null);
700
+ ctxNow().trace?.setLive(null);
352
701
  paintedCursor()?.clearPointer();
353
702
  override.clear();
354
703
  },
355
704
  onKey(e) {
356
705
  if (!last) return;
706
+ const prev = last;
357
707
  last = { ...last, altKey: e.altKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey };
708
+ const changed = prev.altKey !== last.altKey || prev.ctrlKey !== last.ctrlKey || prev.metaKey !== last.metaKey || prev.shiftKey !== last.shiftKey;
709
+ if (changed && hears()) hoverAt(last);
358
710
  refreshSoon();
359
711
  },
360
712
  clear: override.clear
@@ -451,22 +803,24 @@ function typedWithAltGraph(e) {
451
803
  const base = /^(?:Key|Digit)(.)$/.exec(e.code)?.[1];
452
804
  return base === void 0 || base.toLowerCase() !== e.key.toLowerCase();
453
805
  }
806
+ var inMenu = (t) => t instanceof Element && t.closest('[role="menu"]') !== null;
454
807
  function keyboardInput(core) {
455
808
  const { dispatch, dispatcherNow } = core;
456
809
  const heldKeys = /* @__PURE__ */ new Map();
457
810
  const codeOf = (e) => e.code ? { code: e.code } : {};
458
811
  const physicalKey = (e) => e.code || e.key.toLowerCase();
459
812
  const onKeyDown = (e) => {
460
- if (e.defaultPrevented || isEditableTarget(e.target) || focusedControlOwnsKey(e)) return;
461
- if (!core.holdsKeys()) return;
813
+ if (e.defaultPrevented || isEditableTarget(e.target) || inMenu(e.target) || focusedControlOwnsKey(e)) return;
814
+ const reach = { held: core.holdsKeys(), over: core.pointerOver() };
815
+ if (!reach.held && !reach.over) return;
462
816
  if (e.repeat && heldKeys.has(physicalKey(e))) {
463
817
  e.preventDefault();
464
818
  return;
465
819
  }
466
- if (press(e, { altKey: e.altKey, ctrlKey: e.ctrlKey })) return;
467
- if (typedWithAltGraph(e)) press(e, { altKey: false, ctrlKey: false });
820
+ if (press(e, { altKey: e.altKey, ctrlKey: e.ctrlKey }, reach)) return;
821
+ if (typedWithAltGraph(e)) press(e, { altKey: false, ctrlKey: false }, reach);
468
822
  };
469
- const press = (e, mods) => {
823
+ const press = (e, mods, reach) => {
470
824
  const keyEv = {
471
825
  kind: "key",
472
826
  key: e.key,
@@ -476,7 +830,7 @@ function keyboardInput(core) {
476
830
  shiftKey: e.shiftKey,
477
831
  repeat: e.repeat
478
832
  };
479
- const keyResult = dispatch(keyEv);
833
+ const keyResult = dispatch(keyEv, void 0, reach);
480
834
  const heldEv = {
481
835
  kind: "key-held",
482
836
  key: e.key,
@@ -487,7 +841,7 @@ function keyboardInput(core) {
487
841
  shiftKey: e.shiftKey
488
842
  };
489
843
  const heldOn = dispatcherNow();
490
- const heldResult = dispatch(heldEv, heldOn);
844
+ const heldResult = dispatch(heldEv, heldOn, reach);
491
845
  if (heldResult === "handled") {
492
846
  heldKeys.set(physicalKey(e), { key: e.key, ...codeOf(e), on: heldOn });
493
847
  }
@@ -553,6 +907,7 @@ var sampleOf = (e) => ({
553
907
  clientX: e.clientX,
554
908
  clientY: e.clientY,
555
909
  target: e.target,
910
+ timeStamp: e.timeStamp,
556
911
  altKey: e.altKey,
557
912
  ctrlKey: e.ctrlKey,
558
913
  metaKey: e.metaKey,
@@ -620,7 +975,11 @@ function pointerInput(core, opts) {
620
975
  clock: hold.clock,
621
976
  toWorld,
622
977
  toSurface: (p) => core.toCanvasLocal(p.x, p.y),
623
- longPressMs: () => opts.longPress()?.duration,
978
+ longPressMs: () => opts.longPress()?.duration ?? opts.kitSettings().get("gestures.longPressMs"),
979
+ doubleClickMs: () => opts.kitSettings().get("gestures.doubleClickMs"),
980
+ doubleClickPx: () => opts.kitSettings().get("gestures.doubleClickPx"),
981
+ tapPx: () => opts.kitSettings().get("gestures.tapPx"),
982
+ dragPx: () => opts.kitSettings().get("gestures.dragPx"),
624
983
  clicks: opts.clicks,
625
984
  observer: {
626
985
  click: (world) => {
@@ -765,6 +1124,12 @@ function webkitPinchInput(core) {
765
1124
  }
766
1125
 
767
1126
  // src/interactions/dispatcher/channels/wheel.ts
1127
+ var LINE_PX = 16;
1128
+ function pixelsPer(e, canvas) {
1129
+ if (e.deltaMode === 1) return LINE_PX;
1130
+ if (e.deltaMode === 2) return canvas?.clientHeight || LINE_PX * 20;
1131
+ return 1;
1132
+ }
768
1133
  function wheelInput(core, pinchLive) {
769
1134
  const { routeAt, toWorld, target, dispatch } = core;
770
1135
  const onWheel = (e) => {
@@ -777,10 +1142,11 @@ function wheelInput(core, pinchLive) {
777
1142
  routeAt(null, e.clientX, e.clientY);
778
1143
  const local = core.toCanvasLocal(e.clientX, e.clientY);
779
1144
  const hitPoint = toWorld(e.clientX, e.clientY);
1145
+ const toPx = pixelsPer(e, core.canvas);
780
1146
  const ev = {
781
1147
  kind: "wheel",
782
- deltaX: e.deltaX,
783
- deltaY: e.deltaY,
1148
+ deltaX: e.deltaX * toPx,
1149
+ deltaY: e.deltaY * toPx,
784
1150
  clientX: local.x,
785
1151
  clientY: local.y,
786
1152
  ...underPointer(target(), e, hitPoint)
@@ -803,6 +1169,7 @@ function attachInput(canvas, src, channels) {
803
1169
  canvas,
804
1170
  root: src.root,
805
1171
  views: src.views,
1172
+ clientToLocal: src.clientToLocal,
806
1173
  context: src.context,
807
1174
  actions: src.actions,
808
1175
  uses: () => {
@@ -817,6 +1184,7 @@ function attachInput(canvas, src, channels) {
817
1184
  const hover = hoverCursorInput(core, src.paintedCursor, () => held?.count() ?? 0, src.cursorSink);
818
1185
  const pointer = pointerInput(core, {
819
1186
  longPress: src.longPress,
1187
+ kitSettings: src.kitSettings,
820
1188
  onClick: src.onClick,
821
1189
  onDoubleClick: src.onDoubleClick,
822
1190
  clicks: src.clicks,
@@ -837,6 +1205,12 @@ function attachInput(canvas, src, channels) {
837
1205
  listeners.on(canvas, "pointerdown", () => {
838
1206
  src.actions().activate();
839
1207
  });
1208
+ const setOver = (on) => () => {
1209
+ core.setPointerOver(on);
1210
+ };
1211
+ listeners.on(canvas, "pointerenter", setOver(true));
1212
+ listeners.on(canvas, "pointermove", setOver(true));
1213
+ listeners.on(canvas, "pointerleave", setOver(false));
840
1214
  if (channels.pointer) {
841
1215
  listeners.on(window, "keydown", hover.onKey);
842
1216
  listeners.on(window, "keyup", hover.onKey);
@@ -844,6 +1218,14 @@ function attachInput(canvas, src, channels) {
844
1218
  listeners.on(canvas, "pointerdown", pointer.onPointerDown);
845
1219
  listeners.on(canvas, "pointermove", pointer.onHoverMove);
846
1220
  listeners.on(canvas, "dblclick", pointer.onNativeDoubleClick);
1221
+ } else if (channels.hover) {
1222
+ listeners.on(window, "keydown", hover.onKey);
1223
+ listeners.on(window, "keyup", hover.onKey);
1224
+ listeners.on(canvas, "pointerleave", hover.onLeave);
1225
+ listeners.on(canvas, "pointermove", (e) => {
1226
+ core.routeAt(null, e.clientX, e.clientY);
1227
+ hover.moved(e);
1228
+ });
847
1229
  }
848
1230
  if (channels.wheel) {
849
1231
  listeners.on(canvas, "wheel", wheelInput(core, pinch.live), { passive: false, capture: true });
@@ -863,8 +1245,10 @@ function attachInput(canvas, src, channels) {
863
1245
  listeners.on(canvas, "drop", ingest.onDrop);
864
1246
  listeners.on(window, "paste", ingest.onPaste);
865
1247
  }
1248
+ const offCarry = channels.carry ? carryInput(core) : null;
866
1249
  return () => {
867
1250
  listeners.removeAll();
1251
+ offCarry?.();
868
1252
  canvas?.classList.remove(DROPOVER_CLASS);
869
1253
  src.actions().dropKeys();
870
1254
  hover.clear();
@@ -878,29 +1262,39 @@ function attachInput(canvas, src, channels) {
878
1262
 
879
1263
  // src/interactions/dispatcher/useGestureDispatcher.tsx
880
1264
  function useGestureDispatcher(opts) {
881
- const { canvasRef, actions, entriesById, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, clientToWorld, requestRedraw, paintedCursor, cursorSink, getRuleCtx, onClick, onDoubleClick, views, channels, longPress, claimKeys = false } = opts;
1265
+ const { canvasRef, actions, entriesById, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, trace: traceOpt, clientToWorld, clientToLocal, requestRedraw, paintedCursor, cursorSink, getRuleCtx, onClick, onDoubleClick, views, channels, longPress, claimKeys = false, hears = "scope" } = opts;
882
1266
  const pointerChannel = channels?.pointer !== false;
1267
+ const hoverChannel = channels?.hover ?? pointerChannel;
883
1268
  const wheelChannel = channels?.wheel !== false;
884
1269
  const pinchChannel = channels?.pinch !== false;
885
1270
  const contextMenuChannel = channels?.contextMenu !== false;
886
1271
  const ingestChannel = channels?.ingest !== false;
1272
+ const carryChannel = channels?.carry !== false;
887
1273
  const onClickRef = useLatest(onClick);
888
1274
  const onDoubleClickRef = useLatest(onDoubleClick);
889
1275
  const activeTool = useActiveToolContext();
890
1276
  const depRegistry = useDepRegistry();
891
1277
  const [isMac] = useState(isApplePlatform);
1278
+ const [ownTrace] = useState(createDispatchTrace);
1279
+ const trace = traceOpt ?? ownTrace;
1280
+ useEffect(() => depRegistry.register("dispatchTrace", () => trace), [depRegistry, trace]);
892
1281
  const dispatcherRef = useRef(null);
893
1282
  if (!dispatcherRef.current) {
894
1283
  dispatcherRef.current = dispatcherOpt ?? createDispatcher();
895
1284
  }
896
1285
  const registryRef = useLatest(actions);
1286
+ const heard = useMemo(() => {
1287
+ const node = hears === "own" ? actionsNodeOf(actions) : null;
1288
+ return node ? { list: () => node.ownActions() } : actions;
1289
+ }, [actions, hears]);
897
1290
  const ctxRef = useLatest({
898
- actions,
1291
+ actions: heard,
899
1292
  depRegistry,
900
1293
  activeToolId: activeTool.active,
901
1294
  hotkeyStack: activeTool.hotkeyStack,
902
1295
  entriesById,
903
- isMac
1296
+ isMac,
1297
+ trace
904
1298
  });
905
1299
  const rootTargetRef = useLatest({
906
1300
  id: null,
@@ -911,6 +1305,7 @@ function useGestureDispatcher(opts) {
911
1305
  getRuleCtx
912
1306
  });
913
1307
  const viewsRef = useLatest(views);
1308
+ const clientToLocalRef = useLatest(clientToLocal);
914
1309
  const allDispatchers = useCallback(() => [
915
1310
  rootTargetRef.current.dispatcher,
916
1311
  ...(viewsRef.current?.targets() ?? []).map((t) => t.dispatcher)
@@ -918,6 +1313,7 @@ function useGestureDispatcher(opts) {
918
1313
  const requestRedrawRef = useLatest(requestRedraw);
919
1314
  const cursorRef = useLatest({ painted: paintedCursor, sink: cursorSink });
920
1315
  const longPressRef = useLatest(longPress);
1316
+ const kitSettingsRef = useLatest(useKitSettings());
921
1317
  const clicksRef = useRef({ last: null });
922
1318
  useToolLifecycle({
923
1319
  entriesById,
@@ -936,6 +1332,7 @@ function useGestureDispatcher(opts) {
936
1332
  return attachInput(canvasRef.current, {
937
1333
  root: () => rootTargetRef.current,
938
1334
  views: () => viewsRef.current,
1335
+ clientToLocal: () => clientToLocalRef.current,
939
1336
  context: () => ctxRef.current,
940
1337
  actions: () => registryRef.current,
941
1338
  dispatchers: allDispatchers,
@@ -945,28 +1342,35 @@ function useGestureDispatcher(opts) {
945
1342
  paintedCursor: () => cursorRef.current.painted?.(),
946
1343
  cursorSink: () => cursorRef.current.sink?.(),
947
1344
  longPress: () => longPressRef.current,
1345
+ kitSettings: () => kitSettingsRef.current,
948
1346
  onClick: () => onClickRef.current,
949
1347
  onDoubleClick: () => onDoubleClickRef.current,
950
1348
  clicks: clicksRef.current
951
1349
  }, {
952
1350
  keyboard,
953
1351
  pointer: pointerChannel,
1352
+ hover: hoverChannel,
954
1353
  wheel: wheelChannel,
955
1354
  pinch: pinchChannel,
956
1355
  contextMenu: contextMenuChannel,
957
- ingest: ingestChannel
1356
+ ingest: ingestChannel,
1357
+ carry: carryChannel
958
1358
  });
959
1359
  }, [
960
1360
  enabled,
961
1361
  keyboard,
962
1362
  canvasRef,
963
1363
  pointerChannel,
1364
+ hoverChannel,
964
1365
  wheelChannel,
965
1366
  pinchChannel,
966
1367
  contextMenuChannel,
967
1368
  ingestChannel,
1369
+ carryChannel,
968
1370
  allDispatchers,
1371
+ clientToLocalRef,
969
1372
  ctxRef,
1373
+ kitSettingsRef,
970
1374
  longPressRef,
971
1375
  onClickRef,
972
1376
  onDoubleClickRef,
@@ -977,206 +1381,36 @@ function useGestureDispatcher(opts) {
977
1381
  viewsRef
978
1382
  ]);
979
1383
  }
980
- var ActionsContext = createContext(null);
981
- function depsFor(a, r) {
982
- if (!r) return {};
983
- if (a.requires) return buildDepsFromRequires(a, r);
984
- return {
985
- selection: r.get("selection"),
986
- scene: r.get("scene"),
987
- history: r.get("history"),
988
- view: r.get("view"),
989
- activeTool: r.get("activeTool"),
990
- booleansAdapter: r.get("booleansAdapter")
991
- };
992
- }
993
- var ActionsNode = class extends ScopeNode {
994
- constructor(parent, descends, contextDeps) {
995
- super(parent, descends);
996
- this.contextDeps = contextDeps;
997
- NODES.set(this.registry, this);
998
- }
999
- contextDeps;
1000
- actions = /* @__PURE__ */ new Map();
1001
- muted = /* @__PURE__ */ new Map();
1002
- tunings = /* @__PURE__ */ new Map();
1003
- dispatchers = [];
1004
- cache = null;
1005
- /** Called on `activate`, so a scope's dep and tool nodes move with it. */
1006
- onActivate = [];
1007
- resolve(id) {
1008
- for (const n of this.chain()) {
1009
- if (n.muted.has(id)) return void 0;
1010
- const a = n.actions.get(id)?.at(-1);
1011
- if (a) return this.tuned(a);
1012
- }
1013
- return void 0;
1014
- }
1015
- /** `a` under every tuning for its id from the root down to here. */
1016
- tuned(a) {
1017
- const tunings = [];
1018
- for (const n of this.chain()) {
1019
- const stack = n.tunings.get(a.id);
1020
- if (stack) tunings.unshift(...stack);
1021
- }
1022
- return tunings.length ? tuneAction(a, tunings) : a;
1023
- }
1024
- snapshot() {
1025
- const leaf = this.start();
1026
- const cached = this.cache;
1027
- if (cached && cached.version === this.tree.version && cached.leaf === leaf) return cached.list;
1028
- const ids = /* @__PURE__ */ new Set();
1029
- for (const n of leaf.chain()) for (const id of n.actions.keys()) ids.add(id);
1030
- const out = [];
1031
- for (const id of ids) {
1032
- const a = leaf.resolve(id);
1033
- if (a) out.push(a);
1034
- }
1035
- const list = Object.freeze(out);
1036
- this.cache = { version: this.tree.version, leaf, list };
1037
- return list;
1038
- }
1039
- deps() {
1040
- for (const n of this.chain()) {
1041
- const r = n.contextDeps();
1042
- if (r) return r;
1043
- }
1044
- return null;
1045
- }
1046
- dispatcher() {
1047
- for (const n of this.chain()) {
1048
- const d = n.dispatchers.at(-1);
1049
- if (d) return d;
1050
- }
1051
- return null;
1052
- }
1053
- activate() {
1054
- keyHolder = this;
1055
- super.activate();
1056
- for (const f of this.onActivate) f();
1057
- }
1058
- registry = {
1059
- register: (action) => {
1060
- validateActionId(action.id);
1061
- validateActionDefaultBinding(action);
1062
- const stack = this.actions.get(action.id);
1063
- if (stack) stack.push(action);
1064
- else this.actions.set(action.id, [action]);
1065
- this.changed();
1066
- let released = false;
1067
- return () => {
1068
- if (released) return;
1069
- released = true;
1070
- const cur = this.actions.get(action.id);
1071
- if (!cur) return;
1072
- const i = cur.lastIndexOf(action);
1073
- if (i === -1) return;
1074
- cur.splice(i, 1);
1075
- if (cur.length === 0) {
1076
- queueMicrotask(() => {
1077
- if (cur.length === 0 && this.actions.get(action.id) === cur) this.actions.delete(action.id);
1078
- });
1079
- }
1080
- this.changed();
1081
- };
1082
- },
1083
- unregister: (id) => {
1084
- if (this.actions.delete(id)) this.changed();
1085
- },
1086
- mute: (id) => {
1087
- this.muted.set(id, (this.muted.get(id) ?? 0) + 1);
1088
- this.changed();
1089
- let released = false;
1090
- return () => {
1091
- if (released) return;
1092
- released = true;
1093
- const held = this.muted.get(id);
1094
- if (held === void 0) return;
1095
- if (held > 1) this.muted.set(id, held - 1);
1096
- else this.muted.delete(id);
1097
- this.changed();
1098
- };
1099
- },
1100
- tune: (id, tuning) => {
1101
- let stack = this.tunings.get(id);
1102
- if (!stack) this.tunings.set(id, stack = []);
1103
- const release = pushOwner(stack, tuning);
1104
- this.changed();
1105
- let released = false;
1106
- return () => {
1107
- if (released) return;
1108
- released = true;
1109
- release();
1110
- if (stack.length === 0) this.tunings.delete(id);
1111
- this.changed();
1112
- };
1113
- },
1114
- list: () => this.snapshot(),
1115
- trigger: (id, params) => {
1116
- const leaf = this.start();
1117
- const a = leaf.resolve(id);
1118
- if (!a?.invoker || a.invoker.timing !== "immediate") return false;
1119
- const deps = depsFor(a, leaf.deps());
1120
- if (!evaluateEnabled(a, deps).enabled) return false;
1121
- try {
1122
- a.invoker.run(deps, params);
1123
- } catch (err) {
1124
- console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
1125
- }
1126
- return true;
1127
- },
1128
- subscribe: (listener) => this.tree.listeners.subscribe(listener),
1129
- setDispatcher: (d) => pushOwner(this.dispatchers, d),
1130
- begin: (id, params) => {
1131
- const leaf = this.start();
1132
- const a = leaf.resolve(id);
1133
- const disp = leaf.dispatcher();
1134
- if (!a || !disp) return null;
1135
- return disp.beginUiOngoing(id, depsFor(a, leaf.deps()), params);
1136
- },
1137
- activate: () => this.activate(),
1138
- isActive: () => this.isActive(),
1139
- holdsKeys: () => keyHolder === this,
1140
- dropKeys: () => {
1141
- if (keyHolder === this) keyHolder = null;
1142
- }
1143
- };
1144
- };
1145
- var keyHolder = null;
1146
- var NODES = /* @__PURE__ */ new WeakMap();
1147
- function actionsNodeOf(registry) {
1148
- return registry ? NODES.get(registry) ?? null : null;
1149
- }
1150
- function ActionsProvider({ children }) {
1151
- const depReg = useOptionalDepRegistry();
1152
- const depRegRef = useLatest(depReg);
1153
- const node = useMemo(() => new ActionsNode(null, true, () => depRegRef.current), [depRegRef]);
1154
- return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: node.registry, children });
1155
- }
1156
- var IS_DEV = (() => {
1157
- try {
1158
- return Boolean(import.meta.env?.DEV);
1159
- } catch {
1160
- return false;
1161
- }
1162
- })();
1163
- function useActionsRegistry() {
1164
- return useContext(ActionsContext);
1384
+ function useCommand(id, label, run, options) {
1385
+ const latest = useLatest({ run, options });
1386
+ const keys = useStableByContent(options?.keys, deepEqual);
1387
+ const tags = useStableByContent(options?.tags, deepEqual);
1388
+ const variants = useStableByContent(options?.variants, deepEqual);
1389
+ const hasEnabled = options?.enabled !== void 0;
1390
+ const hasActive = options?.active !== void 0;
1391
+ const icon = options?.icon;
1392
+ const action = useMemo(() => {
1393
+ const read = () => latest.current.options;
1394
+ const a = {
1395
+ id,
1396
+ label,
1397
+ invoker: { timing: "immediate", run: (_deps, params) => latest.current.run(params) }
1398
+ };
1399
+ if (keys !== void 0) a.defaultBinding = bindingOf(keys);
1400
+ if (tags !== void 0) a.tags = tags;
1401
+ if (variants !== void 0) a.variants = variants;
1402
+ if (hasEnabled) a.enabled = () => read()?.enabled?.() ?? true;
1403
+ if (hasActive) a.active = () => read()?.active?.() ?? false;
1404
+ if (icon !== void 0) a.icon = icon;
1405
+ return a;
1406
+ }, [id, label, icon, keys, tags, variants, hasEnabled, hasActive, latest]);
1407
+ useAction(action);
1165
1408
  }
1166
- function useAction(action) {
1167
- const reg = useActionsRegistry();
1168
- useEffect(() => {
1169
- if (!reg) {
1170
- if (IS_DEV) {
1171
- console.warn(
1172
- `useAction("${action.id}"): no <ActionsProvider> is in scope, so the action was not registered and its bindings will never fire.`
1173
- );
1174
- }
1175
- return;
1176
- }
1177
- return reg.register(action);
1178
- }, [reg, action]);
1409
+ function bindingOf(keys) {
1410
+ if (isList(keys)) return [...keys];
1411
+ return "spec" in keys ? [keys] : keys;
1179
1412
  }
1413
+ var isList = (keys) => Array.isArray(keys);
1180
1414
  function useNodes(descends, ownsTool, parent) {
1181
1415
  const parentActions = useActionsRegistry();
1182
1416
  const parentDeps = useOptionalDepRegistry();
@@ -1200,7 +1434,8 @@ function useNodes(descends, ownsTool, parent) {
1200
1434
  return nodes;
1201
1435
  }
1202
1436
  function Provide({ nodes, children }) {
1203
- return /* @__PURE__ */ jsx(DepRegistryContext.Provider, { value: nodes.deps.registry, children: /* @__PURE__ */ jsx(ActionsContext.Provider, { value: nodes.actions.registry, children: /* @__PURE__ */ jsx(ActiveToolNodeContext.Provider, { value: nodes.tools, children }) }) });
1437
+ const menuHost = useContext(MenuHostContext) ?? nodes.actions.menuHost();
1438
+ return /* @__PURE__ */ jsx(DepRegistryContext.Provider, { value: nodes.deps.registry, children: /* @__PURE__ */ jsx(ActionsContext.Provider, { value: nodes.actions.registry, children: /* @__PURE__ */ jsx(ActiveToolNodeContext.Provider, { value: nodes.tools, children: /* @__PURE__ */ jsx(MenuHostContext.Provider, { value: menuHost, children }) }) }) });
1204
1439
  }
1205
1440
  function InputScope({ yoke, children }) {
1206
1441
  const nodes = useNodes(false, yoke === void 0, yoke);
@@ -1533,6 +1768,6 @@ function useTools(opts) {
1533
1768
  );
1534
1769
  }
1535
1770
 
1536
- export { ActionsProvider, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, DepRegistryProvider, InputScope, PointerContextProvider, SelectionContextProvider, SelectionContextProviderIfRoot, Yoke, createPointerStore, useAction, useActionsRegistry, useActiveToolContext, useContributions, useDepRegistry, useDepSource, useGestureDispatcher, useOffhandAction, useOngoingAction, useOptionalActiveToolContext, useOptionalDepRegistry, usePointerContext, usePointerPosition, usePublishSelection, useSelectionContext, useTools, useYoke };
1771
+ export { ActionsProvider, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, DepRegistryProvider, InputScope, PointerContextProvider, SelectionContextProvider, SelectionContextProviderIfRoot, Yoke, createPointerStore, useAction, useActionsRegistry, useActiveToolContext, useCommand, useContributions, useDepRegistry, useDepSource, useGestureDispatcher, useMenuHost, useMenus, useOffhandAction, useOngoingAction, useOptionalActiveToolContext, useOptionalDepRegistry, usePointerContext, usePointerPosition, usePublishSelection, useSelectionContext, useTools, useYoke };
1537
1772
  //# sourceMappingURL=react.js.map
1538
1773
  //# sourceMappingURL=react.js.map