@weasel-js/routing 1.9.4 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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, DEV, publishLiveDispatch, LONG_PRESS_HAPTIC_MS, multiTouchGestureId, openPointerSession, pushOwner, evaluateEnabled, validateActionId, validateActionDefaultBinding, depNodeOf, DepNode, clientToCanvas, buildDepsFromRequires, isEditableTarget, focusedControlOwnsKey, itemsFromClipboardData, itemsFromDataTransfer, itemTypesFromDataTransfer } from './chunk-4DR6AJMN.js';
2
2
  import { createContext, useState, useContext, useEffect, useRef, useCallback, useMemo, useSyncExternalStore } from 'react';
3
+ import { isApplePlatform, 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 !== 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,171 @@ 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 hoverOf = (e) => ({
272
+ clientX: e.clientX,
273
+ clientY: e.clientY,
274
+ altKey: e.altKey,
275
+ ctrlKey: e.ctrlKey,
276
+ metaKey: e.metaKey,
277
+ shiftKey: e.shiftKey
278
+ });
279
+ function hoverCursorInput(core, paintedCursor, pointersHeld, cursorSink = () => void 0) {
280
+ const { canvas, routeAt, toWorld, target, dispatch, dispatcherNow, ctxNow } = core;
281
+ const ownSink = createCursorSink({ element: () => canvas, painted: paintedCursor });
282
+ const override = {
283
+ apply: (spec) => (cursorSink() ?? ownSink).setOverride(spec === null ? null : resolveCursorTier(spec)),
284
+ clear: () => (cursorSink() ?? ownSink).setOverride(null)
285
+ };
286
+ let last = null;
287
+ let facts = null;
288
+ const resolve = () => {
289
+ if (!canvas || !last) return;
290
+ if (pointersHeld() > 0 || dispatcherNow().inFlight().size > 0) {
291
+ override.apply(dispatcherNow().inFlightCursor());
292
+ return;
293
+ }
294
+ const h = last;
295
+ routeAt(null, h.clientX, h.clientY);
296
+ const w = toWorld(h.clientX, h.clientY);
297
+ const where = facts?.at === h ? facts.where : underPointer(target(), h, w);
298
+ if (where.affordance?.cursor) {
299
+ override.apply(where.affordance.cursor);
300
+ return;
301
+ }
302
+ const ctx = ctxNow();
303
+ const d = dispatcherNow();
304
+ const press = { kind: "pointerdown", x: w.x, y: w.y, clientX: h.clientX, clientY: h.clientY, ...where };
305
+ if (DEV) publishLiveDispatch(() => d.explain(press, ctx));
306
+ const click = { kind: "click", x: w.x, y: w.y, pressX: w.x, pressY: w.y, ...where };
307
+ const cursor = d.resolveOnly(press, ctx)?.action.cursor ?? dispatcherNow().resolveOnly(click, ctx)?.action.cursor;
308
+ override.apply(cursor ?? null);
309
+ };
310
+ const refresh = () => {
311
+ resolve();
312
+ const painted = paintedCursor();
313
+ if (!painted?.active() || !canvas || !last) return;
314
+ const local = core.toCanvasLocal(last.clientX, last.clientY);
315
+ painted.setPointer(local.x, local.y);
316
+ };
317
+ const refreshSoon = () => {
318
+ setTimeout(() => {
319
+ if (!core.disposed()) refresh();
320
+ }, 0);
321
+ };
322
+ return {
323
+ moved(e) {
324
+ last = hoverOf(e);
325
+ if (pointersHeld() === 0) {
326
+ const w = toWorld(e.clientX, e.clientY);
327
+ const where = underPointer(target(), last, w);
328
+ facts = { at: last, where };
329
+ dispatch({ kind: "hover", x: w.x, y: w.y, clientX: e.clientX, clientY: e.clientY, ...where });
330
+ }
331
+ refresh();
332
+ },
333
+ released(e) {
334
+ last = hoverOf(e);
335
+ refreshSoon();
336
+ },
337
+ onLeave() {
338
+ last = null;
339
+ if (DEV) publishLiveDispatch(null);
340
+ paintedCursor()?.clearPointer();
341
+ override.clear();
342
+ },
343
+ onKey(e) {
344
+ if (!last) return;
345
+ last = { ...last, altKey: e.altKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey };
346
+ refreshSoon();
347
+ },
348
+ clear: override.clear
349
+ };
350
+ }
159
351
 
160
352
  // src/interactions/dispatcher/channels/ingest.ts
161
353
  var DROPOVER_CLASS = "weasel-dropover";
162
354
  function ingestInput(core) {
163
355
  const { canvas, routeAt, toWorld, dispatch } = core;
164
356
  const onDragOver = (e) => {
165
- if (!e.dataTransfer) return;
357
+ const dt = e.dataTransfer;
358
+ if (!dt) return;
359
+ routeAt(null, e.clientX, e.clientY);
360
+ const w = toWorld(e.clientX, e.clientY);
361
+ const wanted = core.dispatcherNow().resolveOnly({
362
+ kind: "drop",
363
+ items: itemTypesFromDataTransfer(dt),
364
+ x: w.x,
365
+ y: w.y,
366
+ clientX: e.clientX,
367
+ clientY: e.clientY,
368
+ altKey: e.altKey,
369
+ ctrlKey: e.ctrlKey,
370
+ metaKey: e.metaKey,
371
+ shiftKey: e.shiftKey
372
+ }, core.ctxNow()) !== null;
373
+ canvas?.classList.toggle(DROPOVER_CLASS, wanted);
374
+ if (!wanted) return;
166
375
  e.preventDefault();
167
- e.dataTransfer.dropEffect = "copy";
168
- canvas?.classList.add(DROPOVER_CLASS);
376
+ dt.dropEffect = "copy";
169
377
  };
170
378
  const onDragLeave = () => {
171
379
  canvas?.classList.remove(DROPOVER_CLASS);
@@ -226,24 +434,32 @@ function ingestInput(core) {
226
434
  }
227
435
 
228
436
  // src/interactions/dispatcher/channels/keyboard.ts
437
+ function typedWithAltGraph(e) {
438
+ if (!e.ctrlKey || !e.altKey || e.key.length !== 1 || !e.getModifierState?.("AltGraph")) return false;
439
+ const base = /^(?:Key|Digit)(.)$/.exec(e.code)?.[1];
440
+ return base === void 0 || base.toLowerCase() !== e.key.toLowerCase();
441
+ }
229
442
  function keyboardInput(core) {
230
443
  const { dispatch, dispatcherNow } = core;
231
444
  const heldKeys = /* @__PURE__ */ new Map();
232
445
  const codeOf = (e) => e.code ? { code: e.code } : {};
233
446
  const physicalKey = (e) => e.code || e.key.toLowerCase();
234
447
  const onKeyDown = (e) => {
235
- if (e.defaultPrevented || isEditableTarget(e.target) || activatesFocusedControl(e)) return;
448
+ if (e.defaultPrevented || isEditableTarget(e.target) || focusedControlOwnsKey(e)) return;
236
449
  if (!core.holdsKeys()) return;
237
450
  if (e.repeat && heldKeys.has(physicalKey(e))) {
238
451
  e.preventDefault();
239
452
  return;
240
453
  }
454
+ if (press(e, { altKey: e.altKey, ctrlKey: e.ctrlKey })) return;
455
+ if (typedWithAltGraph(e)) press(e, { altKey: false, ctrlKey: false });
456
+ };
457
+ const press = (e, mods) => {
241
458
  const keyEv = {
242
459
  kind: "key",
243
460
  key: e.key,
244
461
  ...codeOf(e),
245
- altKey: e.altKey,
246
- ctrlKey: e.ctrlKey,
462
+ ...mods,
247
463
  metaKey: e.metaKey,
248
464
  shiftKey: e.shiftKey,
249
465
  repeat: e.repeat
@@ -254,8 +470,7 @@ function keyboardInput(core) {
254
470
  key: e.key,
255
471
  ...codeOf(e),
256
472
  phase: "down",
257
- altKey: e.altKey,
258
- ctrlKey: e.ctrlKey,
473
+ ...mods,
259
474
  metaKey: e.metaKey,
260
475
  shiftKey: e.shiftKey
261
476
  };
@@ -264,9 +479,9 @@ function keyboardInput(core) {
264
479
  if (heldResult === "handled") {
265
480
  heldKeys.set(physicalKey(e), { key: e.key, ...codeOf(e), on: heldOn });
266
481
  }
267
- if (keyResult === "handled" || heldResult === "handled") {
268
- e.preventDefault();
269
- }
482
+ const handled = keyResult === "handled" || heldResult === "handled";
483
+ if (handled) e.preventDefault();
484
+ return handled;
270
485
  };
271
486
  const onKeyUp = (e) => {
272
487
  const physical = physicalKey(e);
@@ -305,6 +520,176 @@ function keyboardInput(core) {
305
520
  return { onKeyDown, onKeyUp, releaseAll };
306
521
  }
307
522
 
523
+ // src/interactions/dispatcher/channels/listeners.ts
524
+ function listenerSet() {
525
+ const removers = [];
526
+ return {
527
+ on(target, type, listener, options) {
528
+ if (!target) return;
529
+ target.addEventListener(type, listener, options);
530
+ removers.push(() => {
531
+ target.removeEventListener(type, listener, options);
532
+ });
533
+ },
534
+ removeAll() {
535
+ for (const remove of removers.splice(0)) remove();
536
+ }
537
+ };
538
+ }
539
+ var sampleOf = (e) => ({
540
+ pointerId: e.pointerId,
541
+ clientX: e.clientX,
542
+ clientY: e.clientY,
543
+ target: e.target,
544
+ altKey: e.altKey,
545
+ ctrlKey: e.ctrlKey,
546
+ metaKey: e.metaKey,
547
+ shiftKey: e.shiftKey,
548
+ ...stylusOf(e)
549
+ });
550
+ function holdPublisher(core, longPress) {
551
+ return (pending) => {
552
+ const store = longPress()?.state;
553
+ if (!store) return;
554
+ if (!pending) {
555
+ store.set(null);
556
+ return;
557
+ }
558
+ const { press } = pending;
559
+ const evs = longPressEvents(press);
560
+ const d = core.dispatcherNow();
561
+ store.set({
562
+ pointerId: pending.pointerId,
563
+ pointerType: pending.pointerType,
564
+ client: { x: press.clientX, y: press.clientY },
565
+ local: core.toCanvasLocal(press.clientX, press.clientY),
566
+ world: { x: press.worldX, y: press.worldY },
567
+ viewId: core.target().id,
568
+ startedAt: pending.startedAt,
569
+ duration: pending.duration,
570
+ armed: d.resolveOnly(evs.longpress, core.ctxNow()) !== null || d.resolveOnly(evs.contextmenu, core.ctxNow()) !== null
571
+ });
572
+ };
573
+ }
574
+ function holdClock(core) {
575
+ let holdView = null;
576
+ return {
577
+ clock: {
578
+ now: () => performance.now(),
579
+ setTimer: (fn, ms) => {
580
+ const view = core.routedView();
581
+ holdView = { id: view };
582
+ return setTimeout(() => {
583
+ holdView = null;
584
+ core.setRoutedView(view);
585
+ fn();
586
+ }, ms);
587
+ },
588
+ clearTimer: (timer) => {
589
+ holdView = null;
590
+ clearTimeout(timer);
591
+ }
592
+ },
593
+ routeToPress: () => {
594
+ if (holdView) core.setRoutedView(holdView.id);
595
+ }
596
+ };
597
+ }
598
+ function pointerInput(core, opts) {
599
+ const { canvas, routeAt, routeDown, routeRelease, toWorld, target } = core;
600
+ const sessions = /* @__PURE__ */ new Map();
601
+ let claimedDoubleClick = false;
602
+ const hold = holdClock(core);
603
+ const recognizer = createPointerRecognizer({
604
+ sink: {
605
+ dispatch: (ev) => core.dispatch(ev),
606
+ multiTouchInFlight: (fingers) => core.dispatcherNow().inFlight().has(multiTouchGestureId(fingers))
607
+ },
608
+ clock: hold.clock,
609
+ toWorld,
610
+ toSurface: (p) => core.toCanvasLocal(p.x, p.y),
611
+ longPressMs: () => opts.longPress()?.duration,
612
+ clicks: opts.clicks,
613
+ observer: {
614
+ click: (world) => {
615
+ opts.onClick()?.(world);
616
+ },
617
+ doubleClick: (world) => {
618
+ opts.onDoubleClick()?.(world);
619
+ },
620
+ holdChanged: holdPublisher(core, opts.longPress),
621
+ longPress: () => {
622
+ if (opts.longPress()?.haptics === false) return;
623
+ if (typeof navigator !== "undefined" && typeof navigator.vibrate === "function") {
624
+ navigator.vibrate(LONG_PRESS_HAPTIC_MS);
625
+ }
626
+ }
627
+ }
628
+ });
629
+ const onMove = (e) => {
630
+ routeAt(e.pointerId, e.clientX, e.clientY);
631
+ recognizer.move(sampleOf(e));
632
+ opts.hover.moved(e);
633
+ };
634
+ const onUp = (e) => {
635
+ routeAt(e.pointerId, e.clientX, e.clientY);
636
+ sessions.delete(e.pointerId);
637
+ const { doubleClick } = recognizer.release(sampleOf(e));
638
+ if (doubleClick) claimedDoubleClick = doubleClick === "handled" || opts.onDoubleClick() !== void 0;
639
+ routeRelease(e.pointerId);
640
+ opts.hover.released(e);
641
+ };
642
+ const onCancel = (pointerId) => {
643
+ if (core.disposed()) return;
644
+ const at = recognizer.positionOf(pointerId);
645
+ if (!at) return;
646
+ sessions.delete(pointerId);
647
+ routeAt(pointerId, at.x, at.y);
648
+ recognizer.cancel(pointerId);
649
+ routeRelease(pointerId);
650
+ };
651
+ return {
652
+ onPointerDown(e) {
653
+ claimedDoubleClick = false;
654
+ if ((e.button ?? 0) !== 0) return;
655
+ if (!canvas) return;
656
+ if (isInControlWithin(e.target, canvas)) return;
657
+ routeDown(e.pointerId, e.clientX, e.clientY);
658
+ const w = toWorld(e.clientX, e.clientY);
659
+ const where = underPointer(target(), e, w);
660
+ sessions.set(e.pointerId, openPointerSession(canvas, e, {
661
+ onMove,
662
+ onEnd: onUp,
663
+ onCancel: () => {
664
+ onCancel(e.pointerId);
665
+ }
666
+ }));
667
+ recognizer.press({ ...sampleOf(e), x: w.x, y: w.y, pointerType: e.pointerType, ...where });
668
+ },
669
+ onHoverMove(e) {
670
+ if (!sessions.has(e.pointerId)) onMove(e);
671
+ },
672
+ onNativeDoubleClick(e) {
673
+ if (!claimedDoubleClick) return;
674
+ claimedDoubleClick = false;
675
+ e.preventDefault();
676
+ },
677
+ takeNativeHold: () => {
678
+ hold.routeToPress();
679
+ return recognizer.takeHold();
680
+ },
681
+ heldCount: () => recognizer.heldCount(),
682
+ cancelHeld() {
683
+ for (const session of [...sessions.values()]) session.cancel();
684
+ },
685
+ dispose() {
686
+ recognizer.reset();
687
+ for (const session of sessions.values()) session.cancel();
688
+ sessions.clear();
689
+ }
690
+ };
691
+ }
692
+
308
693
  // src/interactions/dispatcher/channels/webkitPinch.ts
309
694
  function webkitPinchInput(core) {
310
695
  const { canvas, routeAt, toWorld, target, dispatch, dispatcherNow, ctxNow } = core;
@@ -318,27 +703,25 @@ function webkitPinchInput(core) {
318
703
  };
319
704
  const pinchEventOf = (e, scale, rotation) => {
320
705
  const g = e;
321
- const rect = canvas?.getBoundingClientRect();
706
+ const rect = typeof g.clientX === "number" && typeof g.clientY === "number" ? void 0 : canvas?.getBoundingClientRect();
322
707
  const cx = typeof g.clientX === "number" ? g.clientX : rect ? rect.left + rect.width / 2 : 0;
323
708
  const cy = typeof g.clientY === "number" ? g.clientY : rect ? rect.top + rect.height / 2 : 0;
324
709
  routeAt(null, cx, cy);
325
- const [localX, localY] = rect ? clientToCanvasRect(rect, cx, cy) : [cx, cy];
710
+ const local = core.toCanvasLocal(cx, cy);
326
711
  const hitPoint = toWorld(cx, cy);
327
- const affordance = target().affordanceAt?.(hitPoint) ?? void 0;
328
- const body = target().classifyTarget?.(hitPoint);
329
- return {
330
- kind: "pinch",
712
+ const mods = {
331
713
  altKey: g.altKey === true,
332
714
  ctrlKey: g.ctrlKey === true,
333
715
  metaKey: g.metaKey === true,
334
- shiftKey: g.shiftKey === true,
716
+ shiftKey: g.shiftKey === true
717
+ };
718
+ return {
719
+ kind: "pinch",
335
720
  scale,
336
721
  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 } : {}
722
+ clientX: local.x,
723
+ clientY: local.y,
724
+ ...underPointer(target(), mods, hitPoint)
342
725
  };
343
726
  };
344
727
  const onGestureStart = (e) => {
@@ -372,7 +755,7 @@ function webkitPinchInput(core) {
372
755
 
373
756
  // src/interactions/dispatcher/channels/wheel.ts
374
757
  function wheelInput(core, pinchLive) {
375
- const { canvas, routeAt, toWorld, target, dispatch } = core;
758
+ const { routeAt, toWorld, target, dispatch } = core;
376
759
  const onWheel = (e) => {
377
760
  core.activate();
378
761
  if (pinchLive() && e.ctrlKey) {
@@ -381,24 +764,15 @@ function wheelInput(core, pinchLive) {
381
764
  return;
382
765
  }
383
766
  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];
767
+ const local = core.toCanvasLocal(e.clientX, e.clientY);
386
768
  const hitPoint = toWorld(e.clientX, e.clientY);
387
- const affordance = target().affordanceAt?.(hitPoint) ?? void 0;
388
- const body = target().classifyTarget?.(hitPoint);
389
769
  const ev = {
390
770
  kind: "wheel",
391
- altKey: e.altKey,
392
- ctrlKey: e.ctrlKey,
393
- metaKey: e.metaKey,
394
- shiftKey: e.shiftKey,
395
771
  deltaX: e.deltaX,
396
772
  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 } : {}
773
+ clientX: local.x,
774
+ clientY: local.y,
775
+ ...underPointer(target(), e, hitPoint)
402
776
  };
403
777
  const result = dispatch(ev);
404
778
  if (result === "handled") {
@@ -410,33 +784,8 @@ function wheelInput(core, pinchLive) {
410
784
  }
411
785
 
412
786
  // 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 } : {}
418
- };
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 };
425
- }
426
- let sumX = 0;
427
- let sumY = 0;
428
- for (const p of pts) {
429
- sumX += p.x;
430
- sumY += p.y;
431
- }
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 };
437
- }
438
787
  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;
788
+ 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
789
  const pointerChannel = channels?.pointer !== false;
441
790
  const wheelChannel = channels?.wheel !== false;
442
791
  const pinchChannel = channels?.pinch !== false;
@@ -446,6 +795,7 @@ function useGestureDispatcher(opts) {
446
795
  const onDoubleClickRef = useLatest(onDoubleClick);
447
796
  const activeTool = useActiveToolContext();
448
797
  const depRegistry = useDepRegistry();
798
+ const [isMac] = useState(isApplePlatform);
449
799
  const dispatcherRef = useRef(null);
450
800
  if (!dispatcherRef.current) {
451
801
  dispatcherRef.current = dispatcherOpt ?? createDispatcher();
@@ -457,7 +807,7 @@ function useGestureDispatcher(opts) {
457
807
  activeToolId: activeTool.active,
458
808
  hotkeyStack: activeTool.hotkeyStack,
459
809
  entriesById,
460
- isMac: IS_MAC
810
+ isMac
461
811
  });
462
812
  const rootTargetRef = useLatest({
463
813
  id: null,
@@ -473,674 +823,100 @@ function useGestureDispatcher(opts) {
473
823
  ...(viewsRef.current?.targets() ?? []).map((t) => t.dispatcher)
474
824
  ], [rootTargetRef, viewsRef]);
475
825
  const requestRedrawRef = useLatest(requestRedraw);
476
- const paintedCursorRef = useLatest(paintedCursor);
826
+ const cursorRef = useLatest({ painted: paintedCursor, sink: cursorSink });
477
827
  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) {
828
+ const clicksRef = useRef({ last: null });
829
+ useToolLifecycle({
830
+ entriesById,
831
+ active: activeTool.active,
832
+ hotkeyStack: activeTool.hotkeyStack,
833
+ // The incoming tool must not inherit a live gesture.
834
+ onSwitch: () => {
509
835
  for (const d of allDispatchers()) d.cancelAll("cancel");
510
836
  }
511
- prevActiveRef.current = activeTool.active;
512
- for (const id of next) if (!prev.has(id)) fireLifecycle(id, "onActivate");
513
837
  });
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
838
  useEffect(() => {
520
839
  if (enabled && claimKeys) registryRef.current.activate();
521
840
  }, [enabled, claimKeys, registryRef]);
522
841
  useEffect(() => {
523
842
  if (!enabled) return;
524
843
  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
844
  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 = {
845
+ const redraw = dispatcherWatch(() => {
846
+ requestRedrawRef.current?.();
847
+ });
848
+ redraw.sync(allDispatchers());
849
+ const core = createDispatchCore({
562
850
  canvas,
563
- target,
564
- routeAt,
565
- routedView: () => routedId,
566
- setRoutedView: (id) => {
567
- routedId = id;
851
+ root: () => rootTargetRef.current,
852
+ views: () => viewsRef.current,
853
+ context: () => ctxRef.current,
854
+ actions: () => registryRef.current,
855
+ uses: () => {
856
+ redraw.sync(allDispatchers());
568
857
  },
569
- dispatcherNow,
570
- ctxNow,
571
- toWorld,
572
- dispatch,
573
- holdsKeys: () => canvas ? registryRef.current.holdsKeys() : registryRef.current.isActive(),
574
- activate: () => registryRef.current.activate(),
575
858
  disposed: () => disposed
576
- };
859
+ });
577
860
  const keys = keyboardInput(core);
578
- const { onKeyDown, onKeyUp } = keys;
579
861
  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;
862
+ const ingest = ingestInput(core);
863
+ const hover = hoverCursorInput(core, () => cursorRef.current.painted?.(), () => pointer.heldCount(), () => cursorRef.current.sink?.());
864
+ const pointer = pointerInput(core, {
865
+ longPress: () => longPressRef.current,
866
+ onClick: () => onClickRef.current,
867
+ onDoubleClick: () => onDoubleClickRef.current,
868
+ clicks: clicksRef.current,
869
+ hover
870
+ });
655
871
  const onWindowBlur = () => {
656
872
  pinch.end();
657
- for (const p of [...held.values()]) p.session.cancel();
873
+ pointer.cancelHeld();
658
874
  keys.releaseAll();
659
875
  };
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
- };
876
+ const listeners = listenerSet();
1085
877
  const keyCapture = keyboard === "first";
1086
878
  if (keyboard) {
1087
- window.addEventListener("keydown", onKeyDown, keyCapture);
1088
- window.addEventListener("keyup", onKeyUp, keyCapture);
879
+ listeners.on(window, "keydown", keys.onKeyDown, keyCapture);
880
+ listeners.on(window, "keyup", keys.onKeyUp, keyCapture);
1089
881
  }
1090
- window.addEventListener("blur", onWindowBlur);
1091
- canvas?.addEventListener("pointerdown", onPressActivate);
1092
- window.addEventListener("keydown", scheduleHoverCursorRefresh);
1093
- window.addEventListener("keyup", scheduleHoverCursorRefresh);
882
+ listeners.on(window, "blur", onWindowBlur);
883
+ listeners.on(canvas, "pointerdown", () => {
884
+ registryRef.current.activate();
885
+ });
1094
886
  if (pointerChannel) {
1095
- canvas?.addEventListener("pointerleave", onHoverPointerLeave);
1096
- canvas?.addEventListener("pointerdown", onPointerDown);
1097
- canvas?.addEventListener("pointermove", onHoverMove);
1098
- canvas?.addEventListener("dblclick", onNativeDoubleClick);
887
+ listeners.on(window, "keydown", hover.onKey);
888
+ listeners.on(window, "keyup", hover.onKey);
889
+ listeners.on(canvas, "pointerleave", hover.onLeave);
890
+ listeners.on(canvas, "pointerdown", pointer.onPointerDown);
891
+ listeners.on(canvas, "pointermove", pointer.onHoverMove);
892
+ listeners.on(canvas, "dblclick", pointer.onNativeDoubleClick);
893
+ }
894
+ if (wheelChannel) {
895
+ listeners.on(canvas, "wheel", wheelInput(core, pinch.live), { passive: false, capture: true });
1099
896
  }
1100
- if (wheelChannel) canvas?.addEventListener("wheel", onWheel, { passive: false, capture: true });
1101
897
  if (pinchChannel) {
1102
- canvas?.addEventListener("gesturestart", onGestureStart);
1103
- canvas?.addEventListener("gesturechange", onGestureChange);
1104
- canvas?.addEventListener("gestureend", onGestureEnd);
898
+ listeners.on(canvas, "gesturestart", pinch.onGestureStart);
899
+ listeners.on(canvas, "gesturechange", pinch.onGestureChange);
900
+ listeners.on(canvas, "gestureend", pinch.onGestureEnd);
901
+ }
902
+ if (contextMenuChannel) {
903
+ listeners.on(canvas, "contextmenu", contextMenuInput(core, pointer.takeNativeHold));
1105
904
  }
1106
- if (contextMenuChannel) canvas?.addEventListener("contextmenu", onContextMenu);
1107
905
  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);
906
+ listeners.on(canvas, "dragenter", ingest.onDragOver);
907
+ listeners.on(canvas, "dragover", ingest.onDragOver);
908
+ listeners.on(canvas, "dragleave", ingest.onDragLeave);
909
+ listeners.on(canvas, "drop", ingest.onDrop);
910
+ listeners.on(window, "paste", ingest.onPaste);
1113
911
  }
1114
912
  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);
913
+ listeners.removeAll();
1137
914
  canvas?.classList.remove(DROPOVER_CLASS);
1138
915
  registryRef.current.dropKeys();
1139
- clearHoverCursor();
1140
- cancelAllLongPress();
916
+ hover.clear();
1141
917
  disposed = true;
1142
- for (const p of held.values()) p.session.cancel();
1143
- held.clear();
918
+ pointer.dispose();
919
+ redraw.stop();
1144
920
  for (const d of allDispatchers()) d.cancelAll("cancel");
1145
921
  };
1146
922
  }, [
@@ -1157,7 +933,7 @@ function useGestureDispatcher(opts) {
1157
933
  longPressRef,
1158
934
  onClickRef,
1159
935
  onDoubleClickRef,
1160
- paintedCursorRef,
936
+ cursorRef,
1161
937
  registryRef,
1162
938
  requestRedrawRef,
1163
939
  rootTargetRef,
@@ -1279,19 +1055,16 @@ var ActionsNode = class extends ScopeNode {
1279
1055
  const leaf = this.start();
1280
1056
  const a = leaf.resolve(id);
1281
1057
  if (!a?.invoker || a.invoker.timing !== "immediate") return false;
1058
+ const deps = depsFor(a, leaf.deps());
1059
+ if (!evaluateEnabled(a, deps).enabled) return false;
1282
1060
  try {
1283
- a.invoker.run(depsFor(a, leaf.deps()), params);
1061
+ a.invoker.run(deps, params);
1284
1062
  } catch (err) {
1285
1063
  console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
1286
1064
  }
1287
1065
  return true;
1288
1066
  },
1289
- subscribe: (listener) => {
1290
- this.tree.listeners.add(listener);
1291
- return () => {
1292
- this.tree.listeners.delete(listener);
1293
- };
1294
- },
1067
+ subscribe: (listener) => this.tree.listeners.subscribe(listener),
1295
1068
  setDispatcher: (d) => pushOwner(this.dispatchers, d),
1296
1069
  begin: (id, params) => {
1297
1070
  const leaf = this.start();
@@ -1407,6 +1180,96 @@ function useOngoingAction(actionId) {
1407
1180
  useEffect(() => () => edit.commit(), [edit, actionId]);
1408
1181
  return edit;
1409
1182
  }
1183
+ function samePos(a, b) {
1184
+ if (a === null || b === null) return a === b;
1185
+ return a.worldX === b.worldX && a.worldY === b.worldY && a.viewId === b.viewId;
1186
+ }
1187
+ function createPointerStore() {
1188
+ let current = null;
1189
+ let version = 0;
1190
+ const changed = createEmitter();
1191
+ const get = () => current;
1192
+ return {
1193
+ get,
1194
+ set(next) {
1195
+ if (samePos(current, next)) return;
1196
+ current = next;
1197
+ version++;
1198
+ changed.emit();
1199
+ },
1200
+ subscribe: changed.subscribe,
1201
+ getVersion: () => version,
1202
+ getDropPoint: get
1203
+ };
1204
+ }
1205
+ var PointerContext = createContext(null);
1206
+ function PointerContextProvider({ children, store }) {
1207
+ const [own] = useState(createPointerStore);
1208
+ return /* @__PURE__ */ jsx(PointerContext.Provider, { value: store ?? own, children });
1209
+ }
1210
+ function usePointerContext() {
1211
+ return useContext(PointerContext);
1212
+ }
1213
+ var noSubscribe = () => () => {
1214
+ };
1215
+ var noPointer = () => null;
1216
+ function usePointerPosition() {
1217
+ const store = usePointerContext();
1218
+ return useSyncExternalStore(
1219
+ store ? store.subscribe : noSubscribe,
1220
+ store ? store.get : noPointer,
1221
+ store ? store.get : noPointer
1222
+ );
1223
+ }
1224
+ var SelectionContext = createContext(null);
1225
+ function SelectionContextProvider({ children }) {
1226
+ const [state, setState] = useState({ selection: [], kinds: void 0 });
1227
+ const lastSerializedRef = useRef("");
1228
+ const publishSelection = useCallback((ids, kinds) => {
1229
+ const next = JSON.stringify([ids, kinds]);
1230
+ if (next === lastSerializedRef.current) return;
1231
+ lastSerializedRef.current = next;
1232
+ setState({ selection: ids, kinds });
1233
+ }, []);
1234
+ const value = useMemo(
1235
+ () => ({
1236
+ selection: state.selection,
1237
+ ...state.kinds !== void 0 ? { kinds: state.kinds } : {},
1238
+ publishSelection
1239
+ }),
1240
+ [state, publishSelection]
1241
+ );
1242
+ return /* @__PURE__ */ jsx(SelectionContext.Provider, { value, children });
1243
+ }
1244
+ function useSelectionContext() {
1245
+ return useContext(SelectionContext);
1246
+ }
1247
+ function SelectionContextProviderIfRoot({
1248
+ children
1249
+ }) {
1250
+ const parent = useContext(SelectionContext);
1251
+ if (parent !== null) return /* @__PURE__ */ jsx(Fragment, { children });
1252
+ return /* @__PURE__ */ jsx(SelectionContextProvider, { children });
1253
+ }
1254
+ function usePublishSelection(ids, kinds) {
1255
+ const ctx = useContext(SelectionContext);
1256
+ const idsRef = useLatest(ids);
1257
+ const kindsRef = useLatest(kinds);
1258
+ const serialized = JSON.stringify([ids, kinds]);
1259
+ const publish = ctx?.publishSelection;
1260
+ useEffect(() => {
1261
+ if (!publish) return;
1262
+ publish(idsRef.current, kindsRef.current);
1263
+ }, [publish, serialized, idsRef, kindsRef]);
1264
+ const ctxRef = useLatest(ctx);
1265
+ const serializedRef = useLatest(serialized);
1266
+ useEffect(() => () => {
1267
+ const slot = ctxRef.current;
1268
+ if (slot && JSON.stringify([slot.selection, slot.kinds]) === serializedRef.current) {
1269
+ slot.publishSelection([]);
1270
+ }
1271
+ }, [ctxRef, serializedRef]);
1272
+ }
1410
1273
  var NO_MODES = () => () => {
1411
1274
  };
1412
1275
  function useContributions(opts) {
@@ -1596,6 +1459,6 @@ function useTools(opts) {
1596
1459
  );
1597
1460
  }
1598
1461
 
1599
- export { ActionsProvider, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, DepRegistryProvider, InputScope, Yoke, useAction, useActionsRegistry, useActiveToolContext, useContributions, useDepRegistry, useDepSource, useGestureDispatcher, useOffhandAction, useOngoingAction, useOptionalActiveToolContext, useOptionalDepRegistry, useTools, useYoke };
1462
+ 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
1463
  //# sourceMappingURL=react.js.map
1601
1464
  //# sourceMappingURL=react.js.map