@weasel-js/routing 1.10.1 → 1.11.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.
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, createDispatcher, assembleEntries, liveScope, reportRouteConflicts, offhandKeyFor, makeToolOffhandAction, buildToolOffhandBindings, dlog, ScopeNode, tuneAction, createMenuHost, pushOwner, evaluateEnabled, tunedParams, validateActionId, validateActionDefaultBinding, isTool, depNodeOf, DepNode, buildDepsFromRequires, clientToCanvas, DEV, publishLiveDispatch, LONG_PRESS_HAPTIC_MS, multiTouchGestureId, openPointerSession, addCarrySurface, gestureIdFor, isEditableTarget, focusedControlOwnsKey, itemsFromClipboardData, itemsFromDataTransfer, itemTypesFromDataTransfer, actionCursor } from './chunk-QBQQIFPI.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 {
@@ -111,6 +111,277 @@ function useDepSource(name, source) {
111
111
  return registry.register(name, () => sourceRef.current());
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());
@@ -184,6 +455,8 @@ function createDispatchCore(src) {
184
455
  return fn ? fn(cx, cy) : { x: cx, y: cy };
185
456
  },
186
457
  toCanvasLocal: (cx, cy) => {
458
+ const own = src.clientToLocal();
459
+ if (own) return own(cx, cy);
187
460
  if (!canvas) return { x: cx, y: cy };
188
461
  const [x, y] = clientToCanvas(canvas, cx, cy);
189
462
  return { x, y };
@@ -232,6 +505,56 @@ function dispatcherWatch(onChange) {
232
505
  };
233
506
  }
234
507
 
508
+ // src/interactions/dispatcher/channels/carry.ts
509
+ function carryInput(core) {
510
+ const { canvas } = core;
511
+ if (!canvas) return () => {
512
+ };
513
+ const pinned = /* @__PURE__ */ new Map();
514
+ return addCarrySurface({
515
+ element: canvas,
516
+ receive(stage, payload, at, pointerId) {
517
+ if (core.disposed()) return "unhandled";
518
+ const event = () => {
519
+ const w = core.toWorld(at.clientX, at.clientY);
520
+ return {
521
+ kind: "carry",
522
+ stage,
523
+ pointerId,
524
+ type: payload.type,
525
+ value: payload.value,
526
+ x: w.x,
527
+ y: w.y,
528
+ clientX: at.clientX,
529
+ clientY: at.clientY,
530
+ altKey: at.altKey,
531
+ ctrlKey: at.ctrlKey,
532
+ metaKey: at.metaKey,
533
+ shiftKey: at.shiftKey
534
+ };
535
+ };
536
+ const routedNow = core.routedView();
537
+ try {
538
+ if (stage === "enter") {
539
+ core.routeAt(null, at.clientX, at.clientY);
540
+ pinned.set(pointerId, core.routedView());
541
+ return core.dispatch(event());
542
+ }
543
+ if (pinned.has(pointerId)) core.setRoutedView(pinned.get(pointerId));
544
+ if (stage !== "move") pinned.delete(pointerId);
545
+ const ev = event();
546
+ if (stage === "drop" && !core.dispatcherNow().inFlight().has(gestureIdFor(ev))) {
547
+ core.routeAt(null, at.clientX, at.clientY);
548
+ return core.dispatch(event());
549
+ }
550
+ return core.dispatch(ev);
551
+ } finally {
552
+ core.setRoutedView(routedNow);
553
+ }
554
+ }
555
+ });
556
+ }
557
+
235
558
  // src/interactions/dispatcher/underPointer.ts
