@weasel-js/routing 1.9.3 → 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, scopeBindings, liveScope, isDev, reportRouteConflicts, offhandKeyFor, makeToolOffhandAction, buildToolOffhandBindings, dlog, isEditableTarget, activatesFocusedControl, clientToCanvasRect, openPointerSession, LONG_PRESS_MS, publishLiveDispatch, pointerGestureId, itemsFromDataTransfer, itemsFromClipboardData, ScopeNode, pushOwner, validateActionId, validateActionDefaultBinding, depNodeOf, DepNode, buildDepsFromRequires } from './chunk-LOP2JTTA.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 { 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,19 +111,93 @@ function useDepSource(name, source) {
115
111
  return registry.register(name, () => sourceRef.current());
116
112
  }, [name, registry, sourceRef]);
117
113
  }
118
- var DROPOVER_CLASS = "weasel-dropover";
119
- function stylusOf(e) {
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
+ }
146
+
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
+ });
120
164
  return {
121
- ...typeof e.pressure === "number" ? { pressure: e.pressure } : {},
122
- ...typeof e.tiltX === "number" ? { tiltX: e.tiltX } : {},
123
- ...typeof e.tiltY === "number" ? { tiltY: e.tiltY } : {}
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
124
199
  };
125
200
  }
126
- var IS_MAC = typeof navigator !== "undefined" && /mac/i.test(
127
- // `||`, not `??`: jsdom (and some embedders) report an empty-string
128
- // platform, which is not nullish and would swallow the UA fallback.
129
- navigator.platform || navigator.userAgent
130
- );
131
201
  function withViewDeps(base, overlay) {
132
202
  return {
133
203
  register: (name, source) => base().register(name, source),
@@ -138,25 +208,584 @@ function withViewDeps(base, overlay) {
138
208
  }
139
209
  };
140
210
  }
