@weasel-js/routing 1.9.4 → 1.10.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,9 +1,10 @@
1
- import { createDepRegistry, createDispatcher, isTool, assembleEntries, liveScope, isDev, reportRouteConflicts, offhandKeyFor, makeToolOffhandAction, buildToolOffhandBindings, dlog, openPointerSession, LONG_PRESS_MS, publishLiveDispatch, pointerGestureId, ScopeNode, pushOwner, validateActionId, validateActionDefaultBinding, depNodeOf, DepNode, clientToCanvasRect, buildDepsFromRequires, isEditableTarget, activatesFocusedControl, itemsFromClipboardData, itemsFromDataTransfer } from './chunk-Y5RURZBH.js';
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
2
  import { createContext, useState, useContext, useEffect, useRef, useCallback, useMemo, useSyncExternalStore } from 'react';
3
+ import { isApplePlatform, heldPointers, createPointerRecognizer, isInControlWithin, longPressEvents, stylusOf } from '@weasel-js/gestures';
3
4
  import { useLatest, useStableByContent, sameList } from '@weasel-js/react';
4
5
  import { jsx, Fragment } from 'react/jsx-runtime';
5
- import { isApplePlatform, isInControlWithin, pastDragThreshold } from '@weasel-js/gestures';
6
- import { resolveCursorTier } from '@weasel-js/cursor';
6
+ import { createCursorSink, resolveCursorTier } from '@weasel-js/cursor';
7
+ import { createEmitter } from '@weasel-js/registry';
7
8
  import { activeModeOf } from '@weasel-js/modes';
8
9
 
9
10
  var ToolNode = class extends ScopeNode {
@@ -48,12 +49,7 @@ var ToolNode = class extends ScopeNode {
48
49
  this.value = { from, state, out };
49
50
  return out;
50
51
  }
51
- subscribe = (listener) => {
52
- this.tree.listeners.add(listener);
53
- return () => {
54
- this.tree.listeners.delete(listener);
55
- };
56
- };
52
+ subscribe = (listener) => this.tree.listeners.subscribe(listener);
57
53
  };
58
54
  var ActiveToolContext = createContext(null);
59
55
  var ActiveToolNodeContext = ActiveToolContext;
@@ -115,8 +111,93 @@ function useDepSource(name, source) {
115
111
  return registry.register(name, () => sourceRef.current());
116
112
  }, [name, registry, sourceRef]);
117
113
  }
114
+ function useToolLifecycle(opts) {
115
+ const { active, hotkeyStack } = opts;
116
+ const liveRef = useRef(/* @__PURE__ */ new Set());
117
+ const prevActiveRef = useRef(active);
118
+ const entriesRef = useLatest(opts.entriesById);
119
+ const onSwitchRef = useLatest(opts.onSwitch);
120
+ const fire = useCallback((id, hook) => {
121
+ const tool = entriesRef.current.get(id);
122
+ if (!tool || !isTool(tool)) return;
123
+ const fn = tool[hook];
124
+ if (!fn) return;
125
+ try {
126
+ fn();
127
+ } catch (err) {
128
+ console.error(`weasel: tool "${id}".${hook} threw`, err);
129
+ }
130
+ }, [entriesRef]);
131
+ useEffect(() => {
132
+ const prev = liveRef.current;
133
+ const next = new Set(active === null ? hotkeyStack : [active, ...hotkeyStack]);
134
+ liveRef.current = next;
135
+ for (const id of prev) if (!next.has(id)) fire(id, "onDeactivate");
136
+ if (prevActiveRef.current !== null && prevActiveRef.current !== active) onSwitchRef.current?.();
137
+ prevActiveRef.current = active;
138
+ for (const id of next) if (!prev.has(id)) fire(id, "onActivate");
139
+ });
140
+ useEffect(() => () => {
141
+ const live = liveRef.current;
142
+ liveRef.current = /* @__PURE__ */ new Set();
143
+ for (const id of live) fire(id, "onDeactivate");
144
+ }, [fire]);
145
+ }
118
146
 
119
147
  // src/interactions/dispatcher/dispatchCore.ts
148
+ function createDispatchCore(src) {
149
+ const { canvas } = src;
150
+ let routedId = null;
151
+ const target = () => {
152
+ if (routedId === null) return src.root();
153
+ return src.views()?.targets().find((t) => t.id === routedId) ?? src.root();
154
+ };
155
+ const dispatcherNow = () => target().dispatcher;
156
+ const routedDeps = withViewDeps(() => src.context().depRegistry, () => target().deps?.());
157
+ const routedRuleCtx = () => target().getRuleCtx?.();
158
+ const ctxNow = () => ({
159
+ ...src.context(),
160
+ depRegistry: routedDeps,
161
+ getRuleCtx: routedRuleCtx,
162
+ viewId: target().id
163
+ });
164
+ return {
165
+ canvas,
166
+ target,
167
+ routeAt: (pointerId, clientX, clientY) => {
168
+ routedId = src.views()?.resolver.at(pointerId, clientX, clientY).id ?? null;
169
+ },
170
+ routeDown: (pointerId, clientX, clientY) => {
171
+ routedId = src.views()?.resolver.begin(pointerId, clientX, clientY).id ?? null;
172
+ },
173
+ routeRelease: (pointerId) => {
174
+ src.views()?.resolver.end(pointerId);
175
+ },
176
+ routedView: () => routedId,
177
+ setRoutedView: (id) => {
178
+ routedId = id;
179
+ },
180
+ dispatcherNow,
181
+ ctxNow,
182
+ toWorld: (cx, cy) => {
183
+ const fn = target().clientToWorld;
184
+ return fn ? fn(cx, cy) : { x: cx, y: cy };
185
+ },
186
+ toCanvasLocal: (cx, cy) => {
187
+ if (!canvas) return { x: cx, y: cy };
188
+ const [x, y] = clientToCanvas(canvas, cx, cy);
189
+ return { x, y };
190
+ },
191
+ dispatch: (ev, on) => {
192
+ const d = on ?? dispatcherNow();
193
+ src.uses(d);
194
+ return d.handleInput(ev, ctxNow());
195
+ },
196
+ holdsKeys: () => canvas ? src.actions().holdsKeys() : src.actions().isActive(),
197
+ activate: () => src.actions().activate(),
198
+ disposed: src.disposed
199
+ };
200
+ }
120
201
  function withViewDeps(base, overlay) {
121
202
  return {
122
203
  register: (name, source) => base().register(name, source),
@@ -128,44 +209,183 @@ function withViewDeps(base, overlay) {
128
209
  };
129
210
  }
130
211
 
212
+ // src/interactions/dispatcher/dispatcherWatch.ts
213
+ function dispatcherWatch(onChange) {
214
+ const subscriptions = /* @__PURE__ */ new Map();
215
+ return {
216
+ sync(live) {
217
+ for (const d of live) {
218
+ if (!subscriptions.has(d)) subscriptions.set(d, d.subscribe(onChange));
219
+ }
220
+ if (subscriptions.size === live.length) return;
221
+ const keep = new Set(live);
222
+ for (const [d, unsubscribe] of subscriptions) {
223
+ if (keep.has(d)) continue;
224
+ unsubscribe();
225
+ subscriptions.delete(d);
226
+ }
227
+ },
228
+ stop() {
229
+ for (const unsubscribe of subscriptions.values()) unsubscribe();
230
+ subscriptions.clear();
231
+ }
232
+ };
233
+ }
234
+
235
+ // src/interactions/dispatcher/underPointer.ts
236
+ function underPointer(view, mods, w) {
237
+ const affordance = view.affordanceAt?.(w) ?? void 0;
238
+ const body = view.classifyTarget?.(w);
239
+ return {
240
+ altKey: mods.altKey,
241
+ ctrlKey: mods.ctrlKey,
242
+ metaKey: mods.metaKey,
243
+ shiftKey: mods.shiftKey,
244
+ ...affordance !== void 0 ? { affordance } : {},
245
+ ...body?.body !== void 0 ? { bodyTarget: body.body } : {},
246
+ ...body?.kind !== void 0 ? { bodyKind: body.kind } : {}
247
+ };
248
+ }
249
+
131
250
  // src/interactions/dispatcher/channels/contextMenu.ts
132
- function contextMenuInput(core) {
251
+ function contextMenuInput(core, takeNativeHold) {
133
252
  const { routeAt, toWorld, target, dispatch } = core;
134
253
  const onContextMenu = (e) => {
135
254
  e.preventDefault();
255
+ if (takeNativeHold()) return;
136
256
  routeAt(null, e.clientX, e.clientY);
137
257
  const w = toWorld(e.clientX, e.clientY);
138
- const affordance = target().affordanceAt?.(w) ?? void 0;
139
- const menuBody = target().classifyTarget?.(w);
140
258
  const ev = {
141
259
  kind: "contextmenu",
142
260
  target: e.target,
143
- altKey: e.altKey,
144
- ctrlKey: e.ctrlKey,
145
- metaKey: e.metaKey,
146
- shiftKey: e.shiftKey,
147
261
  x: w.x,
148
262
  y: w.y,
149
263
  clientX: e.clientX,
150
264
  clientY: e.clientY,
151
- ...affordance !== void 0 ? { affordance } : {},
152
- ...menuBody?.body !== void 0 ? { bodyTarget: menuBody.body } : {},
153
- ...menuBody?.kind !== void 0 ? { bodyKind: menuBody.kind } : {}
265
+ ...underPointer(target(), e, w)
154
266
  };
155
267
  dispatch(ev);
156
268
  };
157
269
  return onContextMenu;
158
270
  }
271
+ var whereOf = ({ affordance, bodyTarget, bodyKind }) => ({
272
+ ...affordance !== void 0 ? { affordance } : {},
273
+ ...bodyTarget !== void 0 ? { bodyTarget } : {},
274
+ ...bodyKind !== void 0 ? { bodyKind } : {}
275
+ });
276
+ var hoverOf = (e) => ({
277
+ clientX: e.clientX,
278
+ clientY: e.clientY,
279
+ altKey: e.altKey,
280
+ ctrlKey: e.ctrlKey,
281
+ metaKey: e.metaKey,
282
+ shiftKey: e.shiftKey
283
+ });
284
+ function hoverCursorInput(core, paintedCursor, pointersHeld, cursorSink = () => void 0) {
285
+ const { canvas, routeAt, toWorld, target, dispatch, dispatcherNow, ctxNow } = core;
286
+ const ownSink = createCursorSink({ element: () => canvas, painted: paintedCursor });
287
+ const override = {
288
+ apply: (spec) => (cursorSink() ?? ownSink).setOverride(spec === null ? null : resolveCursorTier(spec)),
289
+ clear: () => (cursorSink() ?? ownSink).setOverride(null)
290
+ };
291
+ let last = null;
292
+ let facts = null;
293
+ const resolve = () => {
294
+ if (!canvas || !last) return;
295
+ if (pointersHeld() > 0 || dispatcherNow().inFlight().size > 0) {
296
+ override.apply(dispatcherNow().inFlightCursor());
297
+ return;
298
+ }
299
+ const h = last;
300
+ routeAt(null, h.clientX, h.clientY);
301
+ const w = toWorld(h.clientX, h.clientY);
302
+ const where = facts?.at === h ? facts.where : underPointer(target(), h, w);
303
+ if (where.affordance?.cursor) {
304
+ override.apply(where.affordance.cursor);
305
+ return;
306
+ }
307
+ const ctx = ctxNow();
308
+ const d = dispatcherNow();
309
+ 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));
311
+ 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;
313
+ override.apply(cursor ?? null);
314
+ };
315
+ const refresh = () => {
316
+ resolve();
317
+ const painted = paintedCursor();
318
+ if (!painted?.active() || !canvas || !last) return;
319
+ const local = core.toCanvasLocal(last.clientX, last.clientY);
320
+ painted.setPointer(local.x, local.y);
321
+ };
322
+ const refreshSoon = () => {
323
+ setTimeout(() => {
324
+ if (!core.disposed()) refresh();
325
+ }, 0);
326
+ };
327
+ return {
328
+ moved(e) {
329
+ 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
+ }
337
+ refresh();
338
+ },
339
+ released(e) {
340
+ last = hoverOf(e);
341
+ refreshSoon();
342
+ },
343
+ onLeave() {
344
+ if (facts) {
345
+ const { at, where } = facts;
346
+ const w = toWorld(at.clientX, at.clientY);
347
+ dispatch({ kind: "hover", off: true, x: w.x, y: w.y, clientX: at.clientX, clientY: at.clientY, ...where, from: whereOf(where) });
348
+ }
349
+ facts = null;
350
+ last = null;
351
+ if (DEV) publishLiveDispatch(null);
352
+ paintedCursor()?.clearPointer();
353
+ override.clear();
354
+ },
355
+ onKey(e) {
356
+ if (!last) return;
357
+ last = { ...last, altKey: e.altKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey };
358
+ refreshSoon();
359
+ },
360
+ clear: override.clear
361
+ };
362
+ }
159
363
 