236
559
  function underPointer(view, mods, w) {
237
560
  const affordance = view.affordanceAt?.(w) ?? void 0;
@@ -243,9 +566,13 @@ function underPointer(view, mods, w) {
243
566
  shiftKey: mods.shiftKey,
244
567
  ...affordance !== void 0 ? { affordance } : {},
245
568
  ...body?.body !== void 0 ? { bodyTarget: body.body } : {},
246
- ...body?.kind !== void 0 ? { bodyKind: body.kind } : {}
569
+ ...body?.kind !== void 0 ? { bodyKind: body.kind } : {},
570
+ ...body?.doubleClick !== void 0 ? { doubleClickBody: doubleClickBodyOf(body.doubleClick) } : {}
247
571
  };
248
572
  }
573
+ function doubleClickBodyOf(named) {
574
+ return { bodyTarget: named.body, ...named.kind !== void 0 ? { bodyKind: named.kind } : {} };
575
+ }
249
576
 
250
577
  // src/interactions/dispatcher/channels/contextMenu.ts
251
578
  function contextMenuInput(core, takeNativeHold) {
@@ -253,6 +580,7 @@ function contextMenuInput(core, takeNativeHold) {
253
580
  const onContextMenu = (e) => {
254
581
  e.preventDefault();
255
582
  if (takeNativeHold()) return;
583
+ core.activate();
256
584
  routeAt(null, e.clientX, e.clientY);
257
585
  const w = toWorld(e.clientX, e.clientY);
258
586
  const ev = {
@@ -309,7 +637,8 @@ function hoverCursorInput(core, paintedCursor, pointersHeld, cursorSink = () =>
309
637
  const press = { kind: "pointerdown", x: w.x, y: w.y, clientX: h.clientX, clientY: h.clientY, ...where };
310
638
  if (DEV) publishLiveDispatch(() => d.explain(press, ctx));
311
639
  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;
640
+ const cursorOf = (r) => r ? actionCursor(r.action, r.params) : void 0;
641
+ const cursor = cursorOf(d.resolveOnly(press, ctx)) ?? cursorOf(dispatcherNow().resolveOnly(click, ctx));
313
642
  override.apply(cursor ?? null);
314
643
  };
315
644
  const refresh = () => {
@@ -324,16 +653,17 @@ function hoverCursorInput(core, paintedCursor, pointersHeld, cursorSink = () =>
324
653
  if (!core.disposed()) refresh();
325
654
  }, 0);
326
655
  };
656
+ const hoverAt = (at) => {
657
+ const w = toWorld(at.clientX, at.clientY);
658
+ const where = underPointer(target(), at, w);
659
+ const from = facts ? { from: whereOf(facts.where) } : {};
660
+ facts = { at, where };
661
+ dispatch({ kind: "hover", x: w.x, y: w.y, clientX: at.clientX, clientY: at.clientY, ...where, ...from });
662
+ };
327
663
  return {
328
664
  moved(e) {
329
665
  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
- }
666
+ if (pointersHeld() === 0) hoverAt(last);
337
667
  refresh();
338
668
  },
339
669
  released(e) {
@@ -354,7 +684,10 @@ function hoverCursorInput(core, paintedCursor, pointersHeld, cursorSink = () =>
354
684
  },
355
685
  onKey(e) {
356
686
  if (!last) return;
687
+ const prev = last;
357
688
  last = { ...last, altKey: e.altKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey };
689
+ const changed = prev.altKey !== last.altKey || prev.ctrlKey !== last.ctrlKey || prev.metaKey !== last.metaKey || prev.shiftKey !== last.shiftKey;
690
+ if (changed && pointersHeld() === 0) hoverAt(last);
358
691
  refreshSoon();
359
692
  },
360
693
  clear: override.clear
@@ -451,13 +784,14 @@ function typedWithAltGraph(e) {
451
784
  const base = /^(?:Key|Digit)(.)$/.exec(e.code)?.[1];
452
785
  return base === void 0 || base.toLowerCase() !== e.key.toLowerCase();
453
786
  }
787
+ var inMenu = (t) => t instanceof Element && t.closest('[role="menu"]') !== null;
454
788
  function keyboardInput(core) {
455
789
  const { dispatch, dispatcherNow } = core;
456
790
  const heldKeys = /* @__PURE__ */ new Map();
457
791
  const codeOf = (e) => e.code ? { code: e.code } : {};
458
792
  const physicalKey = (e) => e.code || e.key.toLowerCase();
459
793
  const onKeyDown = (e) => {
460
- if (e.defaultPrevented || isEditableTarget(e.target) || focusedControlOwnsKey(e)) return;
794
+ if (e.defaultPrevented || isEditableTarget(e.target) || inMenu(e.target) || focusedControlOwnsKey(e)) return;
461
795
  if (!core.holdsKeys()) return;
462
796
  if (e.repeat && heldKeys.has(physicalKey(e))) {
463
797
  e.preventDefault();
@@ -620,7 +954,11 @@ function pointerInput(core, opts) {
620
954
  clock: hold.clock,
621
955
  toWorld,
622
956
  toSurface: (p) => core.toCanvasLocal(p.x, p.y),
623
- longPressMs: () => opts.longPress()?.duration,
957
+ longPressMs: () => opts.longPress()?.duration ?? opts.kitSettings().get("gestures.longPressMs"),
958
+ doubleClickMs: () => opts.kitSettings().get("gestures.doubleClickMs"),
959
+ doubleClickPx: () => opts.kitSettings().get("gestures.doubleClickPx"),
960
+ tapPx: () => opts.kitSettings().get("gestures.tapPx"),
961
+ dragPx: () => opts.kitSettings().get("gestures.dragPx"),
624
962
  clicks: opts.clicks,
625
963
  observer: {
626
964
  click: (world) => {
@@ -765,6 +1103,12 @@ function webkitPinchInput(core) {
765
1103
  }
766
1104
 
767
1105
  // src/interactions/dispatcher/channels/wheel.ts
1106
+ var LINE_PX = 16;
1107
+ function pixelsPer(e, canvas) {
1108
+ if (e.deltaMode === 1) return LINE_PX;
1109
+ if (e.deltaMode === 2) return canvas?.clientHeight || LINE_PX * 20;
1110
+ return 1;
1111
+ }
768
1112
  function wheelInput(core, pinchLive) {
769
1113
  const { routeAt, toWorld, target, dispatch } = core;
770
1114
  const onWheel = (e) => {
@@ -777,10 +1121,11 @@ function wheelInput(core, pinchLive) {
777
1121
  routeAt(null, e.clientX, e.clientY);
778
1122
  const local = core.toCanvasLocal(e.clientX, e.clientY);
779
1123
  const hitPoint = toWorld(e.clientX, e.clientY);
1124
+ const toPx = pixelsPer(e, core.canvas);
780
1125
  const ev = {
781
1126
  kind: "wheel",
782
- deltaX: e.deltaX,
783
- deltaY: e.deltaY,
1127
+ deltaX: e.deltaX * toPx,
1128
+ deltaY: e.deltaY * toPx,
784
1129
  clientX: local.x,
785
1130
  clientY: local.y,
786
1131
  ...underPointer(target(), e, hitPoint)
@@ -803,6 +1148,7 @@ function attachInput(canvas, src, channels) {
803
1148
  canvas,
804
1149
  root: src.root,
805
1150
  views: src.views,
1151
+ clientToLocal: src.clientToLocal,
806
1152
  context: src.context,
807
1153
  actions: src.actions,
808
1154
  uses: () => {
@@ -817,6 +1163,7 @@ function attachInput(canvas, src, channels) {
817
1163
  const hover = hoverCursorInput(core, src.paintedCursor, () => held?.count() ?? 0, src.cursorSink);
818
1164
  const pointer = pointerInput(core, {
819
1165
  longPress: src.longPress,
1166
+ kitSettings: src.kitSettings,
820
1167
  onClick: src.onClick,
821
1168
  onDoubleClick: src.onDoubleClick,
822
1169
  clicks: src.clicks,
@@ -863,8 +1210,10 @@ function attachInput(canvas, src, channels) {
863
1210
  listeners.on(canvas, "drop", ingest.onDrop);
864
1211
  listeners.on(window, "paste", ingest.onPaste);
865
1212
  }
1213
+ const offCarry = channels.carry ? carryInput(core) : null;
866
1214
  return () => {
867
1215
  listeners.removeAll();
1216
+ offCarry?.();
868
1217
  canvas?.classList.remove(DROPOVER_CLASS);
869
1218
  src.actions().dropKeys();
870
1219
  hover.clear();
@@ -878,12 +1227,13 @@ function attachInput(canvas, src, channels) {
878
1227
 
879
1228
  // src/interactions/dispatcher/useGestureDispatcher.tsx
880
1229
  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;
1230
+ const { canvasRef, actions, entriesById, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, clientToWorld, clientToLocal, requestRedraw, paintedCursor, cursorSink, getRuleCtx, onClick, onDoubleClick, views, channels, longPress, claimKeys = false, hears = "scope" } = opts;
882
1231
  const pointerChannel = channels?.pointer !== false;
883
1232
  const wheelChannel = channels?.wheel !== false;
884
1233
  const pinchChannel = channels?.pinch !== false;
885
1234
  const contextMenuChannel = channels?.contextMenu !== false;
886
1235
  const ingestChannel = channels?.ingest !== false;
1236
+ const carryChannel = channels?.carry !== false;
887
1237
  const onClickRef = useLatest(onClick);
888
1238
  const onDoubleClickRef = useLatest(onDoubleClick);
889
1239
  const activeTool = useActiveToolContext();
@@ -894,8 +1244,12 @@ function useGestureDispatcher(opts) {
894
1244
  dispatcherRef.current = dispatcherOpt ?? createDispatcher();
895
1245
  }
896
1246
  const registryRef = useLatest(actions);
1247
+ const heard = useMemo(() => {
1248
+ const node = hears === "own" ? actionsNodeOf(actions) : null;
1249
+ return node ? { list: () => node.ownActions() } : actions;
1250
+ }, [actions, hears]);
897
1251
  const ctxRef = useLatest({
898
- actions,
1252
+ actions: heard,
899
1253
  depRegistry,
900
1254
  activeToolId: activeTool.active,
901
1255
  hotkeyStack: activeTool.hotkeyStack,
@@ -911,6 +1265,7 @@ function useGestureDispatcher(opts) {
911
1265
  getRuleCtx
912
1266
  });
913
1267
  const viewsRef = useLatest(views);
1268
+ const clientToLocalRef = useLatest(clientToLocal);
914
1269
  const allDispatchers = useCallback(() => [
915
1270
  rootTargetRef.current.dispatcher,
916
1271
  ...(viewsRef.current?.targets() ?? []).map((t) => t.dispatcher)
@@ -918,6 +1273,7 @@ function useGestureDispatcher(opts) {
918
1273
  const requestRedrawRef = useLatest(requestRedraw);
919
1274
  const cursorRef = useLatest({ painted: paintedCursor, sink: cursorSink });
920
1275
  const longPressRef = useLatest(longPress);
1276
+ const kitSettingsRef = useLatest(useKitSettings());
921
1277
  const clicksRef = useRef({ last: null });
922
1278
  useToolLifecycle({
923
1279
  entriesById,
@@ -936,6 +1292,7 @@ function useGestureDispatcher(opts) {
936
1292
  return attachInput(canvasRef.current, {
937
1293
  root: () => rootTargetRef.current,
938
1294
  views: () => viewsRef.current,
1295
+ clientToLocal: () => clientToLocalRef.current,
939
1296
  context: () => ctxRef.current,
940
1297
  actions: () => registryRef.current,
941
1298
  dispatchers: allDispatchers,
@@ -945,6 +1302,7 @@ function useGestureDispatcher(opts) {
945
1302
  paintedCursor: () => cursorRef.current.painted?.(),
946
1303
  cursorSink: () => cursorRef.current.sink?.(),
947
1304
  longPress: () => longPressRef.current,
1305
+ kitSettings: () => kitSettingsRef.current,
948
1306
  onClick: () => onClickRef.current,
949
1307
  onDoubleClick: () => onDoubleClickRef.current,
950
1308
  clicks: clicksRef.current
@@ -954,7 +1312,8 @@ function useGestureDispatcher(opts) {
954
1312
  wheel: wheelChannel,
955
1313
  pinch: pinchChannel,
956
1314
  contextMenu: contextMenuChannel,
957
- ingest: ingestChannel
1315
+ ingest: ingestChannel,
1316
+ carry: carryChannel
958
1317
  });
959
1318
  }, [
960
1319
  enabled,
@@ -965,8 +1324,11 @@ function useGestureDispatcher(opts) {
965
1324
  pinchChannel,
966
1325
  contextMenuChannel,
967
1326
  ingestChannel,
1327
+ carryChannel,
968
1328
  allDispatchers,
1329
+ clientToLocalRef,
969
1330
  ctxRef,
1331
+ kitSettingsRef,
970
1332
  longPressRef,
971
1333
  onClickRef,
972
1334
  onDoubleClickRef,
@@ -977,206 +1339,36 @@ function useGestureDispatcher(opts) {
977
1339
  viewsRef
978
1340
  ]);
979
1341
  }
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);
1342
+ function useCommand(id, label, run, options) {
1343
+ const latest = useLatest({ run, options });
1344
+ const keys = useStableByContent(options?.keys, deepEqual);
1345
+ const tags = useStableByContent(options?.tags, deepEqual);
1346
+ const variants = useStableByContent(options?.variants, deepEqual);
1347
+ const hasEnabled = options?.enabled !== void 0;
1348
+ const hasActive = options?.active !== void 0;
1349
+ const icon = options?.icon;
1350
+ const action = useMemo(() => {
1351
+ const read = () => latest.current.options;
1352
+ const a = {
1353
+ id,
1354
+ label,
1355
+ invoker: { timing: "immediate", run: (_deps, params) => latest.current.run(params) }
1356
+ };
1357
+ if (keys !== void 0) a.defaultBinding = bindingOf(keys);
1358
+ if (tags !== void 0) a.tags = tags;
1359
+ if (variants !== void 0) a.variants = variants;
1360
+ if (hasEnabled) a.enabled = () => read()?.enabled?.() ?? true;
1361
+ if (hasActive) a.active = () => read()?.active?.() ?? false;
1362
+ if (icon !== void 0) a.icon = icon;
1363
+ return a;
1364
+ }, [id, label, icon, keys, tags, variants, hasEnabled, hasActive, latest]);
1365
+ useAction(action);
1165
1366
  }
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]);
1367
+ function bindingOf(keys) {
1368
+ if (isList(keys)) return [...keys];
1369
+ return "spec" in keys ? [keys] : keys;
1179
1370
  }
1371
+ var isList = (keys) => Array.isArray(keys);
1180
1372
  function useNodes(descends, ownsTool, parent) {
1181
1373
  const parentActions = useActionsRegistry();
1182
1374
  const parentDeps = useOptionalDepRegistry();
@@ -1200,7 +1392,8 @@ function useNodes(descends, ownsTool, parent) {
1200
1392
  return nodes;
1201
1393
  }
1202
1394
  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 }) }) });
1395
+ const menuHost = useContext(MenuHostContext) ?? nodes.actions.menuHost();
1396
+ 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
1397
  }
1205
1398
  function InputScope({ yoke, children }) {
1206
1399
  const nodes = useNodes(false, yoke === void 0, yoke);
@@ -1533,6 +1726,6 @@ function useTools(opts) {
1533
1726
  );
1534
1727
  }
1535
1728
 
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 };
1729
+ 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
1730
  //# sourceMappingURL=react.js.map
1538
1731
  //# sourceMappingURL=react.js.map