141
- function computeMultiTouchGeometry(positions) {
142
- const pts = [...positions];
143
- if (pts.length < 2) {
144
- return { centroid: { x: 0, y: 0 }, spread: 0 };
145
- }
146
- let sumX = 0;
147
- let sumY = 0;
148
- for (const p of pts) {
149
- sumX += p.x;
150
- sumY += p.y;
151
- }
152
- const centroid = { x: sumX / pts.length, y: sumY / pts.length };
153
- const dx = pts[1].x - pts[0].x;
154
- const dy = pts[1].y - pts[0].y;
155
- const spread = Math.sqrt(dx * dx + dy * dy);
156
- return { centroid, spread };
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
+
250
+ // src/interactions/dispatcher/channels/contextMenu.ts
251
+ function contextMenuInput(core, takeNativeHold) {
252
+ const { routeAt, toWorld, target, dispatch } = core;
253
+ const onContextMenu = (e) => {
254
+ e.preventDefault();
255
+ if (takeNativeHold()) return;
256
+ routeAt(null, e.clientX, e.clientY);
257
+ const w = toWorld(e.clientX, e.clientY);
258
+ const ev = {
259
+ kind: "contextmenu",
260
+ target: e.target,
261
+ x: w.x,
262
+ y: w.y,
263
+ clientX: e.clientX,
264
+ clientY: e.clientY,
265
+ ...underPointer(target(), e, w)
266
+ };
267
+ dispatch(ev);
268
+ };
269
+ return onContextMenu;
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
+ }
351
+
352
+ // src/interactions/dispatcher/channels/ingest.ts
353
+ var DROPOVER_CLASS = "weasel-dropover";
354
+ function ingestInput(core) {
355
+ const { canvas, routeAt, toWorld, dispatch } = core;
356
+ const onDragOver = (e) => {
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;
375
+ e.preventDefault();
376
+ dt.dropEffect = "copy";
377
+ };
378
+ const onDragLeave = () => {
379
+ canvas?.classList.remove(DROPOVER_CLASS);
380
+ };
381
+ const onDrop = (e) => {
382
+ canvas?.classList.remove(DROPOVER_CLASS);
383
+ const dt = e.dataTransfer;
384
+ if (!dt) return;
385
+ e.preventDefault();
386
+ routeAt(null, e.clientX, e.clientY);
387
+ const dropView = core.routedView();
388
+ const w = toWorld(e.clientX, e.clientY);
389
+ const base = {
390
+ altKey: e.altKey,
391
+ ctrlKey: e.ctrlKey,
392
+ metaKey: e.metaKey,
393
+ shiftKey: e.shiftKey
394
+ };
395
+ void itemsFromDataTransfer(dt).then((items) => {
396
+ if (core.disposed()) return;
397
+ if (items.length === 0) return;
398
+ const routedNow = core.routedView();
399
+ core.setRoutedView(dropView);
400
+ try {
401
+ dispatch({
402
+ kind: "drop",
403
+ items,
404
+ x: w.x,
405
+ y: w.y,
406
+ clientX: e.clientX,
407
+ clientY: e.clientY,
408
+ ...base
409
+ });
410
+ } finally {
411
+ core.setRoutedView(routedNow);
412
+ }
413
+ });
414
+ };
415
+ const onPaste = (e) => {
416
+ if (e.defaultPrevented || isEditableTarget(e.target)) return;
417
+ if (!core.holdsKeys()) return;
418
+ const cd = e.clipboardData;
419
+ if (!cd) return;
420
+ const items = itemsFromClipboardData(cd);
421
+ if (items.length === 0) return;
422
+ const result = dispatch({
423
+ kind: "paste",
424
+ items,
425
+ // ClipboardEvent carries no modifier state.
426
+ altKey: false,
427
+ ctrlKey: false,
428
+ metaKey: false,
429
+ shiftKey: false
430
+ });
431
+ if (result === "handled") e.preventDefault();
432
+ };
433
+ return { onDragOver, onDragLeave, onDrop, onPaste };
434
+ }
435
+
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
+ }
442
+ function keyboardInput(core) {
443
+ const { dispatch, dispatcherNow } = core;
444
+ const heldKeys = /* @__PURE__ */ new Map();
445
+ const codeOf = (e) => e.code ? { code: e.code } : {};
446
+ const physicalKey = (e) => e.code || e.key.toLowerCase();
447
+ const onKeyDown = (e) => {
448
+ if (e.defaultPrevented || isEditableTarget(e.target) || focusedControlOwnsKey(e)) return;
449
+ if (!core.holdsKeys()) return;
450
+ if (e.repeat && heldKeys.has(physicalKey(e))) {
451
+ e.preventDefault();
452
+ return;
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) => {
458
+ const keyEv = {
459
+ kind: "key",
460
+ key: e.key,
461
+ ...codeOf(e),
462
+ ...mods,
463
+ metaKey: e.metaKey,
464
+ shiftKey: e.shiftKey,
465
+ repeat: e.repeat
466
+ };
467
+ const keyResult = dispatch(keyEv);
468
+ const heldEv = {
469
+ kind: "key-held",
470
+ key: e.key,
471
+ ...codeOf(e),
472
+ phase: "down",
473
+ ...mods,
474
+ metaKey: e.metaKey,
475
+ shiftKey: e.shiftKey
476
+ };
477
+ const heldOn = dispatcherNow();
478
+ const heldResult = dispatch(heldEv, heldOn);
479
+ if (heldResult === "handled") {
480
+ heldKeys.set(physicalKey(e), { key: e.key, ...codeOf(e), on: heldOn });
481
+ }
482
+ const handled = keyResult === "handled" || heldResult === "handled";
483
+ if (handled) e.preventDefault();
484
+ return handled;
485
+ };
486
+ const onKeyUp = (e) => {
487
+ const physical = physicalKey(e);
488
+ const down = heldKeys.get(physical);
489
+ if (down === void 0) return;
490
+ const ev = {
491
+ kind: "key-held",
492
+ key: down.key,
493
+ ...down.code !== void 0 ? { code: down.code } : {},
494
+ phase: "up",
495
+ altKey: e.altKey,
496
+ ctrlKey: e.ctrlKey,
497
+ metaKey: e.metaKey,
498
+ shiftKey: e.shiftKey
499
+ };
500
+ heldKeys.delete(physical);
501
+ dispatch(ev, down.on);
502
+ };
503
+ const releaseAll = () => {
504
+ const releasing = [...heldKeys.values()];
505
+ heldKeys.clear();
506
+ for (const { key, code, on } of releasing) {
507
+ const ev = {
508
+ kind: "key-held",
509
+ key,
510
+ ...code !== void 0 ? { code } : {},
511
+ phase: "up",
512
+ altKey: false,
513
+ ctrlKey: false,
514
+ metaKey: false,
515
+ shiftKey: false
516
+ };
517
+ dispatch(ev, on);
518
+ }
519
+ };
520
+ return { onKeyDown, onKeyUp, releaseAll };
157
521
  }
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
+
693
+ // src/interactions/dispatcher/channels/webkitPinch.ts
694
+ function webkitPinchInput(core) {
695
+ const { canvas, routeAt, toWorld, target, dispatch, dispatcherNow, ctxNow } = core;
696
+ let pinchLive = false;
697
+ let pinchScale = 1;
698
+ let pinchRotation = 0;
699
+ const endPinch = () => {
700
+ pinchLive = false;
701
+ pinchScale = 1;
702
+ pinchRotation = 0;
703
+ };
704
+ const pinchEventOf = (e, scale, rotation) => {
705
+ const g = e;
706
+ const rect = typeof g.clientX === "number" && typeof g.clientY === "number" ? void 0 : canvas?.getBoundingClientRect();
707
+ const cx = typeof g.clientX === "number" ? g.clientX : rect ? rect.left + rect.width / 2 : 0;
708
+ const cy = typeof g.clientY === "number" ? g.clientY : rect ? rect.top + rect.height / 2 : 0;
709
+ routeAt(null, cx, cy);
710
+ const local = core.toCanvasLocal(cx, cy);
711
+ const hitPoint = toWorld(cx, cy);
712
+ const mods = {
713
+ altKey: g.altKey === true,
714
+ ctrlKey: g.ctrlKey === true,
715
+ metaKey: g.metaKey === true,
716
+ shiftKey: g.shiftKey === true
717
+ };
718
+ return {
719
+ kind: "pinch",
720
+ scale,
721
+ rotation,
722
+ clientX: local.x,
723
+ clientY: local.y,
724
+ ...underPointer(target(), mods, hitPoint)
725
+ };
726
+ };
727
+ const onGestureStart = (e) => {
728
+ endPinch();
729
+ const probe = pinchEventOf(e, 1, 0);
730
+ if (dispatcherNow().resolveOnly(probe, ctxNow()) === null) return;
731
+ pinchLive = true;
732
+ e.preventDefault();
733
+ };
734
+ const onGestureChange = (e) => {
735
+ const g = e;
736
+ const scale = typeof g.scale === "number" && g.scale > 0 ? g.scale : pinchScale;
737
+ const rotation = typeof g.rotation === "number" ? g.rotation : pinchRotation;
738
+ const step = scale / pinchScale;
739
+ const turn = rotation - pinchRotation;
740
+ pinchScale = scale;
741
+ pinchRotation = rotation;
742
+ if (dispatch(pinchEventOf(e, step, turn)) === "handled") {
743
+ pinchLive = true;
744
+ e.preventDefault();
745
+ } else if (pinchLive) {
746
+ e.preventDefault();
747
+ }
748
+ };
749
+ const onGestureEnd = (e) => {
750
+ if (pinchLive) e.preventDefault();
751
+ endPinch();
752
+ };
753
+ return { onGestureStart, onGestureChange, onGestureEnd, live: () => pinchLive, end: endPinch };
754
+ }
755
+
756
+ // src/interactions/dispatcher/channels/wheel.ts
757
+ function wheelInput(core, pinchLive) {
758
+ const { routeAt, toWorld, target, dispatch } = core;
759
+ const onWheel = (e) => {
760
+ core.activate();
761
+ if (pinchLive() && e.ctrlKey) {
762
+ e.preventDefault();
763
+ e.stopPropagation();
764
+ return;
765
+ }
766
+ routeAt(null, e.clientX, e.clientY);
767
+ const local = core.toCanvasLocal(e.clientX, e.clientY);
768
+ const hitPoint = toWorld(e.clientX, e.clientY);
769
+ const ev = {
770
+ kind: "wheel",
771
+ deltaX: e.deltaX,
772
+ deltaY: e.deltaY,
773
+ clientX: local.x,
774
+ clientY: local.y,
775
+ ...underPointer(target(), e, hitPoint)
776
+ };
777
+ const result = dispatch(ev);
778
+ if (result === "handled") {
779
+ e.preventDefault();
780
+ e.stopImmediatePropagation();
781
+ }
782
+ };
783
+ return onWheel;
784
+ }
785
+
786
+ // src/interactions/dispatcher/useGestureDispatcher.tsx
158
787
  function useGestureDispatcher(opts) {
159
- const { canvasRef, actions, entriesById, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, clientToWorld, requestRedraw, paintedCursor, getRuleCtx, onClick, onDoubleClick, views, channels, longPress } = 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;
160
789
  const pointerChannel = channels?.pointer !== false;
161
790
  const wheelChannel = channels?.wheel !== false;
162
791
  const pinchChannel = channels?.pinch !== false;
@@ -166,6 +795,7 @@ function useGestureDispatcher(opts) {
166
795
  const onDoubleClickRef = useLatest(onDoubleClick);
167
796
  const activeTool = useActiveToolContext();
168
797
  const depRegistry = useDepRegistry();
798
+ const [isMac] = useState(isApplePlatform);
169
799
  const dispatcherRef = useRef(null);
170
800
  if (!dispatcherRef.current) {
171
801
  dispatcherRef.current = dispatcherOpt ?? createDispatcher();
@@ -177,7 +807,7 @@ function useGestureDispatcher(opts) {
177
807
  activeToolId: activeTool.active,
178
808
  hotkeyStack: activeTool.hotkeyStack,
179
809
  entriesById,
180
- isMac: IS_MAC
810
+ isMac
181
811
  });
182
812
  const rootTargetRef = useLatest({
183
813
  id: null,
@@ -193,844 +823,100 @@ function useGestureDispatcher(opts) {
193
823
  ...(viewsRef.current?.targets() ?? []).map((t) => t.dispatcher)
194
824
  ], [rootTargetRef, viewsRef]);
195
825
  const requestRedrawRef = useLatest(requestRedraw);
196
- const paintedCursorRef = useLatest(paintedCursor);
826
+ const cursorRef = useLatest({ painted: paintedCursor, sink: cursorSink });
197
827
  const longPressRef = useLatest(longPress);
198
- const lastClickRef = useRef(null);
199
- const liveToolsRef = useRef(/* @__PURE__ */ new Set());
200
- const prevActiveRef = useRef(activeTool.active);
201
- const entriesByIdRef = useLatest(entriesById);
202
- const fireLifecycle = useCallback((id, hook) => {
203
- const tool = entriesByIdRef.current.get(id);
204
- if (!tool || !isTool(tool)) return;
205
- const fn = tool[hook];
206
- if (!fn) return;
207
- try {
208
- fn({ scratch: tool.initScratch?.() });
209
- } catch (err) {
210
- console.error(`weasel: tool "${id}".${hook} threw`, err);
211
- }
212
- }, [entriesByIdRef]);
213
- useEffect(() => {
214
- const prev = liveToolsRef.current;
215
- const next = new Set(
216
- activeTool.active === null ? activeTool.hotkeyStack : [activeTool.active, ...activeTool.hotkeyStack]
217
- );
218
- liveToolsRef.current = next;
219
- for (const id of prev) if (!next.has(id)) fireLifecycle(id, "onDeactivate");
220
- 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: () => {
221
835
  for (const d of allDispatchers()) d.cancelAll("cancel");
222
836
  }
223
- prevActiveRef.current = activeTool.active;
224
- for (const id of next) if (!prev.has(id)) fireLifecycle(id, "onActivate");
225
837
  });
226
- useEffect(() => () => {
227
- const live = liveToolsRef.current;
228
- liveToolsRef.current = /* @__PURE__ */ new Set();
229
- for (const id of live) fireLifecycle(id, "onDeactivate");
230
- }, [fireLifecycle]);
838
+ useEffect(() => {
839
+ if (enabled && claimKeys) registryRef.current.activate();
840
+ }, [enabled, claimKeys, registryRef]);
231
841
  useEffect(() => {
232
842
  if (!enabled) return;
233
843
  let disposed = false;
234
- let routedId = null;
235
- const target = () => {
236
- if (routedId === null) return rootTargetRef.current;
237
- return viewsRef.current?.targets().find((t) => t.id === routedId) ?? rootTargetRef.current;
238
- };
239
- const routeAt = (pointerId, clientX, clientY) => {
240
- routedId = viewsRef.current?.resolver.at(pointerId, clientX, clientY).id ?? null;
241
- };
242
- const routeDown = (pointerId, clientX, clientY) => {
243
- routedId = viewsRef.current?.resolver.begin(pointerId, clientX, clientY).id ?? null;
244
- };
245
- const routeRelease = (pointerId) => {
246
- viewsRef.current?.resolver.end(pointerId);
247
- };
248
- const dispatcherNow = () => target().dispatcher;
249
- const routedDeps = withViewDeps(() => ctxRef.current.depRegistry, () => target().deps?.());
250
- const routedRuleCtx = () => target().getRuleCtx?.();
251
- const ctxNow = () => ({
252
- ...ctxRef.current,
253
- depRegistry: routedDeps,
254
- getRuleCtx: routedRuleCtx,
255
- viewId: target().id
256
- });
257
844
  const canvas = canvasRef.current;
258
- const toWorld = (cx, cy) => {
259
- const fn = target().clientToWorld;
260
- return fn ? fn(cx, cy) : { x: cx, y: cy };
261
- };
262
- const dispatch = (ev, on) => {
263
- const d = on ?? dispatcherNow();
264
- const before = d.inFlight().size;
265
- const result = d.handleInput(ev, ctxNow());
266
- const after = d.inFlight().size;
267
- if (before > 0 || after > 0) requestRedrawRef.current?.();
268
- return result;
269
- };
270
- const heldKeys = /* @__PURE__ */ new Map();
271
- const physicalKey = (e) => e.code || e.key.toLowerCase();
272
- const held = /* @__PURE__ */ new Map();
273
- const toCanvasLocal = (p) => {
274
- const rect = canvas?.getBoundingClientRect();
275
- if (!rect) return p;
276
- const [x, y] = clientToCanvasRect(rect, p.x, p.y);
277
- return { x, y };
278
- };
279
- const heldGeometry = () => {
280
- const { centroid, spread } = computeMultiTouchGeometry(
281
- [...held.values()].map((p) => p.pos)
282
- );
283
- return { centroid: toCanvasLocal(centroid), spread };
284
- };
285
- let pinchLive = false;
286
- let pinchScale = 1;
287
- let pinchRotation = 0;
288
- const endPinch = () => {
289
- pinchLive = false;
290
- pinchScale = 1;
291
- pinchRotation = 0;
292
- };
293
- let multiTouchStart = null;
294
- const TAP_THRESHOLD_PX = 8;
295
- const longPressTimers = /* @__PURE__ */ new Map();
296
- const clearPending = (pointerId) => {
297
- const store = longPressRef.current?.state;
298
- const p = store?.get();
299
- if (p && (pointerId === void 0 || p.pointerId === pointerId)) store.set(null);
300
- };
301
- const cancelLongPress = (pointerId) => {
302
- const t = longPressTimers.get(pointerId);
303
- if (t !== void 0) {
304
- clearTimeout(t);
305
- longPressTimers.delete(pointerId);
306
- }
307
- clearPending(pointerId);
308
- };
309
- const cancelAllLongPress = () => {
310
- for (const t of longPressTimers.values()) clearTimeout(t);
311
- longPressTimers.clear();
312
- clearPending();
313
- };
314
- const HAPTIC_MS = 15;
315
- const longPressEvents = (down) => {
316
- const shared = {
317
- target: down.target,
318
- altKey: down.altKey,
319
- ctrlKey: down.ctrlKey,
320
- metaKey: down.metaKey,
321
- shiftKey: down.shiftKey,
322
- ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
323
- ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
324
- ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
325
- };
326
- const at = { x: down.worldX, y: down.worldY, clientX: down.clientX, clientY: down.clientY };
327
- return {
328
- longpress: { kind: "longpress", ...at, ...shared },
329
- contextmenu: { kind: "contextmenu", ...at, ...shared }
330
- };
331
- };
332
- const longPressWouldFire = (down) => {
333
- const evs = longPressEvents(down);
334
- const d = dispatcherNow();
335
- return d.resolveOnly(evs.longpress, ctxNow()) !== null || d.resolveOnly(evs.contextmenu, ctxNow()) !== null;
336
- };
337
- const fireLongPress = (pointerId, pointerType) => {
338
- const down = held.get(pointerId)?.down;
339
- clearPending(pointerId);
340
- if (!down) return;
341
- const evs = longPressEvents(down);
342
- let result = dispatch(evs.longpress);
343
- if (result === "unhandled") result = dispatch(evs.contextmenu);
344
- if (result === "handled" && longPressRef.current?.haptics !== false && (pointerType === "touch" || pointerType === "pen") && typeof navigator !== "undefined" && typeof navigator.vibrate === "function") {
345
- navigator.vibrate(HAPTIC_MS);
346
- }
347
- };
348
- const DOUBLE_CLICK_MAX_MS = 600;
349
- const DOUBLE_CLICK_MAX_PX = 8;
350
- const onKeyDown = (e) => {
351
- if (e.defaultPrevented || isEditableTarget(e.target) || activatesFocusedControl(e)) return;
352
- if (!registryRef.current.isActive()) return;
353
- if (e.repeat && heldKeys.has(physicalKey(e))) {
354
- e.preventDefault();
355
- return;
356
- }
357
- const keyEv = {
358
- kind: "key",
359
- key: e.key,
360
- altKey: e.altKey,
361
- ctrlKey: e.ctrlKey,
362
- metaKey: e.metaKey,
363
- shiftKey: e.shiftKey,
364
- repeat: e.repeat
365
- };
366
- const keyResult = dispatch(keyEv);
367
- const heldEv = {
368
- kind: "key-held",
369
- key: e.key,
370
- phase: "down",
371
- altKey: e.altKey,
372
- ctrlKey: e.ctrlKey,
373
- metaKey: e.metaKey,
374
- shiftKey: e.shiftKey
375
- };
376
- const heldResult = dispatch(heldEv);
377
- if (heldResult === "handled") {
378
- heldKeys.set(physicalKey(e), e.key);
379
- }
380
- if (keyResult === "handled" || heldResult === "handled") {
381
- e.preventDefault();
382
- }
383
- };
384
- const onKeyUp = (e) => {
385
- const physical = physicalKey(e);
386
- const downKey = heldKeys.get(physical);
387
- if (downKey === void 0) return;
388
- const ev = {
389
- kind: "key-held",
390
- key: downKey,
391
- phase: "up",
392
- altKey: e.altKey,
393
- ctrlKey: e.ctrlKey,
394
- metaKey: e.metaKey,
395
- shiftKey: e.shiftKey
396
- };
397
- dispatch(ev);
398
- heldKeys.delete(physical);
399
- };
845
+ const redraw = dispatcherWatch(() => {
846
+ requestRedrawRef.current?.();
847
+ });
848
+ redraw.sync(allDispatchers());
849
+ const core = createDispatchCore({
850
+ canvas,
851
+ root: () => rootTargetRef.current,
852
+ views: () => viewsRef.current,
853
+ context: () => ctxRef.current,
854
+ actions: () => registryRef.current,
855
+ uses: () => {
856
+ redraw.sync(allDispatchers());
857
+ },
858
+ disposed: () => disposed
859
+ });
860
+ const keys = keyboardInput(core);
861
+ const pinch = webkitPinchInput(core);
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
+ });
400
871
  const onWindowBlur = () => {
401
- endPinch();
402
- for (const p of [...held.values()]) p.session.cancel();
403
- for (const key of heldKeys.values()) {
404
- const ev = {
405
- kind: "key-held",
406
- key,
407
- phase: "up",
408
- altKey: false,
409
- ctrlKey: false,
410
- metaKey: false,
411
- shiftKey: false
412
- };
413
- dispatch(ev);
414
- }
415
- heldKeys.clear();
416
- };
417
- const onWheel = (e) => {
418
- registryRef.current.activate();
419
- if (pinchLive && e.ctrlKey) {
420
- e.preventDefault();
421
- e.stopPropagation();
422
- return;
423
- }
424
- routeAt(null, e.clientX, e.clientY);
425
- const rect = canvas?.getBoundingClientRect();
426
- const [localX, localY] = rect ? clientToCanvasRect(rect, e.clientX, e.clientY) : [e.clientX, e.clientY];
427
- const hitPoint = toWorld(e.clientX, e.clientY);
428
- const affordance = target().affordanceAt?.(hitPoint) ?? void 0;
429
- const body = target().classifyTarget?.(hitPoint);
430
- const ev = {
431
- kind: "wheel",
432
- altKey: e.altKey,
433
- ctrlKey: e.ctrlKey,
434
- metaKey: e.metaKey,
435
- shiftKey: e.shiftKey,
436
- deltaX: e.deltaX,
437
- deltaY: e.deltaY,
438
- clientX: localX,
439
- clientY: localY,
440
- ...affordance !== void 0 ? { affordance } : {},
441
- ...body?.body !== void 0 ? { bodyTarget: body.body } : {},
442
- ...body?.kind !== void 0 ? { bodyKind: body.kind } : {}
443
- };
444
- const result = dispatch(ev);
445
- if (result === "handled") {
446
- e.preventDefault();
447
- e.stopImmediatePropagation();
448
- }
449
- };
450
- const pinchEventOf = (e, scale, rotation) => {
451
- const g = e;
452
- const rect = canvas?.getBoundingClientRect();
453
- const cx = typeof g.clientX === "number" ? g.clientX : rect ? rect.left + rect.width / 2 : 0;
454
- const cy = typeof g.clientY === "number" ? g.clientY : rect ? rect.top + rect.height / 2 : 0;
455
- routeAt(null, cx, cy);
456
- const [localX, localY] = rect ? clientToCanvasRect(rect, cx, cy) : [cx, cy];
457
- const hitPoint = toWorld(cx, cy);
458
- const affordance = target().affordanceAt?.(hitPoint) ?? void 0;
459
- const body = target().classifyTarget?.(hitPoint);
460
- return {
461
- kind: "pinch",
462
- altKey: g.altKey === true,
463
- ctrlKey: g.ctrlKey === true,
464
- metaKey: g.metaKey === true,
465
- shiftKey: g.shiftKey === true,
466
- scale,
467
- rotation,
468
- clientX: localX,
469
- clientY: localY,
470
- ...affordance !== void 0 ? { affordance } : {},
471
- ...body?.body !== void 0 ? { bodyTarget: body.body } : {},
472
- ...body?.kind !== void 0 ? { bodyKind: body.kind } : {}
473
- };
474
- };
475
- const onGestureStart = (e) => {
476
- endPinch();
477
- const probe = pinchEventOf(e, 1, 0);
478
- if (dispatcherNow().resolveOnly(probe, ctxNow()) === null) return;
479
- pinchLive = true;
480
- e.preventDefault();
481
- };
482
- const onGestureChange = (e) => {
483
- const g = e;
484
- const scale = typeof g.scale === "number" && g.scale > 0 ? g.scale : pinchScale;
485
- const rotation = typeof g.rotation === "number" ? g.rotation : pinchRotation;
486
- const step = scale / pinchScale;
487
- const turn = rotation - pinchRotation;
488
- pinchScale = scale;
489
- pinchRotation = rotation;
490
- if (dispatch(pinchEventOf(e, step, turn)) === "handled") {
491
- pinchLive = true;
492
- e.preventDefault();
493
- } else if (pinchLive) {
494
- e.preventDefault();
495
- }
496
- };
497
- const onGestureEnd = (e) => {
498
- if (pinchLive) e.preventDefault();
499
- endPinch();
500
- };
501
- let claimedDoubleClick = false;
502
- const onNativeDoubleClick = (e) => {
503
- if (!claimedDoubleClick) return;
504
- claimedDoubleClick = false;
505
- e.preventDefault();
506
- };
507
- const onPointerDown = (e) => {
508
- claimedDoubleClick = false;
509
- registryRef.current.activate();
510
- if ((e.button ?? 0) !== 0) return;
511
- if (!canvas) return;
512
- if (isInControlWithin(e.target, canvas)) return;
513
- routeDown(e.pointerId, e.clientX, e.clientY);
514
- const w = toWorld(e.clientX, e.clientY);
515
- const affordance = target().affordanceAt?.(w) ?? void 0;
516
- const body = target().classifyTarget?.(w);
517
- const bodyTarget = body?.body;
518
- const bodyKind = body?.kind;
519
- const ev = {
520
- kind: "pointerdown",
521
- target: e.target,
522
- pointerId: e.pointerId,
523
- x: w.x,
524
- y: w.y,
525
- clientX: e.clientX,
526
- clientY: e.clientY,
527
- altKey: e.altKey,
528
- ctrlKey: e.ctrlKey,
529
- metaKey: e.metaKey,
530
- shiftKey: e.shiftKey,
531
- ...stylusOf(e),
532
- ...affordance !== void 0 ? { affordance } : {},
533
- ...bodyTarget !== void 0 ? { bodyTarget } : {},
534
- ...bodyKind !== void 0 ? { bodyKind } : {}
535
- };
536
- held.set(e.pointerId, {
537
- session: openPointerSession(canvas, e, {
538
- onMove: onPointerMove,
539
- onEnd: onPointerUp,
540
- onCancel: () => {
541
- onPointerCancel(e.pointerId);
542
- }
543
- }),
544
- pos: { x: e.clientX, y: e.clientY },
545
- // Deferred until the first past-threshold pointermove: a bare
546
- // press-and-release should fire `click`, not start a drag action.
547
- buffered: { ev, clientX: e.clientX, clientY: e.clientY },
548
- down: {
549
- target: e.target,
550
- clientX: e.clientX,
551
- clientY: e.clientY,
552
- worldX: w.x,
553
- worldY: w.y,
554
- affordance,
555
- bodyTarget,
556
- bodyKind,
557
- altKey: e.altKey,
558
- ctrlKey: e.ctrlKey,
559
- metaKey: e.metaKey,
560
- shiftKey: e.shiftKey
561
- }
562
- });
563
- if (held.size < 2) dispatch({ ...ev, stage: "press" });
564
- if ((e.pointerType === "touch" || e.pointerType === "pen") && held.size === 1) {
565
- cancelLongPress(e.pointerId);
566
- const duration = longPressRef.current?.duration ?? LONG_PRESS_MS;
567
- longPressTimers.set(
568
- e.pointerId,
569
- setTimeout(() => {
570
- longPressTimers.delete(e.pointerId);
571
- if (disposed) return;
572
- fireLongPress(e.pointerId, e.pointerType);
573
- }, duration)
574
- );
575
- const store = longPressRef.current?.state;
576
- const rec = held.get(e.pointerId);
577
- if (store && rec) {
578
- store.set({
579
- pointerId: e.pointerId,
580
- pointerType: e.pointerType,
581
- client: { x: e.clientX, y: e.clientY },
582
- local: toCanvasLocal({ x: e.clientX, y: e.clientY }),
583
- world: { x: w.x, y: w.y },
584
- viewId: target().id,
585
- startedAt: performance.now(),
586
- duration,
587
- armed: longPressWouldFire(rec.down)
588
- });
589
- }
590
- }
591
- if (held.size >= 2) {
592
- cancelAllLongPress();
593
- for (const p of held.values()) p.buffered = null;
594
- const { centroid, spread } = heldGeometry();
595
- if (!multiTouchStart) {
596
- multiTouchStart = { fingers: held.size, centroid: { ...centroid } };
597
- } else {
598
- multiTouchStart.fingers = held.size;
599
- }
600
- const mt = {
601
- kind: "multitouch",
602
- fingers: held.size,
603
- altKey: e.altKey,
604
- ctrlKey: e.ctrlKey,
605
- metaKey: e.metaKey,
606
- shiftKey: e.shiftKey,
607
- centroid,
608
- spread
609
- };
610
- dispatch(mt);
611
- }
612
- };
613
- const onPointerMove = (e) => {
614
- routeAt(e.pointerId, e.clientX, e.clientY);
615
- const rec = held.get(e.pointerId);
616
- if (rec) rec.pos = { x: e.clientX, y: e.clientY };
617
- const buffered = rec?.buffered;
618
- if (rec && buffered) {
619
- if (pastDragThreshold(buffered, e)) {
620
- rec.buffered = null;
621
- cancelLongPress(e.pointerId);
622
- dispatch(buffered.ev);
623
- }
624
- }
625
- const mtGestureId = `multitouch-${held.size}`;
626
- if (held.size >= 2 && dispatcherNow().inFlight().has(mtGestureId)) {
627
- const { centroid, spread } = heldGeometry();
628
- const mtEv = {
629
- kind: "multitouch",
630
- fingers: held.size,
631
- altKey: e.altKey,
632
- ctrlKey: e.ctrlKey,
633
- metaKey: e.metaKey,
634
- shiftKey: e.shiftKey,
635
- centroid,
636
- spread
637
- };
638
- dispatch(mtEv);
639
- }
640
- const w = toWorld(e.clientX, e.clientY);
641
- const ev = {
642
- kind: "pointermove",
643
- pointerId: e.pointerId,
644
- x: w.x,
645
- y: w.y,
646
- clientX: e.clientX,
647
- clientY: e.clientY,
648
- altKey: e.altKey,
649
- ctrlKey: e.ctrlKey,
650
- metaKey: e.metaKey,
651
- shiftKey: e.shiftKey,
652
- ...stylusOf(e)
653
- };
654
- dispatch(ev);
655
- lastHover = {
656
- clientX: e.clientX,
657
- clientY: e.clientY,
658
- altKey: e.altKey,
659
- ctrlKey: e.ctrlKey,
660
- metaKey: e.metaKey,
661
- shiftKey: e.shiftKey
662
- };
663
- refreshHoverCursor();
664
- };
665
- let lastHover = null;
666
- let cursorOverridden = false;
667
- const clearHoverCursor = () => {
668
- paintedCursorRef.current?.()?.setOverride(null);
669
- if (cursorOverridden && canvas) {
670
- canvas.style.cursor = "";
671
- cursorOverridden = false;
672
- }
673
- };
674
- const applyHoverCursor = (spec) => {
675
- const tier = spec === null ? null : resolveCursorTier(spec);
676
- if (!tier || !canvas) {
677
- clearHoverCursor();
678
- return;
679
- }
680
- const painted = tier.kind === "painted" ? tier : null;
681
- paintedCursorRef.current?.()?.setOverride(painted);
682
- const cursor = tier.kind === "css" ? tier.css : "none";
683
- if (!cursor) {
684
- clearHoverCursor();
685
- return;
686
- }
687
- canvas.style.cursor = cursor;
688
- cursorOverridden = true;
689
- };
690
- function pushPaintedPointer() {
691
- const painted = paintedCursorRef.current?.();
692
- if (!painted?.active() || !canvas || !lastHover) return;
693
- const r = canvas.getBoundingClientRect();
694
- painted.setPointer(lastHover.clientX - r.left, lastHover.clientY - r.top);
695
- }
696
- function refreshHoverCursor() {
697
- resolveHoverCursor();
698
- pushPaintedPointer();
699
- }
700
- function resolveHoverCursor() {
701
- if (!canvas || !lastHover) return;
702
- if (held.size > 0 || dispatcherNow().inFlight().size > 0) {
703
- applyHoverCursor(dispatcherNow().inFlightCursor());
704
- return;
705
- }
706
- const h = lastHover;
707
- routeAt(null, h.clientX, h.clientY);
708
- const w = toWorld(h.clientX, h.clientY);
709
- const affordance = target().affordanceAt?.(w) ?? void 0;
710
- if (affordance?.cursor) {
711
- applyHoverCursor(affordance.cursor);
712
- return;
713
- }
714
- const hoverBody = target().classifyTarget?.(w);
715
- const bodyTarget = hoverBody?.body;
716
- const bodyKind = hoverBody?.kind;
717
- const where = {
718
- altKey: h.altKey,
719
- ctrlKey: h.ctrlKey,
720
- metaKey: h.metaKey,
721
- shiftKey: h.shiftKey,
722
- ...affordance !== void 0 ? { affordance } : {},
723
- ...bodyTarget !== void 0 ? { bodyTarget } : {},
724
- ...bodyKind !== void 0 ? { bodyKind } : {}
725
- };
726
- const ctx = ctxNow();
727
- const press = {
728
- kind: "pointerdown",
729
- x: w.x,
730
- y: w.y,
731
- clientX: h.clientX,
732
- clientY: h.clientY,
733
- ...where
734
- };
735
- const d = dispatcherNow();
736
- publishLiveDispatch(() => d.explain(press, ctx));
737
- const dragCursor = d.resolveOnly(press, ctx)?.action.cursor;
738
- const cursor = dragCursor ?? dispatcherNow().resolveOnly(
739
- { kind: "click", x: w.x, y: w.y, pressX: w.x, pressY: w.y, ...where },
740
- ctx
741
- )?.action.cursor;
742
- applyHoverCursor(cursor ?? null);
743
- }
744
- const scheduleHoverCursorRefresh = (e) => {
745
- if (!lastHover) return;
746
- lastHover = {
747
- ...lastHover,
748
- altKey: e.altKey,
749
- ctrlKey: e.ctrlKey,
750
- metaKey: e.metaKey,
751
- shiftKey: e.shiftKey
752
- };
753
- setTimeout(() => {
754
- if (!disposed) refreshHoverCursor();
755
- }, 0);
756
- };
757
- const onHoverMove = (e) => {
758
- if (held.has(e.pointerId)) return;
759
- onPointerMove(e);
760
- };
761
- const onHoverPointerLeave = () => {
762
- lastHover = null;
763
- publishLiveDispatch(null);
764
- paintedCursorRef.current?.()?.clearPointer();
765
- clearHoverCursor();
766
- };
767
- const onPointerUp = (e) => {
768
- routeAt(e.pointerId, e.clientX, e.clientY);
769
- cancelLongPress(e.pointerId);
770
- const prevSize = held.size;
771
- const rec = held.get(e.pointerId);
772
- held.delete(e.pointerId);
773
- const staysUnderThreshold = rec?.buffered != null;
774
- if (prevSize >= 2 && held.size < 2) {
775
- const mtGestureId = `multitouch-${prevSize}`;
776
- if (dispatcherNow().inFlight().has(mtGestureId)) {
777
- dispatcherNow().cancelAll("commit");
778
- }
779
- const start = multiTouchStart;
780
- if (start) {
781
- const centroid = toCanvasLocal(
782
- computeMultiTouchGeometry([
783
- ...[...held.values()].map((p) => p.pos),
784
- { x: e.clientX, y: e.clientY }
785
- ]).centroid
786
- );
787
- const dx = centroid.x - start.centroid.x;
788
- const dy = centroid.y - start.centroid.y;
789
- if (Math.hypot(dx, dy) <= TAP_THRESHOLD_PX) {
790
- const tap = {
791
- kind: "multitouchtap",
792
- fingers: start.fingers,
793
- altKey: e.altKey,
794
- ctrlKey: e.ctrlKey,
795
- metaKey: e.metaKey,
796
- shiftKey: e.shiftKey
797
- };
798
- dispatch(tap);
799
- }
800
- multiTouchStart = null;
801
- }
802
- }
803
- const movedDuringDrag = rec ? rec.down.clientX !== e.clientX || rec.down.clientY !== e.clientY : true;
804
- const hadDragInFlight = dispatcherNow().inFlight().has(pointerGestureId(e.pointerId)) && movedDuringDrag;
805
- const wUp = toWorld(e.clientX, e.clientY);
806
- const ev = {
807
- kind: "pointerup",
808
- pointerId: e.pointerId,
809
- x: wUp.x,
810
- y: wUp.y,
811
- clientX: e.clientX,
812
- clientY: e.clientY,
813
- altKey: e.altKey,
814
- ctrlKey: e.ctrlKey,
815
- metaKey: e.metaKey,
816
- shiftKey: e.shiftKey
817
- };
818
- dispatch(ev);
819
- const down = rec?.down;
820
- if (!hadDragInFlight && staysUnderThreshold && down) {
821
- const wClick = toWorld(e.clientX, e.clientY);
822
- const clickEv = {
823
- kind: "click",
824
- target: e.target,
825
- altKey: e.altKey,
826
- ctrlKey: e.ctrlKey,
827
- metaKey: e.metaKey,
828
- shiftKey: e.shiftKey,
829
- x: wClick.x,
830
- y: wClick.y,
831
- clientX: e.clientX,
832
- clientY: e.clientY,
833
- pressX: down.worldX,
834
- pressY: down.worldY,
835
- ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
836
- ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
837
- ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
838
- };
839
- dispatch(clickEv);
840
- onClickRef.current?.({ x: wClick.x, y: wClick.y });
841
- const now = Date.now();
842
- const prev = lastClickRef.current;
843
- const dx = e.clientX - (prev?.clientX ?? 0);
844
- const dy = e.clientY - (prev?.clientY ?? 0);
845
- if (prev && now - prev.t < DOUBLE_CLICK_MAX_MS && Math.hypot(dx, dy) < DOUBLE_CLICK_MAX_PX) {
846
- const dblEv = {
847
- kind: "doubleclick",
848
- target: e.target,
849
- altKey: e.altKey,
850
- ctrlKey: e.ctrlKey,
851
- metaKey: e.metaKey,
852
- shiftKey: e.shiftKey,
853
- x: wClick.x,
854
- y: wClick.y,
855
- clientX: e.clientX,
856
- clientY: e.clientY,
857
- ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
858
- ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
859
- ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
860
- };
861
- onDoubleClickRef.current?.({ x: wClick.x, y: wClick.y });
862
- const handled = dispatch(dblEv) === "handled";
863
- claimedDoubleClick = handled || onDoubleClickRef.current !== void 0;
864
- lastClickRef.current = null;
865
- } else {
866
- lastClickRef.current = { t: now, clientX: e.clientX, clientY: e.clientY };
867
- }
868
- }
869
- routeRelease(e.pointerId);
870
- lastHover = {
871
- clientX: e.clientX,
872
- clientY: e.clientY,
873
- altKey: e.altKey,
874
- ctrlKey: e.ctrlKey,
875
- metaKey: e.metaKey,
876
- shiftKey: e.shiftKey
877
- };
878
- setTimeout(() => {
879
- if (!disposed) refreshHoverCursor();
880
- }, 0);
881
- };
882
- const onPointerCancel = (pointerId) => {
883
- if (disposed) return;
884
- const rec = held.get(pointerId);
885
- if (!rec) return;
886
- held.delete(pointerId);
887
- routeAt(pointerId, rec.pos.x, rec.pos.y);
888
- cancelLongPress(pointerId);
889
- multiTouchStart = null;
890
- const ev = {
891
- kind: "pointercancel",
892
- pointerId,
893
- altKey: rec.down.altKey,
894
- ctrlKey: rec.down.ctrlKey,
895
- metaKey: rec.down.metaKey,
896
- shiftKey: rec.down.shiftKey
897
- };
898
- dispatch(ev);
899
- routeRelease(pointerId);
900
- };
901
- const onContextMenu = (e) => {
902
- e.preventDefault();
903
- routeAt(null, e.clientX, e.clientY);
904
- const w = toWorld(e.clientX, e.clientY);
905
- const affordance = target().affordanceAt?.(w) ?? void 0;
906
- const menuBody = target().classifyTarget?.(w);
907
- const ev = {
908
- kind: "contextmenu",
909
- target: e.target,
910
- altKey: e.altKey,
911
- ctrlKey: e.ctrlKey,
912
- metaKey: e.metaKey,
913
- shiftKey: e.shiftKey,
914
- x: w.x,
915
- y: w.y,
916
- clientX: e.clientX,
917
- clientY: e.clientY,
918
- ...affordance !== void 0 ? { affordance } : {},
919
- ...menuBody?.body !== void 0 ? { bodyTarget: menuBody.body } : {},
920
- ...menuBody?.kind !== void 0 ? { bodyKind: menuBody.kind } : {}
921
- };
922
- dispatch(ev);
923
- };
924
- const onDragOver = (e) => {
925
- if (!e.dataTransfer) return;
926
- e.preventDefault();
927
- e.dataTransfer.dropEffect = "copy";
928
- canvas?.classList.add(DROPOVER_CLASS);
929
- };
930
- const onDragLeave = () => {
931
- canvas?.classList.remove(DROPOVER_CLASS);
932
- };
933
- const onDrop = (e) => {
934
- canvas?.classList.remove(DROPOVER_CLASS);
935
- const dt = e.dataTransfer;
936
- if (!dt) return;
937
- e.preventDefault();
938
- routeAt(null, e.clientX, e.clientY);
939
- const dropOn = target().dispatcher;
940
- const w = toWorld(e.clientX, e.clientY);
941
- const base = {
942
- altKey: e.altKey,
943
- ctrlKey: e.ctrlKey,
944
- metaKey: e.metaKey,
945
- shiftKey: e.shiftKey
946
- };
947
- void itemsFromDataTransfer(dt).then((items) => {
948
- if (disposed) return;
949
- if (items.length === 0) return;
950
- dispatch({
951
- kind: "drop",
952
- items,
953
- x: w.x,
954
- y: w.y,
955
- clientX: e.clientX,
956
- clientY: e.clientY,
957
- ...base
958
- }, dropOn);
959
- });
960
- };
961
- const onPaste = (e) => {
962
- if (isEditableTarget(e.target)) return;
963
- const cd = e.clipboardData;
964
- if (!cd) return;
965
- const items = itemsFromClipboardData(cd);
966
- if (items.length === 0) return;
967
- const result = dispatch({
968
- kind: "paste",
969
- items,
970
- // ClipboardEvent carries no modifier state.
971
- altKey: false,
972
- ctrlKey: false,
973
- metaKey: false,
974
- shiftKey: false
975
- });
976
- if (result === "handled") e.preventDefault();
872
+ pinch.end();
873
+ pointer.cancelHeld();
874
+ keys.releaseAll();
977
875
  };
876
+ const listeners = listenerSet();
978
877
  const keyCapture = keyboard === "first";
979
878
  if (keyboard) {
980
- window.addEventListener("keydown", onKeyDown, keyCapture);
981
- window.addEventListener("keyup", onKeyUp, keyCapture);
879
+ listeners.on(window, "keydown", keys.onKeyDown, keyCapture);
880
+ listeners.on(window, "keyup", keys.onKeyUp, keyCapture);
982
881
  }
983
- window.addEventListener("blur", onWindowBlur);
984
- window.addEventListener("keydown", scheduleHoverCursorRefresh);
985
- window.addEventListener("keyup", scheduleHoverCursorRefresh);
882
+ listeners.on(window, "blur", onWindowBlur);
883
+ listeners.on(canvas, "pointerdown", () => {
884
+ registryRef.current.activate();
885
+ });
986
886
  if (pointerChannel) {
987
- canvas?.addEventListener("pointerleave", onHoverPointerLeave);
988
- canvas?.addEventListener("pointerdown", onPointerDown);
989
- canvas?.addEventListener("pointermove", onHoverMove);
990
- 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 });
991
896
  }
992
- if (wheelChannel) canvas?.addEventListener("wheel", onWheel, { passive: false, capture: true });
993
897
  if (pinchChannel) {
994
- canvas?.addEventListener("gesturestart", onGestureStart);
995
- canvas?.addEventListener("gesturechange", onGestureChange);
996
- 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));
997
904
  }