160
364
  // src/interactions/dispatcher/channels/ingest.ts
161
365
  var DROPOVER_CLASS = "weasel-dropover";
162
366
  function ingestInput(core) {
163
367
  const { canvas, routeAt, toWorld, dispatch } = core;
164
368
  const onDragOver = (e) => {
165
- if (!e.dataTransfer) return;
369
+ const dt = e.dataTransfer;
370
+ if (!dt) return;
371
+ routeAt(null, e.clientX, e.clientY);
372
+ const w = toWorld(e.clientX, e.clientY);
373
+ const wanted = core.dispatcherNow().resolveOnly({
374
+ kind: "drop",
375
+ items: itemTypesFromDataTransfer(dt),
376
+ x: w.x,
377
+ y: w.y,
378
+ clientX: e.clientX,
379
+ clientY: e.clientY,
380
+ altKey: e.altKey,
381
+ ctrlKey: e.ctrlKey,
382
+ metaKey: e.metaKey,
383
+ shiftKey: e.shiftKey
384
+ }, core.ctxNow()) !== null;
385
+ canvas?.classList.toggle(DROPOVER_CLASS, wanted);
386
+ if (!wanted) return;
166
387
  e.preventDefault();
167
- e.dataTransfer.dropEffect = "copy";
168
- canvas?.classList.add(DROPOVER_CLASS);
388
+ dt.dropEffect = "copy";
169
389
  };
170
390
  const onDragLeave = () => {
171
391
  canvas?.classList.remove(DROPOVER_CLASS);
@@ -226,24 +446,32 @@ function ingestInput(core) {
226
446
  }
227
447
 
228
448
  // src/interactions/dispatcher/channels/keyboard.ts
449
+ function typedWithAltGraph(e) {
450
+ if (!e.ctrlKey || !e.altKey || e.key.length !== 1 || !e.getModifierState?.("AltGraph")) return false;
451
+ const base = /^(?:Key|Digit)(.)$/.exec(e.code)?.[1];
452
+ return base === void 0 || base.toLowerCase() !== e.key.toLowerCase();
453
+ }
229
454
  function keyboardInput(core) {
230
455
  const { dispatch, dispatcherNow } = core;
231
456
  const heldKeys = /* @__PURE__ */ new Map();
232
457
  const codeOf = (e) => e.code ? { code: e.code } : {};
233
458
  const physicalKey = (e) => e.code || e.key.toLowerCase();
234
459
  const onKeyDown = (e) => {
235
- if (e.defaultPrevented || isEditableTarget(e.target) || activatesFocusedControl(e)) return;
460
+ if (e.defaultPrevented || isEditableTarget(e.target) || focusedControlOwnsKey(e)) return;
236
461
  if (!core.holdsKeys()) return;
237
462
  if (e.repeat && heldKeys.has(physicalKey(e))) {
238
463
  e.preventDefault();
239
464
  return;
240
465
  }
466
+ if (press(e, { altKey: e.altKey, ctrlKey: e.ctrlKey })) return;
467
+ if (typedWithAltGraph(e)) press(e, { altKey: false, ctrlKey: false });
468
+ };
469
+ const press = (e, mods) => {
241
470
  const keyEv = {
242
471
  kind: "key",
243
472
  key: e.key,
244
473
  ...codeOf(e),
245
- altKey: e.altKey,
246
- ctrlKey: e.ctrlKey,
474
+ ...mods,
247
475
  metaKey: e.metaKey,
248
476
  shiftKey: e.shiftKey,
249
477
  repeat: e.repeat
@@ -254,8 +482,7 @@ function keyboardInput(core) {
254
482
  key: e.key,
255
483
  ...codeOf(e),
256
484
  phase: "down",
257
- altKey: e.altKey,
258
- ctrlKey: e.ctrlKey,
485
+ ...mods,
259
486
  metaKey: e.metaKey,
260
487
  shiftKey: e.shiftKey
261
488
  };
@@ -264,9 +491,9 @@ function keyboardInput(core) {
264
491
  if (heldResult === "handled") {
265
492
  heldKeys.set(physicalKey(e), { key: e.key, ...codeOf(e), on: heldOn });
266
493
  }
267
- if (keyResult === "handled" || heldResult === "handled") {
268
- e.preventDefault();
269
- }
494
+ const handled = keyResult === "handled" || heldResult === "handled";
495
+ if (handled) e.preventDefault();
496
+ return handled;
270
497
  };
271
498
  const onKeyUp = (e) => {
272
499
  const physical = physicalKey(e);
@@ -305,6 +532,175 @@ function keyboardInput(core) {
305
532
  return { onKeyDown, onKeyUp, releaseAll };
306
533
  }
307
534
 
535
+ // src/interactions/dispatcher/channels/listeners.ts
536
+ function listenerSet() {
537
+ const removers = [];
538
+ return {
539
+ on(target, type, listener, options) {
540
+ if (!target) return;
541
+ target.addEventListener(type, listener, options);
542
+ removers.push(() => {
543
+ target.removeEventListener(type, listener, options);
544
+ });
545
+ },
546
+ removeAll() {
547
+ for (const remove of removers.splice(0)) remove();
548
+ }
549
+ };
550
+ }
551
+ var sampleOf = (e) => ({
552
+ pointerId: e.pointerId,
553
+ clientX: e.clientX,
554
+ clientY: e.clientY,
555
+ target: e.target,
556
+ altKey: e.altKey,
557
+ ctrlKey: e.ctrlKey,
558
+ metaKey: e.metaKey,
559
+ shiftKey: e.shiftKey,
560
+ ...stylusOf(e)
561
+ });
562
+ function holdPublisher(core, longPress) {
563
+ return (pending) => {
564
+ const store = longPress()?.state;
565
+ if (!store) return;
566
+ if (!pending) {
567
+ store.set(null);
568
+ return;
569
+ }
570
+ const { press } = pending;
571
+ const evs = longPressEvents(press);
572
+ const d = core.dispatcherNow();
573
+ store.set({
574
+ pointerId: pending.pointerId,
575
+ pointerType: pending.pointerType,
576
+ client: { x: press.clientX, y: press.clientY },
577
+ local: core.toCanvasLocal(press.clientX, press.clientY),
578
+ world: { x: press.worldX, y: press.worldY },
579
+ viewId: core.target().id,
580
+ startedAt: pending.startedAt,
581
+ duration: pending.duration,
582
+ armed: d.resolveOnly(evs.longpress, core.ctxNow()) !== null || d.resolveOnly(evs.contextmenu, core.ctxNow()) !== null
583
+ });
584
+ };
585
+ }
586
+ function holdClock(core) {
587
+ let holdView = null;
588
+ return {
589
+ clock: {
590
+ now: () => performance.now(),
591
+ setTimer: (fn, ms) => {
592
+ const view = core.routedView();
593
+ holdView = { id: view };
594
+ return setTimeout(() => {
595
+ holdView = null;
596
+ core.setRoutedView(view);
597
+ fn();
598
+ }, ms);
599
+ },
600
+ clearTimer: (timer) => {
601
+ holdView = null;
602
+ clearTimeout(timer);
603
+ }
604
+ },
605
+ routeToPress: () => {
606
+ if (holdView) core.setRoutedView(holdView.id);
607
+ }
608
+ };
609
+ }
610
+ function pointerInput(core, opts) {
611
+ const { canvas, routeAt, routeDown, routeRelease, toWorld, target } = core;
612
+ const sessions = /* @__PURE__ */ new Map();
613
+ let claimedDoubleClick = false;
614
+ const hold = holdClock(core);
615
+ const recognizer = createPointerRecognizer({
616
+ sink: {
617
+ dispatch: (ev) => core.dispatch(ev),
618
+ multiTouchInFlight: (fingers) => core.dispatcherNow().inFlight().has(multiTouchGestureId(fingers))
619
+ },
620
+ clock: hold.clock,
621
+ toWorld,
622
+ toSurface: (p) => core.toCanvasLocal(p.x, p.y),
623
+ longPressMs: () => opts.longPress()?.duration,
624
+ clicks: opts.clicks,
625
+ observer: {
626
+ click: (world) => {
627
+ opts.onClick()?.(world);
628
+ },
629
+ doubleClick: (world) => {
630
+ opts.onDoubleClick()?.(world);
631
+ },
632
+ holdChanged: holdPublisher(core, opts.longPress),
633
+ longPress: () => {
634
+ if (opts.longPress()?.haptics === false) return;
635
+ if (typeof navigator !== "undefined" && typeof navigator.vibrate === "function") {
636
+ navigator.vibrate(LONG_PRESS_HAPTIC_MS);
637
+ }
638
+ }
639
+ }
640
+ });
641
+ const onMove = (e) => {
642
+ routeAt(e.pointerId, e.clientX, e.clientY);
643
+ recognizer.move(sampleOf(e));
644
+ opts.hover.moved(e);
645
+ };
646
+ const onUp = (e) => {
647
+ routeAt(e.pointerId, e.clientX, e.clientY);
648
+ sessions.delete(e.pointerId);
649
+ const { doubleClick } = recognizer.release(sampleOf(e));
650
+ if (doubleClick) claimedDoubleClick = doubleClick === "handled" || opts.onDoubleClick() !== void 0;
651
+ routeRelease(e.pointerId);
652
+ opts.hover.released(e);
653
+ };
654
+ const onCancel = (pointerId) => {
655
+ if (core.disposed()) return;
656
+ const at = recognizer.positionOf(pointerId);
657
+ if (!at) return;
658
+ sessions.delete(pointerId);
659
+ routeAt(pointerId, at.x, at.y);
660
+ recognizer.cancel(pointerId);
661
+ routeRelease(pointerId);
662
+ };
663
+ return {
664
+ onPointerDown(e) {
665
+ claimedDoubleClick = false;
666
+ if ((e.button ?? 0) !== 0) return;
667
+ if (!canvas) return;
668
+ if (isInControlWithin(e.target, canvas)) return;
669
+ routeDown(e.pointerId, e.clientX, e.clientY);
670
+ const w = toWorld(e.clientX, e.clientY);
671
+ const where = underPointer(target(), e, w);
672
+ sessions.set(e.pointerId, openPointerSession(canvas, e, {
673
+ onMove,
674
+ onEnd: onUp,
675
+ onCancel: () => {
676
+ onCancel(e.pointerId);
677
+ }
678
+ }));
679
+ recognizer.press({ ...sampleOf(e), x: w.x, y: w.y, pointerType: e.pointerType, ...where });
680
+ },
681
+ onHoverMove(e) {
682
+ if (!sessions.has(e.pointerId)) onMove(e);
683
+ },
684
+ onNativeDoubleClick(e) {
685
+ if (!claimedDoubleClick) return;
686
+ claimedDoubleClick = false;
687
+ e.preventDefault();
688
+ },
689
+ takeNativeHold: () => {
690
+ hold.routeToPress();
691
+ return recognizer.takeHold();
692
+ },
693
+ cancelHeld() {
694
+ for (const session of [...sessions.values()]) session.cancel();
695
+ },
696
+ dispose() {
697
+ recognizer.reset();
698
+ for (const session of sessions.values()) session.cancel();
699
+ sessions.clear();
700
+ }
701
+ };
702
+ }
703
+
308
704
  // src/interactions/dispatcher/channels/webkitPinch.ts
309
705
  function webkitPinchInput(core) {
310
706
  const { canvas, routeAt, toWorld, target, dispatch, dispatcherNow, ctxNow } = core;
@@ -318,27 +714,25 @@ function webkitPinchInput(core) {
318
714
  };
319
715
  const pinchEventOf = (e, scale, rotation) => {
320
716
  const g = e;
321
- const rect = canvas?.getBoundingClientRect();
717
+ const rect = typeof g.clientX === "number" && typeof g.clientY === "number" ? void 0 : canvas?.getBoundingClientRect();
322
718
  const cx = typeof g.clientX === "number" ? g.clientX : rect ? rect.left + rect.width / 2 : 0;
323
719
  const cy = typeof g.clientY === "number" ? g.clientY : rect ? rect.top + rect.height / 2 : 0;
324
720
  routeAt(null, cx, cy);
325
- const [localX, localY] = rect ? clientToCanvasRect(rect, cx, cy) : [cx, cy];
721
+ const local = core.toCanvasLocal(cx, cy);
326
722
  const hitPoint = toWorld(cx, cy);
327
- const affordance = target().affordanceAt?.(hitPoint) ?? void 0;
328
- const body = target().classifyTarget?.(hitPoint);
329
- return {
330
- kind: "pinch",
723
+ const mods = {
331
724
  altKey: g.altKey === true,
332
725
  ctrlKey: g.ctrlKey === true,
333
726
  metaKey: g.metaKey === true,
334
- shiftKey: g.shiftKey === true,
727
+ shiftKey: g.shiftKey === true
728
+ };
729
+ return {
730
+ kind: "pinch",
335
731
  scale,
336
732
  rotation,
337
- clientX: localX,
338
- clientY: localY,
339
- ...affordance !== void 0 ? { affordance } : {},
340
- ...body?.body !== void 0 ? { bodyTarget: body.body } : {},
341
- ...body?.kind !== void 0 ? { bodyKind: body.kind } : {}
733
+ clientX: local.x,
734
+ clientY: local.y,
735
+ ...underPointer(target(), mods, hitPoint)
342
736
  };
343
737
  };
344
738
  const onGestureStart = (e) => {
@@ -372,7 +766,7 @@ function webkitPinchInput(core) {
372
766
 
373
767
  // src/interactions/dispatcher/channels/wheel.ts
374
768
  function wheelInput(core, pinchLive) {
375
- const { canvas, routeAt, toWorld, target, dispatch } = core;
769
+ const { routeAt, toWorld, target, dispatch } = core;
376
770
  const onWheel = (e) => {
377
771
  core.activate();
378
772
  if (pinchLive() && e.ctrlKey) {
@@ -381,24 +775,15 @@ function wheelInput(core, pinchLive) {
381
775
  return;
382
776
  }
383
777
  routeAt(null, e.clientX, e.clientY);
384
- const rect = canvas?.getBoundingClientRect();
385
- const [localX, localY] = rect ? clientToCanvasRect(rect, e.clientX, e.clientY) : [e.clientX, e.clientY];
778
+ const local = core.toCanvasLocal(e.clientX, e.clientY);
386
779
  const hitPoint = toWorld(e.clientX, e.clientY);
387
- const affordance = target().affordanceAt?.(hitPoint) ?? void 0;
388
- const body = target().classifyTarget?.(hitPoint);
389
780
  const ev = {
390
781
  kind: "wheel",
391
- altKey: e.altKey,
392
- ctrlKey: e.ctrlKey,
393
- metaKey: e.metaKey,
394
- shiftKey: e.shiftKey,
395
782
  deltaX: e.deltaX,
396
783
  deltaY: e.deltaY,
397
- clientX: localX,
398
- clientY: localY,
399
- ...affordance !== void 0 ? { affordance } : {},
400
- ...body?.body !== void 0 ? { bodyTarget: body.body } : {},
401
- ...body?.kind !== void 0 ? { bodyKind: body.kind } : {}
784
+ clientX: local.x,
785
+ clientY: local.y,
786
+ ...underPointer(target(), e, hitPoint)
402
787
  };
403
788
  const result = dispatch(ev);
404
789
  if (result === "handled") {
@@ -409,34 +794,91 @@ function wheelInput(core, pinchLive) {
409
794
  return onWheel;
410
795
  }
411
796
 
412
- // src/interactions/dispatcher/useGestureDispatcher.tsx
413
- function stylusOf(e) {
414
- return {
415
- ...typeof e.pressure === "number" ? { pressure: e.pressure } : {},
416
- ...typeof e.tiltX === "number" ? { tiltX: e.tiltX } : {},
417
- ...typeof e.tiltY === "number" ? { tiltY: e.tiltY } : {}
797
+ // src/interactions/dispatcher/attachInput.ts
798
+ function attachInput(canvas, src, channels) {
799
+ let disposed = false;
800
+ const redraw = dispatcherWatch(src.requestRedraw);
801
+ redraw.sync(src.dispatchers());
802
+ const core = createDispatchCore({
803
+ canvas,
804
+ root: src.root,
805
+ views: src.views,
806
+ context: src.context,
807
+ actions: src.actions,
808
+ uses: () => {
809
+ redraw.sync(src.dispatchers());
810
+ },
811
+ disposed: () => disposed
812
+ });
813
+ const keys = keyboardInput(core);
814
+ const pinch = webkitPinchInput(core);
815
+ const ingest = ingestInput(core);
816
+ const held = canvas ? heldPointers(canvas) : null;
817
+ const hover = hoverCursorInput(core, src.paintedCursor, () => held?.count() ?? 0, src.cursorSink);
818
+ const pointer = pointerInput(core, {
819
+ longPress: src.longPress,
820
+ onClick: src.onClick,
821
+ onDoubleClick: src.onDoubleClick,
822
+ clicks: src.clicks,
823
+ hover
824
+ });
825
+ const onWindowBlur = () => {
826
+ pinch.end();
827
+ pointer.cancelHeld();
828
+ keys.releaseAll();
418
829
  };
419
- }
420
- var IS_MAC = isApplePlatform();
421
- function computeMultiTouchGeometry(positions) {
422
- const pts = [...positions];
423
- if (pts.length < 2) {
424
- return { centroid: { x: 0, y: 0 }, spread: 0 };
830
+ const listeners = listenerSet();
831
+ const keyCapture = channels.keyboard === "first";
832
+ if (channels.keyboard) {
833
+ listeners.on(window, "keydown", keys.onKeyDown, keyCapture);
834
+ listeners.on(window, "keyup", keys.onKeyUp, keyCapture);
425
835
  }
426
- let sumX = 0;
427
- let sumY = 0;
428
- for (const p of pts) {
429
- sumX += p.x;
430
- sumY += p.y;
836
+ listeners.on(window, "blur", onWindowBlur);
837
+ listeners.on(canvas, "pointerdown", () => {
838
+ src.actions().activate();
839
+ });
840
+ if (channels.pointer) {
841
+ listeners.on(window, "keydown", hover.onKey);
842
+ listeners.on(window, "keyup", hover.onKey);
843
+ listeners.on(canvas, "pointerleave", hover.onLeave);
844
+ listeners.on(canvas, "pointerdown", pointer.onPointerDown);
845
+ listeners.on(canvas, "pointermove", pointer.onHoverMove);
846
+ listeners.on(canvas, "dblclick", pointer.onNativeDoubleClick);
847
+ }
848
+ if (channels.wheel) {
849
+ listeners.on(canvas, "wheel", wheelInput(core, pinch.live), { passive: false, capture: true });
850
+ }
851
+ if (channels.pinch) {
852
+ listeners.on(canvas, "gesturestart", pinch.onGestureStart);
853
+ listeners.on(canvas, "gesturechange", pinch.onGestureChange);
854
+ listeners.on(canvas, "gestureend", pinch.onGestureEnd);
855
+ }
856
+ if (channels.contextMenu) {
857
+ listeners.on(canvas, "contextmenu", contextMenuInput(core, pointer.takeNativeHold));
858
+ }
859
+ if (channels.ingest) {
860
+ listeners.on(canvas, "dragenter", ingest.onDragOver);
861
+ listeners.on(canvas, "dragover", ingest.onDragOver);
862
+ listeners.on(canvas, "dragleave", ingest.onDragLeave);
863
+ listeners.on(canvas, "drop", ingest.onDrop);
864
+ listeners.on(window, "paste", ingest.onPaste);
431
865
  }
432
- const centroid = { x: sumX / pts.length, y: sumY / pts.length };
433
- const dx = pts[1].x - pts[0].x;
434
- const dy = pts[1].y - pts[0].y;
435
- const spread = Math.sqrt(dx * dx + dy * dy);
436
- return { centroid, spread };
866
+ return () => {
867
+ listeners.removeAll();
868
+ canvas?.classList.remove(DROPOVER_CLASS);
869
+ src.actions().dropKeys();
870
+ hover.clear();
871
+ disposed = true;
872
+ pointer.dispose();
873
+ held?.release();
874
+ redraw.stop();
875
+ for (const d of src.dispatchers()) d.cancelAll("cancel");
876
+ };
437
877
  }
878
+
879
+ // src/interactions/dispatcher/useGestureDispatcher.tsx
438
880
  function useGestureDispatcher(opts) {
439
- const { canvasRef, actions, entriesById, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, clientToWorld, requestRedraw, paintedCursor, getRuleCtx, onClick, onDoubleClick, views, channels, longPress, claimKeys = false } = 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;
440
882
  const pointerChannel = channels?.pointer !== false;
441
883
  const wheelChannel = channels?.wheel !== false;
442
884
  const pinchChannel = channels?.pinch !== false;
@@ -446,6 +888,7 @@ function useGestureDispatcher(opts) {
446
888
  const onDoubleClickRef = useLatest(onDoubleClick);
447
889
  const activeTool = useActiveToolContext();
448
890
  const depRegistry = useDepRegistry();
891
+ const [isMac] = useState(isApplePlatform);
449
892
  const dispatcherRef = useRef(null);
450
893
  if (!dispatcherRef.current) {
451
894
  dispatcherRef.current = dispatcherOpt ?? createDispatcher();
@@ -457,7 +900,7 @@ function useGestureDispatcher(opts) {
457
900
  activeToolId: activeTool.active,
458
901
  hotkeyStack: activeTool.hotkeyStack,
459
902
  entriesById,
460
- isMac: IS_MAC
903
+ isMac
461
904
  });
462
905
  const rootTargetRef = useLatest({
463
906
  id: null,
@@ -473,676 +916,46 @@ function useGestureDispatcher(opts) {
473
916
  ...(viewsRef.current?.targets() ?? []).map((t) => t.dispatcher)
474
917
  ], [rootTargetRef, viewsRef]);
475
918
  const requestRedrawRef = useLatest(requestRedraw);
476
- const paintedCursorRef = useLatest(paintedCursor);
919
+ const cursorRef = useLatest({ painted: paintedCursor, sink: cursorSink });
477
920
  const longPressRef = useLatest(longPress);
478
- const lastClickRef = useRef(null);
479
- const liveToolsRef = useRef(/* @__PURE__ */ new Set());
480
- const prevActiveRef = useRef(activeTool.active);
481
- const entriesByIdRef = useLatest(entriesById);
482
- const lifecycleScratchRef = useRef(/* @__PURE__ */ new Map());
483
- const fireLifecycle = useCallback((id, hook) => {
484
- const scratches = lifecycleScratchRef.current;
485
- const tool = entriesByIdRef.current.get(id);
486
- if (!tool || !isTool(tool)) {
487
- scratches.delete(id);
488
- return;
489
- }
490
- if (hook === "onActivate" || !scratches.has(id)) scratches.set(id, tool.initScratch?.());
491
- const scratch = scratches.get(id);
492
- if (hook === "onDeactivate") scratches.delete(id);
493
- const fn = tool[hook];
494
- if (!fn) return;
495
- try {
496
- fn({ scratch });
497
- } catch (err) {
498
- console.error(`weasel: tool "${id}".${hook} threw`, err);
499
- }
500
- }, [entriesByIdRef]);
501
- useEffect(() => {
502
- const prev = liveToolsRef.current;
503
- const next = new Set(
504
- activeTool.active === null ? activeTool.hotkeyStack : [activeTool.active, ...activeTool.hotkeyStack]
505
- );
506
- liveToolsRef.current = next;
507
- for (const id of prev) if (!next.has(id)) fireLifecycle(id, "onDeactivate");
508
- if (prevActiveRef.current !== activeTool.active) {
921
+ const clicksRef = useRef({ last: null });
922
+ useToolLifecycle({
923
+ entriesById,
924
+ active: activeTool.active,
925
+ hotkeyStack: activeTool.hotkeyStack,
926
+ // The incoming tool must not inherit a live gesture.
927
+ onSwitch: () => {
509
928
  for (const d of allDispatchers()) d.cancelAll("cancel");
510
929
  }
511
- prevActiveRef.current = activeTool.active;
512
- for (const id of next) if (!prev.has(id)) fireLifecycle(id, "onActivate");
513
930
  });
514
- useEffect(() => () => {
515
- const live = liveToolsRef.current;
516
- liveToolsRef.current = /* @__PURE__ */ new Set();
517
- for (const id of live) fireLifecycle(id, "onDeactivate");
518
- }, [fireLifecycle]);
519
931
  useEffect(() => {
520
932
  if (enabled && claimKeys) registryRef.current.activate();
521
933
  }, [enabled, claimKeys, registryRef]);
522
934
  useEffect(() => {
523
935
  if (!enabled) return;
524
- let disposed = false;
525
- let routedId = null;
526
- const target = () => {
527
- if (routedId === null) return rootTargetRef.current;
528
- return viewsRef.current?.targets().find((t) => t.id === routedId) ?? rootTargetRef.current;
529
- };
530
- const routeAt = (pointerId, clientX, clientY) => {
531
- routedId = viewsRef.current?.resolver.at(pointerId, clientX, clientY).id ?? null;
532
- };
533
- const routeDown = (pointerId, clientX, clientY) => {
534
- routedId = viewsRef.current?.resolver.begin(pointerId, clientX, clientY).id ?? null;
535
- };
536
- const routeRelease = (pointerId) => {
537
- viewsRef.current?.resolver.end(pointerId);
538
- };
539
- const dispatcherNow = () => target().dispatcher;
540
- const routedDeps = withViewDeps(() => ctxRef.current.depRegistry, () => target().deps?.());
541
- const routedRuleCtx = () => target().getRuleCtx?.();
542
- const ctxNow = () => ({
543
- ...ctxRef.current,
544
- depRegistry: routedDeps,
545
- getRuleCtx: routedRuleCtx,
546
- viewId: target().id
547
- });
548
- const canvas = canvasRef.current;
549
- const toWorld = (cx, cy) => {
550
- const fn = target().clientToWorld;
551
- return fn ? fn(cx, cy) : { x: cx, y: cy };
552
- };
553
- const dispatch = (ev, on) => {
554
- const d = on ?? dispatcherNow();
555
- const before = d.inFlight().size;
556
- const result = d.handleInput(ev, ctxNow());
557
- const after = d.inFlight().size;
558
- if (before > 0 || after > 0) requestRedrawRef.current?.();
559
- return result;
560
- };
561
- const core = {
562
- canvas,
563
- target,
564
- routeAt,
565
- routedView: () => routedId,
566
- setRoutedView: (id) => {
567
- routedId = id;
936
+ return attachInput(canvasRef.current, {
937
+ root: () => rootTargetRef.current,
938
+ views: () => viewsRef.current,
939
+ context: () => ctxRef.current,
940
+ actions: () => registryRef.current,
941
+ dispatchers: allDispatchers,
942
+ requestRedraw: () => {
943
+ requestRedrawRef.current?.();
568
944
  },
569
- dispatcherNow,
570
- ctxNow,
571
- toWorld,
572
- dispatch,
573
- holdsKeys: () => canvas ? registryRef.current.holdsKeys() : registryRef.current.isActive(),
574
- activate: () => registryRef.current.activate(),
575
- disposed: () => disposed
576
- };
577
- const keys = keyboardInput(core);
578
- const { onKeyDown, onKeyUp } = keys;
579
- const pinch = webkitPinchInput(core);
580
- const { onGestureStart, onGestureChange, onGestureEnd } = pinch;
581
- const onWheel = wheelInput(core, pinch.live);
582
- const onContextMenu = contextMenuInput(core);
583
- const { onDragOver, onDragLeave, onDrop, onPaste } = ingestInput(core);
584
- const held = /* @__PURE__ */ new Map();
585
- const toCanvasLocal = (p) => {
586
- const rect = canvas?.getBoundingClientRect();
587
- if (!rect) return p;
588
- const [x, y] = clientToCanvasRect(rect, p.x, p.y);
589
- return { x, y };
590
- };
591
- const heldGeometry = () => {
592
- const { centroid, spread } = computeMultiTouchGeometry(
593
- [...held.values()].map((p) => p.pos)
594
- );
595
- return { centroid: toCanvasLocal(centroid), spread };
596
- };
597
- let multiTouchStart = null;
598
- const TAP_THRESHOLD_PX = 8;
599
- const longPressTimers = /* @__PURE__ */ new Map();
600
- const clearPending = (pointerId) => {
601
- const store = longPressRef.current?.state;
602
- const p = store?.get();
603
- if (p && (pointerId === void 0 || p.pointerId === pointerId)) store.set(null);
604
- };
605
- const cancelLongPress = (pointerId) => {
606
- const t = longPressTimers.get(pointerId);
607
- if (t !== void 0) {
608
- clearTimeout(t);
609
- longPressTimers.delete(pointerId);
610
- }
611
- clearPending(pointerId);
612
- };
613
- const cancelAllLongPress = () => {
614
- for (const t of longPressTimers.values()) clearTimeout(t);
615
- longPressTimers.clear();
616
- clearPending();
617
- };
618
- const HAPTIC_MS = 15;
619
- const longPressEvents = (down) => {
620
- const shared = {
621
- target: down.target,
622
- altKey: down.altKey,
623
- ctrlKey: down.ctrlKey,
624
- metaKey: down.metaKey,
625
- shiftKey: down.shiftKey,
626
- ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
627
- ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
628
- ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
629
- };
630
- const at = { x: down.worldX, y: down.worldY, clientX: down.clientX, clientY: down.clientY };
631
- return {
632
- longpress: { kind: "longpress", ...at, ...shared },
633
- contextmenu: { kind: "contextmenu", ...at, ...shared }
634
- };
635
- };
636
- const longPressWouldFire = (down) => {
637
- const evs = longPressEvents(down);
638
- const d = dispatcherNow();
639
- return d.resolveOnly(evs.longpress, ctxNow()) !== null || d.resolveOnly(evs.contextmenu, ctxNow()) !== null;
640
- };
641
- const fireLongPress = (pointerId, pointerType) => {
642
- const rec = held.get(pointerId);
643
- clearPending(pointerId);
644
- if (!rec) return;
645
- const evs = longPressEvents(rec.down);
646
- let result = dispatch(evs.longpress);
647
- if (result === "unhandled") result = dispatch(evs.contextmenu);
648
- if (result === "handled") rec.buffered = null;
649
- if (result === "handled" && longPressRef.current?.haptics !== false && (pointerType === "touch" || pointerType === "pen") && typeof navigator !== "undefined" && typeof navigator.vibrate === "function") {
650
- navigator.vibrate(HAPTIC_MS);
651
- }
652
- };
653
- const DOUBLE_CLICK_MAX_MS = 600;
654
- const DOUBLE_CLICK_MAX_PX = 8;
655
- const onWindowBlur = () => {
656
- pinch.end();
657
- for (const p of [...held.values()]) p.session.cancel();
658
- keys.releaseAll();
659
- };
660
- let claimedDoubleClick = false;
661
- const onNativeDoubleClick = (e) => {
662
- if (!claimedDoubleClick) return;
663
- claimedDoubleClick = false;
664
- e.preventDefault();
665
- };
666
- const onPointerDown = (e) => {
667
- claimedDoubleClick = false;
668
- if ((e.button ?? 0) !== 0) return;
669
- if (!canvas) return;
670
- if (isInControlWithin(e.target, canvas)) return;
671
- routeDown(e.pointerId, e.clientX, e.clientY);
672
- const w = toWorld(e.clientX, e.clientY);
673
- const affordance = target().affordanceAt?.(w) ?? void 0;
674
- const body = target().classifyTarget?.(w);
675
- const bodyTarget = body?.body;
676
- const bodyKind = body?.kind;
677
- const ev = {
678
- kind: "pointerdown",
679
- target: e.target,
680
- pointerId: e.pointerId,
681
- x: w.x,
682
- y: w.y,
683
- clientX: e.clientX,
684
- clientY: e.clientY,
685
- altKey: e.altKey,
686
- ctrlKey: e.ctrlKey,
687
- metaKey: e.metaKey,
688
- shiftKey: e.shiftKey,
689
- ...stylusOf(e),
690
- ...affordance !== void 0 ? { affordance } : {},
691
- ...bodyTarget !== void 0 ? { bodyTarget } : {},
692
- ...bodyKind !== void 0 ? { bodyKind } : {}
693
- };
694
- held.set(e.pointerId, {
695
- session: openPointerSession(canvas, e, {
696
- onMove: onPointerMove,
697
- onEnd: onPointerUp,
698
- onCancel: () => {
699
- onPointerCancel(e.pointerId);
700
- }
701
- }),
702
- pos: { x: e.clientX, y: e.clientY },
703
- // Deferred until the first past-threshold pointermove: a bare
704
- // press-and-release should fire `click`, not start a drag action.
705
- buffered: { ev, clientX: e.clientX, clientY: e.clientY },
706
- down: {
707
- target: e.target,
708
- clientX: e.clientX,
709
- clientY: e.clientY,
710
- worldX: w.x,
711
- worldY: w.y,
712
- affordance,
713
- bodyTarget,
714
- bodyKind,
715
- altKey: e.altKey,
716
- ctrlKey: e.ctrlKey,
717
- metaKey: e.metaKey,
718
- shiftKey: e.shiftKey
719
- }
720
- });
721
- if (held.size < 2) dispatch({ ...ev, stage: "press" });
722
- if ((e.pointerType === "touch" || e.pointerType === "pen") && held.size === 1) {
723
- cancelLongPress(e.pointerId);
724
- const duration = longPressRef.current?.duration ?? LONG_PRESS_MS;
725
- longPressTimers.set(
726
- e.pointerId,
727
- setTimeout(() => {
728
- longPressTimers.delete(e.pointerId);
729
- if (disposed) return;
730
- fireLongPress(e.pointerId, e.pointerType);
731
- }, duration)
732
- );
733
- const store = longPressRef.current?.state;
734
- const rec = held.get(e.pointerId);
735
- if (store && rec) {
736
- store.set({
737
- pointerId: e.pointerId,
738
- pointerType: e.pointerType,
739
- client: { x: e.clientX, y: e.clientY },
740
- local: toCanvasLocal({ x: e.clientX, y: e.clientY }),
741
- world: { x: w.x, y: w.y },
742
- viewId: target().id,
743
- startedAt: performance.now(),
744
- duration,
745
- armed: longPressWouldFire(rec.down)
746
- });
747
- }
748
- }
749
- if (held.size >= 2) {
750
- cancelAllLongPress();
751
- for (const p of held.values()) p.buffered = null;
752
- const { centroid, spread } = heldGeometry();
753
- if (!multiTouchStart) {
754
- multiTouchStart = {
755
- fingers: held.size,
756
- centroid: { ...centroid },
757
- at: /* @__PURE__ */ new Map(),
758
- moved: false,
759
- startedAt: performance.now()
760
- };
761
- } else {
762
- multiTouchStart.fingers = held.size;
763
- }
764
- for (const [id, p] of held) {
765
- if (!multiTouchStart.at.has(id)) multiTouchStart.at.set(id, { ...p.pos });
766
- }
767
- const mt = {
768
- kind: "multitouch",
769
- fingers: held.size,
770
- altKey: e.altKey,
771
- ctrlKey: e.ctrlKey,
772
- metaKey: e.metaKey,
773
- shiftKey: e.shiftKey,
774
- centroid,
775
- spread
776
- };
777
- dispatch(mt);
778
- }
779
- };
780
- const endMultiTouch = (end, fingers, mods) => {
781
- dispatch({
782
- kind: "multitouch",
783
- fingers,
784
- end,
785
- altKey: mods.altKey,
786
- ctrlKey: mods.ctrlKey,
787
- metaKey: mods.metaKey,
788
- shiftKey: mods.shiftKey
789
- });
790
- };
791
- const onPointerMove = (e) => {
792
- routeAt(e.pointerId, e.clientX, e.clientY);
793
- const rec = held.get(e.pointerId);
794
- if (rec) rec.pos = { x: e.clientX, y: e.clientY };
795
- const joinedAt = multiTouchStart?.at.get(e.pointerId);
796
- if (multiTouchStart && joinedAt && Math.hypot(e.clientX - joinedAt.x, e.clientY - joinedAt.y) > TAP_THRESHOLD_PX) {
797
- multiTouchStart.moved = true;
798
- }
799
- const buffered = rec?.buffered;
800
- if (rec && buffered) {
801
- if (pastDragThreshold(buffered, e)) {
802
- rec.buffered = null;
803
- cancelLongPress(e.pointerId);
804
- dispatch(buffered.ev);
805
- }
806
- }
807
- const mtGestureId = `multitouch-${held.size}`;
808
- if (held.size >= 2 && dispatcherNow().inFlight().has(mtGestureId)) {
809
- const { centroid, spread } = heldGeometry();
810
- const mtEv = {
811
- kind: "multitouch",
812
- fingers: held.size,
813
- altKey: e.altKey,
814
- ctrlKey: e.ctrlKey,
815
- metaKey: e.metaKey,
816
- shiftKey: e.shiftKey,
817
- centroid,
818
- spread
819
- };
820
- dispatch(mtEv);
821
- }
822
- const w = toWorld(e.clientX, e.clientY);
823
- const ev = {
824
- kind: "pointermove",
825
- pointerId: e.pointerId,
826
- x: w.x,
827
- y: w.y,
828
- clientX: e.clientX,
829
- clientY: e.clientY,
830
- altKey: e.altKey,
831
- ctrlKey: e.ctrlKey,
832
- metaKey: e.metaKey,
833
- shiftKey: e.shiftKey,
834
- ...stylusOf(e)
835
- };
836
- dispatch(ev);
837
- lastHover = {
838
- clientX: e.clientX,
839
- clientY: e.clientY,
840
- altKey: e.altKey,
841
- ctrlKey: e.ctrlKey,
842
- metaKey: e.metaKey,
843
- shiftKey: e.shiftKey
844
- };
845
- refreshHoverCursor();
846
- };
847
- let lastHover = null;
848
- let cursorOverridden = false;
849
- const clearHoverCursor = () => {
850
- paintedCursorRef.current?.()?.setOverride(null);
851
- if (cursorOverridden && canvas) {
852
- canvas.style.cursor = "";
853
- cursorOverridden = false;
854
- }
855
- };
856
- const applyHoverCursor = (spec) => {
857
- const tier = spec === null ? null : resolveCursorTier(spec);
858
- if (!tier || !canvas) {
859
- clearHoverCursor();
860
- return;
861
- }
862
- const cursor = tier.kind === "css" ? tier.css : "none";
863
- if (!cursor) {
864
- clearHoverCursor();
865
- return;
866
- }
867
- paintedCursorRef.current?.()?.setOverride(tier.kind === "painted" ? tier : "none");
868
- canvas.style.cursor = cursor;
869
- cursorOverridden = true;
870
- };
871
- function pushPaintedPointer() {
872
- const painted = paintedCursorRef.current?.();
873
- if (!painted?.active() || !canvas || !lastHover) return;
874
- const r = canvas.getBoundingClientRect();
875
- painted.setPointer(lastHover.clientX - r.left, lastHover.clientY - r.top);
876
- }
877
- function refreshHoverCursor() {
878
- resolveHoverCursor();
879
- pushPaintedPointer();
880
- }
881
- function resolveHoverCursor() {
882
- if (!canvas || !lastHover) return;
883
- if (held.size > 0 || dispatcherNow().inFlight().size > 0) {
884
- applyHoverCursor(dispatcherNow().inFlightCursor());
885
- return;
886
- }
887
- const h = lastHover;
888
- routeAt(null, h.clientX, h.clientY);
889
- const w = toWorld(h.clientX, h.clientY);
890
- const affordance = target().affordanceAt?.(w) ?? void 0;
891
- if (affordance?.cursor) {
892
- applyHoverCursor(affordance.cursor);
893
- return;
894
- }
895
- const hoverBody = target().classifyTarget?.(w);
896
- const bodyTarget = hoverBody?.body;
897
- const bodyKind = hoverBody?.kind;
898
- const where = {
899
- altKey: h.altKey,
900
- ctrlKey: h.ctrlKey,
901
- metaKey: h.metaKey,
902
- shiftKey: h.shiftKey,
903
- ...affordance !== void 0 ? { affordance } : {},
904
- ...bodyTarget !== void 0 ? { bodyTarget } : {},
905
- ...bodyKind !== void 0 ? { bodyKind } : {}
906
- };
907
- const ctx = ctxNow();
908
- const press = {
909
- kind: "pointerdown",
910
- x: w.x,
911
- y: w.y,
912
- clientX: h.clientX,
913
- clientY: h.clientY,
914
- ...where
915
- };
916
- const d = dispatcherNow();
917
- publishLiveDispatch(() => d.explain(press, ctx));
918
- const dragCursor = d.resolveOnly(press, ctx)?.action.cursor;
919
- const cursor = dragCursor ?? dispatcherNow().resolveOnly(
920
- { kind: "click", x: w.x, y: w.y, pressX: w.x, pressY: w.y, ...where },
921
- ctx
922
- )?.action.cursor;
923
- applyHoverCursor(cursor ?? null);
924
- }
925
- const scheduleHoverCursorRefresh = (e) => {
926
- if (!lastHover) return;
927
- lastHover = {
928
- ...lastHover,
929
- altKey: e.altKey,
930
- ctrlKey: e.ctrlKey,
931
- metaKey: e.metaKey,
932
- shiftKey: e.shiftKey
933
- };
934
- setTimeout(() => {
935
- if (!disposed) refreshHoverCursor();
936
- }, 0);
937
- };
938
- const onHoverMove = (e) => {
939
- if (held.has(e.pointerId)) return;
940
- onPointerMove(e);
941
- };
942
- const onHoverPointerLeave = () => {
943
- lastHover = null;
944
- publishLiveDispatch(null);
945
- paintedCursorRef.current?.()?.clearPointer();
946
- clearHoverCursor();
947
- };
948
- const onPointerUp = (e) => {
949
- routeAt(e.pointerId, e.clientX, e.clientY);
950
- cancelLongPress(e.pointerId);
951
- const prevSize = held.size;
952
- const rec = held.get(e.pointerId);
953
- held.delete(e.pointerId);
954
- const staysUnderThreshold = rec?.buffered != null;
955
- if (prevSize >= 2) endMultiTouch("commit", prevSize, e);
956
- if (prevSize >= 2 && held.size < 2) {
957
- const start = multiTouchStart;
958
- if (start) {
959
- const centroid = toCanvasLocal(
960
- computeMultiTouchGeometry([
961
- ...[...held.values()].map((p) => p.pos),
962
- { x: e.clientX, y: e.clientY }
963
- ]).centroid
964
- );
965
- const dx = centroid.x - start.centroid.x;
966
- const dy = centroid.y - start.centroid.y;
967
- const brief = performance.now() - start.startedAt < (longPressRef.current?.duration ?? LONG_PRESS_MS);
968
- if (brief && !start.moved && Math.hypot(dx, dy) <= TAP_THRESHOLD_PX) {
969
- const tap = {
970
- kind: "multitouchtap",
971
- fingers: start.fingers,
972
- altKey: e.altKey,
973
- ctrlKey: e.ctrlKey,
974
- metaKey: e.metaKey,
975
- shiftKey: e.shiftKey
976
- };
977
- dispatch(tap);
978
- }
979
- multiTouchStart = null;
980
- }
981
- }
982
- const movedDuringDrag = rec ? rec.down.clientX !== e.clientX || rec.down.clientY !== e.clientY : true;
983
- const hadDragInFlight = dispatcherNow().inFlight().has(pointerGestureId(e.pointerId)) && movedDuringDrag;
984
- const wUp = toWorld(e.clientX, e.clientY);
985
- const ev = {
986
- kind: "pointerup",
987
- pointerId: e.pointerId,
988
- x: wUp.x,
989
- y: wUp.y,
990
- clientX: e.clientX,
991
- clientY: e.clientY,
992
- altKey: e.altKey,
993
- ctrlKey: e.ctrlKey,
994
- metaKey: e.metaKey,
995
- shiftKey: e.shiftKey
996
- };
997
- dispatch(ev);
998
- const down = rec?.down;
999
- if (!hadDragInFlight && staysUnderThreshold && down) {
1000
- const wClick = toWorld(e.clientX, e.clientY);
1001
- const clickEv = {
1002
- kind: "click",
1003
- target: e.target,
1004
- altKey: e.altKey,
1005
- ctrlKey: e.ctrlKey,
1006
- metaKey: e.metaKey,
1007
- shiftKey: e.shiftKey,
1008
- x: wClick.x,
1009
- y: wClick.y,
1010
- clientX: e.clientX,
1011
- clientY: e.clientY,
1012
- pressX: down.worldX,
1013
- pressY: down.worldY,
1014
- ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
1015
- ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
1016
- ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
1017
- };
1018
- dispatch(clickEv);
1019
- onClickRef.current?.({ x: wClick.x, y: wClick.y });
1020
- const now = Date.now();
1021
- const prev = lastClickRef.current;
1022
- const dx = e.clientX - (prev?.clientX ?? 0);
1023
- const dy = e.clientY - (prev?.clientY ?? 0);
1024
- if (prev && now - prev.t < DOUBLE_CLICK_MAX_MS && Math.hypot(dx, dy) < DOUBLE_CLICK_MAX_PX) {
1025
- const dblEv = {
1026
- kind: "doubleclick",
1027
- target: e.target,
1028
- altKey: e.altKey,
1029
- ctrlKey: e.ctrlKey,
1030
- metaKey: e.metaKey,
1031
- shiftKey: e.shiftKey,
1032
- x: wClick.x,
1033
- y: wClick.y,
1034
- clientX: e.clientX,
1035
- clientY: e.clientY,
1036
- ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
1037
- ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
1038
- ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
1039
- };
1040
- onDoubleClickRef.current?.({ x: wClick.x, y: wClick.y });
1041
- const handled = dispatch(dblEv) === "handled";
1042
- claimedDoubleClick = handled || onDoubleClickRef.current !== void 0;
1043
- lastClickRef.current = null;
1044
- } else {
1045
- lastClickRef.current = { t: now, clientX: e.clientX, clientY: e.clientY };
1046
- }
1047
- }
1048
- routeRelease(e.pointerId);
1049
- lastHover = {
1050
- clientX: e.clientX,
1051
- clientY: e.clientY,
1052
- altKey: e.altKey,
1053
- ctrlKey: e.ctrlKey,
1054
- metaKey: e.metaKey,
1055
- shiftKey: e.shiftKey
1056
- };
1057
- setTimeout(() => {
1058
- if (!disposed) refreshHoverCursor();
1059
- }, 0);
1060
- };
1061
- const onPointerCancel = (pointerId) => {
1062
- if (disposed) return;
1063
- const rec = held.get(pointerId);
1064
- if (!rec) return;
1065
- const prevSize = held.size;
1066
- held.delete(pointerId);
1067
- routeAt(pointerId, rec.pos.x, rec.pos.y);
1068
- cancelLongPress(pointerId);
1069
- if (prevSize >= 2) endMultiTouch("cancel", prevSize, rec.down);
1070
- multiTouchStart = null;
1071
- const ev = {
1072
- kind: "pointercancel",
1073
- pointerId,
1074
- altKey: rec.down.altKey,
1075
- ctrlKey: rec.down.ctrlKey,
1076
- metaKey: rec.down.metaKey,
1077
- shiftKey: rec.down.shiftKey
1078
- };
1079
- dispatch(ev);
1080
- routeRelease(pointerId);
1081
- };
1082
- const onPressActivate = () => {
1083
- registryRef.current.activate();
1084
- };
1085
- const keyCapture = keyboard === "first";
1086
- if (keyboard) {
1087
- window.addEventListener("keydown", onKeyDown, keyCapture);
1088
- window.addEventListener("keyup", onKeyUp, keyCapture);
1089
- }
1090
- window.addEventListener("blur", onWindowBlur);
1091
- canvas?.addEventListener("pointerdown", onPressActivate);
1092
- window.addEventListener("keydown", scheduleHoverCursorRefresh);
1093
- window.addEventListener("keyup", scheduleHoverCursorRefresh);
1094
- if (pointerChannel) {
1095
- canvas?.addEventListener("pointerleave", onHoverPointerLeave);
1096
- canvas?.addEventListener("pointerdown", onPointerDown);
1097
- canvas?.addEventListener("pointermove", onHoverMove);
1098
- canvas?.addEventListener("dblclick", onNativeDoubleClick);
1099
- }
1100
- if (wheelChannel) canvas?.addEventListener("wheel", onWheel, { passive: false, capture: true });
1101
- if (pinchChannel) {
1102
- canvas?.addEventListener("gesturestart", onGestureStart);
1103
- canvas?.addEventListener("gesturechange", onGestureChange);
1104
- canvas?.addEventListener("gestureend", onGestureEnd);
1105
- }
1106
- if (contextMenuChannel) canvas?.addEventListener("contextmenu", onContextMenu);
1107
- if (ingestChannel) {
1108
- canvas?.addEventListener("dragenter", onDragOver);
1109
- canvas?.addEventListener("dragover", onDragOver);
1110
- canvas?.addEventListener("dragleave", onDragLeave);
1111
- canvas?.addEventListener("drop", onDrop);
1112
- window.addEventListener("paste", onPaste);
1113
- }
1114
- return () => {
1115
- if (keyboard) {
1116
- window.removeEventListener("keydown", onKeyDown, keyCapture);
1117
- window.removeEventListener("keyup", onKeyUp, keyCapture);
1118
- }
1119
- window.removeEventListener("blur", onWindowBlur);
1120
- canvas?.removeEventListener("pointerdown", onPressActivate);
1121
- window.removeEventListener("keydown", scheduleHoverCursorRefresh);
1122
- window.removeEventListener("keyup", scheduleHoverCursorRefresh);
1123
- canvas?.removeEventListener("pointerleave", onHoverPointerLeave);
1124
- canvas?.removeEventListener("wheel", onWheel, { capture: true });
1125
- canvas?.removeEventListener("gesturestart", onGestureStart);
1126
- canvas?.removeEventListener("gesturechange", onGestureChange);
1127
- canvas?.removeEventListener("gestureend", onGestureEnd);
1128
- canvas?.removeEventListener("pointerdown", onPointerDown);
1129
- canvas?.removeEventListener("pointermove", onHoverMove);
1130
- canvas?.removeEventListener("dblclick", onNativeDoubleClick);
1131
- canvas?.removeEventListener("contextmenu", onContextMenu);
1132
- canvas?.removeEventListener("dragenter", onDragOver);
1133
- canvas?.removeEventListener("dragover", onDragOver);
1134
- canvas?.removeEventListener("dragleave", onDragLeave);
1135
- canvas?.removeEventListener("drop", onDrop);
1136
- window.removeEventListener("paste", onPaste);
1137
- canvas?.classList.remove(DROPOVER_CLASS);
1138
- registryRef.current.dropKeys();
1139
- clearHoverCursor();
1140
- cancelAllLongPress();
1141
- disposed = true;
1142
- for (const p of held.values()) p.session.cancel();
1143
- held.clear();
1144
- for (const d of allDispatchers()) d.cancelAll("cancel");
1145
- };
945
+ paintedCursor: () => cursorRef.current.painted?.(),
946
+ cursorSink: () => cursorRef.current.sink?.(),
947
+ longPress: () => longPressRef.current,
948
+ onClick: () => onClickRef.current,
949
+ onDoubleClick: () => onDoubleClickRef.current,
950
+ clicks: clicksRef.current
951
+ }, {
952
+ keyboard,
953
+ pointer: pointerChannel,
954
+ wheel: wheelChannel,
955
+ pinch: pinchChannel,
956
+ contextMenu: contextMenuChannel,
957
+ ingest: ingestChannel
958
+ });
1146
959
  }, [
1147
960
  enabled,
1148
961
  keyboard,
@@ -1157,7 +970,7 @@ function useGestureDispatcher(opts) {
1157
970
  longPressRef,
1158
971
  onClickRef,
1159
972
  onDoubleClickRef,
1160
- paintedCursorRef,
973
+ cursorRef,
1161
974
  registryRef,
1162
975
  requestRedrawRef,
1163
976
  rootTargetRef,
@@ -1186,6 +999,7 @@ var ActionsNode = class extends ScopeNode {
1186
999
  contextDeps;
1187
1000
  actions = /* @__PURE__ */ new Map();
1188
1001
  muted = /* @__PURE__ */ new Map();
1002
+ tunings = /* @__PURE__ */ new Map();
1189
1003
  dispatchers = [];
1190
1004
  cache = null;
1191
1005
  /** Called on `activate`, so a scope's dep and tool nodes move with it. */
@@ -1194,10 +1008,19 @@ var ActionsNode = class extends ScopeNode {
1194
1008
  for (const n of this.chain()) {
1195
1009
  if (n.muted.has(id)) return void 0;
1196
1010
  const a = n.actions.get(id)?.at(-1);
1197
- if (a) return a;
1011
+ if (a) return this.tuned(a);
1198
1012
  }
1199
1013
  return void 0;
1200
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
+ }
1201
1024
  snapshot() {
1202
1025
  const leaf = this.start();
1203
1026
  const cached = this.cache;
@@ -1274,24 +1097,35 @@ var ActionsNode = class extends ScopeNode {
1274
1097
  this.changed();
1275
1098
  };
1276
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
+ },
1277
1114
  list: () => this.snapshot(),
1278
1115
  trigger: (id, params) => {
1279
1116
  const leaf = this.start();
1280
1117
  const a = leaf.resolve(id);
1281
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;
1282
1121
  try {
1283
- a.invoker.run(depsFor(a, leaf.deps()), params);
1122
+ a.invoker.run(deps, params);
1284
1123
  } catch (err) {
1285
1124
  console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
1286
1125
  }
1287
1126
  return true;
1288
1127
  },
1289
- subscribe: (listener) => {
1290
- this.tree.listeners.add(listener);
1291
- return () => {
1292
- this.tree.listeners.delete(listener);
1293
- };
1294
- },
1128
+ subscribe: (listener) => this.tree.listeners.subscribe(listener),
1295
1129
  setDispatcher: (d) => pushOwner(this.dispatchers, d),
1296
1130
  begin: (id, params) => {
1297
1131
  const leaf = this.start();
@@ -1404,9 +1238,112 @@ function useOngoingAction(actionId) {
1404
1238
  }
1405
1239
  };
1406
1240
  }, [regRef, idRef]);
1407
- useEffect(() => () => edit.commit(), [edit, actionId]);
1241
+ const leaving = useRef(null);
1242
+ useEffect(() => {
1243
+ const left = leaving.current;
1244
+ leaving.current = null;
1245
+ if (left !== null && left !== actionId) edit.commit();
1246
+ return () => {
1247
+ leaving.current = actionId;
1248
+ queueMicrotask(() => {
1249
+ if (leaving.current !== actionId) return;
1250
+ leaving.current = null;
1251
+ edit.commit();
1252
+ });
1253
+ };
1254
+ }, [edit, actionId]);
1408
1255
  return edit;
1409
1256
  }
1257
+ function samePos(a, b) {
1258
+ if (a === null || b === null) return a === b;
1259
+ return a.worldX === b.worldX && a.worldY === b.worldY && a.viewId === b.viewId;
1260
+ }
1261
+ function createPointerStore() {
1262
+ let current = null;
1263
+ let version = 0;
1264
+ const changed = createEmitter();
1265
+ const get = () => current;
1266
+ return {
1267
+ get,
1268
+ set(next) {
1269
+ if (samePos(current, next)) return;
1270
+ current = next;
1271
+ version++;
1272
+ changed.emit();
1273
+ },
1274
+ subscribe: changed.subscribe,
1275
+ getVersion: () => version,
1276
+ getDropPoint: get
1277
+ };
1278
+ }
1279
+ var PointerContext = createContext(null);
1280
+ function PointerContextProvider({ children, store }) {
1281
+ const [own] = useState(createPointerStore);
1282
+ return /* @__PURE__ */ jsx(PointerContext.Provider, { value: store ?? own, children });
1283
+ }
1284
+ function usePointerContext() {
1285
+ return useContext(PointerContext);
1286
+ }
1287
+ var noSubscribe = () => () => {
1288
+ };
1289
+ var noPointer = () => null;
1290
+ function usePointerPosition() {
1291
+ const store = usePointerContext();
1292
+ return useSyncExternalStore(
1293
+ store ? store.subscribe : noSubscribe,
1294
+ store ? store.get : noPointer,
1295
+ store ? store.get : noPointer
1296
+ );
1297
+ }
1298
+ var SelectionContext = createContext(null);
1299
+ function SelectionContextProvider({ children }) {
1300
+ const [state, setState] = useState({ selection: [], kinds: void 0 });
1301
+ const lastSerializedRef = useRef("");
1302
+ const publishSelection = useCallback((ids, kinds) => {
1303
+ const next = JSON.stringify([ids, kinds]);
1304
+ if (next === lastSerializedRef.current) return;
1305
+ lastSerializedRef.current = next;
1306
+ setState({ selection: ids, kinds });
1307
+ }, []);
1308
+ const value = useMemo(
1309
+ () => ({
1310
+ selection: state.selection,
1311
+ ...state.kinds !== void 0 ? { kinds: state.kinds } : {},
1312
+ publishSelection
1313
+ }),
1314
+ [state, publishSelection]
1315
+ );
1316
+ return /* @__PURE__ */ jsx(SelectionContext.Provider, { value, children });
1317
+ }
1318
+ function useSelectionContext() {
1319
+ return useContext(SelectionContext);
1320
+ }
1321
+ function SelectionContextProviderIfRoot({
1322
+ children
1323
+ }) {
1324
+ const parent = useContext(SelectionContext);
1325
+ if (parent !== null) return /* @__PURE__ */ jsx(Fragment, { children });
1326
+ return /* @__PURE__ */ jsx(SelectionContextProvider, { children });
1327
+ }
1328
+ function usePublishSelection(ids, kinds) {
1329
+ const ctx = useContext(SelectionContext);
1330
+ const idsRef = useLatest(ids);
1331
+ const kindsRef = useLatest(kinds);
1332
+ const serialized = JSON.stringify([ids, kinds]);
1333
+ const publish = ctx?.publishSelection;
1334
+ useEffect(() => {
1335
+ if (!publish) return;
1336
+ publish(idsRef.current, kindsRef.current);
1337
+ }, [publish, serialized, idsRef, kindsRef]);
1338
+ const ctxRef = useLatest(ctx);
1339
+ const serializedRef = useLatest(serialized);
1340
+ useEffect(() => () => {
1341
+ const slot = ctxRef.current;
1342
+ if (slot && JSON.stringify([slot.selection, slot.kinds]) === serializedRef.current) {
1343
+ slot.publishSelection([]);
1344
+ }
1345
+ }, [ctxRef, serializedRef]);
1346
+ }
1410
1347
  var NO_MODES = () => () => {
1411
1348
  };
1412
1349
  function useContributions(opts) {
@@ -1596,6 +1533,6 @@ function useTools(opts) {
1596
1533
  );
1597
1534
  }
1598
1535
 
1599
- export { ActionsProvider, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, DepRegistryProvider, InputScope, Yoke, useAction, useActionsRegistry, useActiveToolContext, useContributions, useDepRegistry, useDepSource, useGestureDispatcher, useOffhandAction, useOngoingAction, useOptionalActiveToolContext, useOptionalDepRegistry, useTools, useYoke };
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 };
1600
1537
  //# sourceMappingURL=react.js.map
1601
1538
  //# sourceMappingURL=react.js.map