998
- if (contextMenuChannel) canvas?.addEventListener("contextmenu", onContextMenu);
999
905
  if (ingestChannel) {
1000
- canvas?.addEventListener("dragenter", onDragOver);
1001
- canvas?.addEventListener("dragover", onDragOver);
1002
- canvas?.addEventListener("dragleave", onDragLeave);
1003
- canvas?.addEventListener("drop", onDrop);
1004
- 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);
1005
911
  }
1006
912
  return () => {
1007
- if (keyboard) {
1008
- window.removeEventListener("keydown", onKeyDown, keyCapture);
1009
- window.removeEventListener("keyup", onKeyUp, keyCapture);
1010
- }
1011
- window.removeEventListener("blur", onWindowBlur);
1012
- window.removeEventListener("keydown", scheduleHoverCursorRefresh);
1013
- window.removeEventListener("keyup", scheduleHoverCursorRefresh);
1014
- canvas?.removeEventListener("pointerleave", onHoverPointerLeave);
1015
- canvas?.removeEventListener("wheel", onWheel, { capture: true });
1016
- canvas?.removeEventListener("gesturestart", onGestureStart);
1017
- canvas?.removeEventListener("gesturechange", onGestureChange);
1018
- canvas?.removeEventListener("gestureend", onGestureEnd);
1019
- canvas?.removeEventListener("pointerdown", onPointerDown);
1020
- canvas?.removeEventListener("pointermove", onHoverMove);
1021
- canvas?.removeEventListener("dblclick", onNativeDoubleClick);
1022
- canvas?.removeEventListener("contextmenu", onContextMenu);
1023
- canvas?.removeEventListener("dragenter", onDragOver);
1024
- canvas?.removeEventListener("dragover", onDragOver);
1025
- canvas?.removeEventListener("dragleave", onDragLeave);
1026
- canvas?.removeEventListener("drop", onDrop);
1027
- window.removeEventListener("paste", onPaste);
913
+ listeners.removeAll();
1028
914
  canvas?.classList.remove(DROPOVER_CLASS);
1029
- clearHoverCursor();
1030
- cancelAllLongPress();
915
+ registryRef.current.dropKeys();
916
+ hover.clear();
1031
917
  disposed = true;
1032
- for (const p of held.values()) p.session.cancel();
1033
- held.clear();
918
+ pointer.dispose();
919
+ redraw.stop();
1034
920
  for (const d of allDispatchers()) d.cancelAll("cancel");
1035
921
  };
1036
922
  }, [
@@ -1047,7 +933,7 @@ function useGestureDispatcher(opts) {
1047
933
  longPressRef,
1048
934
  onClickRef,
1049
935
  onDoubleClickRef,
1050
- paintedCursorRef,
936
+ cursorRef,
1051
937
  registryRef,
1052
938
  requestRedrawRef,
1053
939
  rootTargetRef,
@@ -1118,6 +1004,7 @@ var ActionsNode = class extends ScopeNode {
1118
1004
  return null;
1119
1005
  }
1120
1006
  activate() {
1007
+ keyHolder = this;
1121
1008
  super.activate();
1122
1009
  for (const f of this.onActivate) f();
1123
1010
  }
@@ -1138,7 +1025,11 @@ var ActionsNode = class extends ScopeNode {
1138
1025
  const i = cur.lastIndexOf(action);
1139
1026
  if (i === -1) return;
1140
1027
  cur.splice(i, 1);
1141
- if (cur.length === 0) this.actions.delete(action.id);
1028
+ if (cur.length === 0) {
1029
+ queueMicrotask(() => {
1030
+ if (cur.length === 0 && this.actions.get(action.id) === cur) this.actions.delete(action.id);
1031
+ });
1032
+ }
1142
1033
  this.changed();
1143
1034
  };
1144
1035
  },
@@ -1163,22 +1054,17 @@ var ActionsNode = class extends ScopeNode {
1163
1054
  trigger: (id, params) => {
1164
1055
  const leaf = this.start();
1165
1056
  const a = leaf.resolve(id);
1166
- if (!a) return false;
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;
1167
1060
  try {
1168
- if (a.invoker && a.invoker.timing === "immediate") {
1169
- a.invoker.run(depsFor(a, leaf.deps()), params);
1170
- }
1061
+ a.invoker.run(deps, params);
1171
1062
  } catch (err) {
1172
1063
  console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
1173
1064
  }
1174
1065
  return true;
1175
1066
  },
1176
- subscribe: (listener) => {
1177
- this.tree.listeners.add(listener);
1178
- return () => {
1179
- this.tree.listeners.delete(listener);
1180
- };
1181
- },
1067
+ subscribe: (listener) => this.tree.listeners.subscribe(listener),
1182
1068
  setDispatcher: (d) => pushOwner(this.dispatchers, d),
1183
1069
  begin: (id, params) => {
1184
1070
  const leaf = this.start();
@@ -1188,9 +1074,14 @@ var ActionsNode = class extends ScopeNode {
1188
1074
  return disp.beginUiOngoing(id, depsFor(a, leaf.deps()), params);
1189
1075
  },
1190
1076
  activate: () => this.activate(),
1191
- isActive: () => this.isActive()
1077
+ isActive: () => this.isActive(),
1078
+ holdsKeys: () => keyHolder === this,
1079
+ dropKeys: () => {
1080
+ if (keyHolder === this) keyHolder = null;
1081
+ }
1192
1082
  };
1193
1083
  };
1084
+ var keyHolder = null;
1194
1085
  var NODES = /* @__PURE__ */ new WeakMap();
1195
1086
  function actionsNodeOf(registry) {
1196
1087
  return registry ? NODES.get(registry) ?? null : null;
@@ -1271,16 +1162,14 @@ function useOngoingAction(actionId) {
1271
1162
  ctrlRef.current = null;
1272
1163
  ctrl?.end(reason);
1273
1164
  };
1165
+ const drive = (params) => {
1166
+ if (ctrlRef.current?.active) ctrlRef.current.update(params);
1167
+ else ctrlRef.current = regRef.current?.begin(idRef.current, params) ?? null;
1168
+ };
1274
1169
  return {
1275
- input(params) {
1276
- if (ctrlRef.current) ctrlRef.current.update(params);
1277
- else ctrlRef.current = regRef.current?.begin(idRef.current, params) ?? null;
1278
- },
1170
+ input: drive,
1279
1171
  commit(params) {
1280
- if (params !== void 0) {
1281
- if (ctrlRef.current) ctrlRef.current.update(params);
1282
- else ctrlRef.current = regRef.current?.begin(idRef.current, params) ?? null;
1283
- }
1172
+ if (params !== void 0) drive(params);
1284
1173
  end("commit");
1285
1174
  },
1286
1175
  cancel() {
@@ -1291,6 +1180,98 @@ function useOngoingAction(actionId) {
1291
1180
  useEffect(() => () => edit.commit(), [edit, actionId]);
1292
1181
  return edit;
1293
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
+ }
1273
+ var NO_MODES = () => () => {
1274
+ };
1294
1275
  function useContributions(opts) {
1295
1276
  const ctx = useActiveToolContext();
1296
1277
  const actionsRegistry = useActionsRegistry();
@@ -1309,25 +1290,18 @@ function useContributions(opts) {
1309
1290
  const setFocused = useCallback((id) => {
1310
1291
  ctx.setActive(id);
1311
1292
  }, [ctx]);
1312
- const snapshot = useCallback(() => {
1313
- const engagedIds = new Set(engaged);
1314
- const ordered = [];
1315
- for (const id of engaged) {
1316
- const entry = entries.find((e) => e.id === id);
1317
- if (entry && !ordered.includes(entry)) ordered.push(entry);
1318
- }
1319
- for (const entry of entries) {
1320
- if (!ordered.includes(entry)) ordered.push(entry);
1321
- }
1322
- return {
1323
- ordered,
1324
- state: { focusedId: focused, engagedIds }
1325
- };
1326
- }, [entries, focused, engaged]);
1327
- const scopedBindings = useCallback(() => {
1328
- const { ordered, state } = snapshot();
1329
- return scopeBindings(ordered, state);
1330
- }, [snapshot]);
1293
+ const modes = opts.modes;
1294
+ const modeVersion = useSyncExternalStore(
1295
+ modes ? modes.subscribe : NO_MODES,
1296
+ () => modes?.getVersion() ?? 0
1297
+ );
1298
+ const snapshot = useCallback(() => assembleEntries(entries, {
1299
+ focusedId: focused,
1300
+ hotkeyStack: engaged,
1301
+ ...modes ? { allowedCapabilities: activeModeOf(modes.current()).allowedCapabilities } : {}
1302
+ // The version is what changes; `modes` is one object for the registry's life.
1303
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1304
+ }), [entries, focused, engaged, modes, modeVersion]);
1331
1305
  const overlays = useCallback((position = "top") => {
1332
1306
  const { ordered, state } = snapshot();
1333
1307
  const active = [];
@@ -1367,8 +1341,8 @@ function useContributions(opts) {
1367
1341
  });
1368
1342
  }, [entrySig, entries, actionsRegistry, modesRegistry]);
1369
1343
  return useMemo(
1370
- () => ({ entries, focused, setFocused, scopedBindings, overlays }),
1371
- [entries, focused, setFocused, scopedBindings, overlays]
1344
+ () => ({ entries, focused, setFocused, overlays }),
1345
+ [entries, focused, setFocused, overlays]
1372
1346
  );
1373
1347
  }
1374
1348
  function useOffhandAction(entries) {
@@ -1485,6 +1459,6 @@ function useTools(opts) {
1485
1459
  );
1486
1460
  }
1487
1461
 
1488
- 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 };
1489
1463
  //# sourceMappingURL=react.js.map
1490
1464
  //# sourceMappingURL=react.js.map