@effindomv2/runtime 0.1.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.
Files changed (92) hide show
  1. package/LICENSE.md +6 -0
  2. package/dist/bridge.js +4 -0
  3. package/dist/bridge.js.map +7 -0
  4. package/dist/effindom.v2.manifest.json +68 -0
  5. package/dist/fonts/NotoColorEmoji.ttf +0 -0
  6. package/dist/fonts/NotoEmoji-Regular.ttf +0 -0
  7. package/dist/fonts/NotoSans-Bold.ttf +0 -0
  8. package/dist/fonts/NotoSans-BoldItalic.ttf +0 -0
  9. package/dist/fonts/NotoSans-Italic.ttf +0 -0
  10. package/dist/fonts/NotoSans-Regular.ttf +0 -0
  11. package/dist/fonts/NotoSansMono-Bold.ttf +0 -0
  12. package/dist/fonts/NotoSansMono-Regular.ttf +0 -0
  13. package/dist/fonts/NotoSansSymbols2-Regular.ttf +0 -0
  14. package/dist/harness.js +2 -0
  15. package/dist/harness.js.map +7 -0
  16. package/dist/index.html +53 -0
  17. package/dist/runtime/effindom-core-v2.wasm32-simd.JQXIaRaN0-JahfIVFiSLE49WzzCENvef_2EDEm09nJs.wasm +0 -0
  18. package/dist/runtime/effindom-core-v2.wasm32-simd.y7RzpkMARiFeRkpgiqKQsAfv4Hf17NYdpni-6aLNhMs.js.symbols +10079 -0
  19. package/dist/runtime/effindom-core-v2.wasm32-simd.yhT7DGUv4soEv4W91WVZl3T7T_ecKojk5_IcnwL79a0.js +1 -0
  20. package/dist/runtime/effindom-core-v2.wasm32.JSfMkp9ertJzSZxA-_xz3yacrJUhswxlwbqbJLRIuqw.wasm +0 -0
  21. package/dist/runtime/effindom-core-v2.wasm32.xNgsQv7dCwf8Uy-PfJSoRNyk9-q1OSogUwkk5g6ZBjk.js.symbols +10088 -0
  22. package/dist/runtime/effindom-core-v2.wasm32.yhT7DGUv4soEv4W91WVZl3T7T_ecKojk5_IcnwL79a0.js +1 -0
  23. package/dist/runtime/effindom-core-v2.wasm64-simd.GkByf-CPorNOs1CORny_8JjVk8Z3piiFq92r-uw1Syc.js +1 -0
  24. package/dist/runtime/effindom-core-v2.wasm64-simd.p4P98oRu2wEWxtRRW8RHr27JhGeWvWlziZXDM_z3Nc4.js.symbols +10286 -0
  25. package/dist/runtime/effindom-core-v2.wasm64-simd.y75FYXRwhQrpaDGYbZWrohGDv0AmjTb-EjXwOjBIgnM.wasm +0 -0
  26. package/dist/runtime/effindom-core-v2.wasm64.GkByf-CPorNOs1CORny_8JjVk8Z3piiFq92r-uw1Syc.js +1 -0
  27. package/dist/runtime/effindom-core-v2.wasm64.emhE1_CJs4_zXp8wiQS_5lYpUQ0OchmXgxksi0ykaBs.js.symbols +10298 -0
  28. package/dist/runtime/effindom-core-v2.wasm64.sO-Yu70cfN8Qs3a5iEp6cbFPaiOchqcMKUzryu4npNo.wasm +0 -0
  29. package/dist/runtime/effindom-ui-v2.wasm32-simd.0Mas1XD03eYvemryTioWaZOBuBA5ij7MFlTa8CgEZWs.wasm +0 -0
  30. package/dist/runtime/effindom-ui-v2.wasm32-simd.ThSDClMnSWdwf9d89JZfYor0G1Z6OxR4lOc75rNRuD4.js.symbols +1890 -0
  31. package/dist/runtime/effindom-ui-v2.wasm32-simd.wved0xEV4EKXVNBU3Sx7giD4faxD2YII9sQ2N_wCP4I.js +2 -0
  32. package/dist/runtime/effindom-ui-v2.wasm32.H7kYg99bT9ADGh0uUvj6H9Dk1L058nVFLv_4R79IXW8.js.symbols +1900 -0
  33. package/dist/runtime/effindom-ui-v2.wasm32.tp53X7nHfG_EUq29naDyElfnqhMw2D1Tr1T-BJAYO7w.wasm +0 -0
  34. package/dist/runtime/effindom-ui-v2.wasm32.wved0xEV4EKXVNBU3Sx7giD4faxD2YII9sQ2N_wCP4I.js +2 -0
  35. package/dist/runtime/effindom-ui-v2.wasm64-simd.86tk9Z3xIpgTOykET_8Nn9iUVJnp1AzOHW4fVQRGtQE.wasm +0 -0
  36. package/dist/runtime/effindom-ui-v2.wasm64-simd.RQaXil22Chu63-vxK9oOuX8wUY044kbo190oYIbBU4M.js.symbols +1918 -0
  37. package/dist/runtime/effindom-ui-v2.wasm64-simd.ZS1KEAg0XQex-VXkfgpBHE8MIoqPF8qpaf8nOjANb_U.js +2 -0
  38. package/dist/runtime/effindom-ui-v2.wasm64.YSwpMFbr-Q1SBe0Ze8mub1u1PqsvSz3QIYuA3eaUMME.js.symbols +1924 -0
  39. package/dist/runtime/effindom-ui-v2.wasm64.ZS1KEAg0XQex-VXkfgpBHE8MIoqPF8qpaf8nOjANb_U.js +2 -0
  40. package/dist/runtime/effindom-ui-v2.wasm64.ioQ9DuM6gR_EjlfRHdF8EvNPBcKCs0PQbbY9-cjTV6Y.wasm +0 -0
  41. package/dist/runtime/icudt_minimal.962CX1q0-Nbv-OqXPaub5piYTOLumUk-nEvemcvvnpw.dat +0 -0
  42. package/package.json +62 -0
  43. package/scripts/build.sh +279 -0
  44. package/scripts/build_assets.sh +51 -0
  45. package/scripts/font_assets.sh +52 -0
  46. package/scripts/generate_manifest.py +121 -0
  47. package/scripts/stage_package_assets.sh +42 -0
  48. package/src/bridge/commit-policy.ts +10 -0
  49. package/src/bridge/events/canvas-geometry.ts +78 -0
  50. package/src/bridge/events/key-router.ts +187 -0
  51. package/src/bridge/events/pointer-router.ts +619 -0
  52. package/src/bridge/events/semantic-hit-testing.ts +27 -0
  53. package/src/bridge/events.ts +54 -0
  54. package/src/bridge/find-dialog.ts +690 -0
  55. package/src/bridge/find-session.ts +158 -0
  56. package/src/bridge/font-catalog.ts +51 -0
  57. package/src/bridge/google-fonts.ts +63 -0
  58. package/src/bridge/incremental-font-packages.ts +216 -0
  59. package/src/bridge/init.ts +77 -0
  60. package/src/bridge/interaction/editor-model.ts +371 -0
  61. package/src/bridge/interaction/editor-mutations.ts +495 -0
  62. package/src/bridge/interaction/editor-session.ts +628 -0
  63. package/src/bridge/interaction/logs.ts +23 -0
  64. package/src/bridge/interaction/text-encoding.ts +51 -0
  65. package/src/bridge/interaction.ts +86 -0
  66. package/src/bridge/local-types.ts +105 -0
  67. package/src/bridge/platform.ts +68 -0
  68. package/src/bridge/pointer-move-coalescer.ts +41 -0
  69. package/src/bridge/pull-to-refresh.ts +124 -0
  70. package/src/bridge/render-loop.ts +268 -0
  71. package/src/bridge/runtime/asset-manager.ts +202 -0
  72. package/src/bridge/runtime/find-controller.ts +269 -0
  73. package/src/bridge/runtime/font-manager.ts +691 -0
  74. package/src/bridge/runtime/open-canvas-api.ts +72 -0
  75. package/src/bridge/runtime/semantic-controller.ts +133 -0
  76. package/src/bridge/runtime/text-documents.ts +234 -0
  77. package/src/bridge/runtime.ts +315 -0
  78. package/src/bridge/touch-gesture.ts +159 -0
  79. package/src/bridge/utils/assets.ts +572 -0
  80. package/src/bridge/utils/backends.ts +163 -0
  81. package/src/bridge/utils/encoding.ts +128 -0
  82. package/src/bridge/utils/fetch.ts +147 -0
  83. package/src/bridge/utils/heap.ts +118 -0
  84. package/src/bridge.ts +93 -0
  85. package/src/clipboard.ts +139 -0
  86. package/src/core-types.ts +595 -0
  87. package/src/find-on-page.ts +284 -0
  88. package/src/harness.ts +53 -0
  89. package/src/index.ts +40 -0
  90. package/src/open-canvas.ts +108 -0
  91. package/src/runtime-config.ts +96 -0
  92. package/src/semantic.ts +905 -0
@@ -0,0 +1,619 @@
1
+ import type { BridgeRuntime } from '../../core-types';
2
+ import type { BridgeInteractionState } from '../local-types';
3
+ import { commitIfVisualWork } from '../commit-policy';
4
+ import { PointerMoveCoalescer } from '../pointer-move-coalescer';
5
+ import { PULL_TO_REFRESH_THRESHOLD, type PullToRefreshOverlay } from '../pull-to-refresh';
6
+ import {
7
+ getPointerPosition,
8
+ isPointerInsideCanvas,
9
+ normalizeWheelDelta,
10
+ } from './canvas-geometry';
11
+ import { findSemanticTextboxHandleAtPoint } from './semantic-hit-testing';
12
+ import {
13
+ type TouchGestureState,
14
+ transitionTouchGesture,
15
+ } from '../touch-gesture';
16
+ import { computeModifiers, handleToBigInt } from '../utils/encoding';
17
+
18
+ const UI_EVENT_POINTER_DOWN = 1;
19
+ const UI_EVENT_POINTER_UP = 2;
20
+ const UI_EVENT_POINTER_MOVE = 3;
21
+ const UI_EVENT_POINTER_LEAVE = 5;
22
+ const EDGE_AUTOSCROLL_THRESHOLD = 30;
23
+ const TOUCH_SCROLL_THRESHOLD = 8;
24
+ const TOUCH_AXIS_LOCK_DOMINANCE_RATIO = 1.25;
25
+ const TOUCH_AXIS_BREAKOUT_DISTANCE = 32;
26
+ const TOUCH_AXIS_BREAKOUT_RATIO = 0.72;
27
+ const TOUCH_AXIS_BREAKOUT_STEP_THRESHOLD = 8;
28
+
29
+ interface PendingPointerMove {
30
+ handle: bigint;
31
+ x: number;
32
+ y: number;
33
+ clientX: number;
34
+ clientY: number;
35
+ pointerInsideCanvas: boolean;
36
+ modifiers: number;
37
+ }
38
+
39
+ function resolvePrimaryTouchAxis(deltaX: number, deltaY: number): 'x' | 'y' {
40
+ const absX = Math.abs(deltaX);
41
+ const absY = Math.abs(deltaY);
42
+ if (absX >= absY * TOUCH_AXIS_LOCK_DOMINANCE_RATIO) {
43
+ return 'x';
44
+ }
45
+ if (absY >= absX * TOUCH_AXIS_LOCK_DOMINANCE_RATIO) {
46
+ return 'y';
47
+ }
48
+ return absX >= absY ? 'x' : 'y';
49
+ }
50
+
51
+ function shouldUnlockTouchAxis(axisMode: 'x' | 'y' | 'xy' | null, travelX: number, travelY: number): boolean {
52
+ if (axisMode === null || axisMode === 'xy') {
53
+ return false;
54
+ }
55
+ const primaryTravel = axisMode === 'x' ? travelX : travelY;
56
+ const secondaryTravel = axisMode === 'x' ? travelY : travelX;
57
+ if (primaryTravel < TOUCH_AXIS_BREAKOUT_DISTANCE || secondaryTravel < TOUCH_AXIS_BREAKOUT_DISTANCE) {
58
+ return false;
59
+ }
60
+ return secondaryTravel >= TOUCH_AXIS_BREAKOUT_DISTANCE &&
61
+ secondaryTravel >= primaryTravel * TOUCH_AXIS_BREAKOUT_RATIO;
62
+ }
63
+
64
+ function currentInteractionTimeMs(): bigint {
65
+ return BigInt(Math.floor(performance.now()));
66
+ }
67
+
68
+ function semanticRoleAtHandle(runtime: BridgeRuntime, handle: bigint): string | null {
69
+ const handleKey = handle.toString();
70
+ const node = runtime.getSemanticTree().find((entry) => entry.handle === handleKey);
71
+ return node?.roleName ?? null;
72
+ }
73
+
74
+ export function installPointerHandlers(
75
+ runtime: BridgeRuntime,
76
+ interactionState: BridgeInteractionState,
77
+ pullToRefresh: PullToRefreshOverlay,
78
+ ): () => void {
79
+ const { canvas, ui } = runtime;
80
+ let primaryPointerDown = false;
81
+ let activePrimaryPointerId: number | null = null;
82
+ let suppressedContextMenuPointerId: number | null = null;
83
+ let edgeAutoScrollTickScheduled = false;
84
+ let activePrimaryPointerType: string | null = null;
85
+ let activeTouchGesture: TouchGestureState | null = null;
86
+ let touchGestureBreakoutTravel = { x: 0.0, y: 0.0 };
87
+
88
+ const applySelectionAutoScroll = (x: number, y: number): void => {
89
+ if (!primaryPointerDown || activePrimaryPointerType === 'touch') {
90
+ return;
91
+ }
92
+ if (handleToBigInt(ui._ui_selection_autoscroll(x, y, EDGE_AUTOSCROLL_THRESHOLD)) === 0n) {
93
+ return;
94
+ }
95
+ runtime.flushPendingCommit();
96
+ runtime.requestFrame();
97
+ };
98
+
99
+ const processPointerMove = (pending: PendingPointerMove): void => {
100
+ interactionState.setPointerInsideCanvas(pending.pointerInsideCanvas);
101
+ interactionState.setLastPointerClientPosition(pending.clientX, pending.clientY);
102
+ interactionState.setLastPointerPosition(pending.x, pending.y);
103
+ interactionState.setLastPointerModifiers(pending.modifiers);
104
+ ui._ui_set_interaction_time(currentInteractionTimeMs());
105
+ ui._ui_on_pointer_event(UI_EVENT_POINTER_MOVE, pending.handle, pending.x, pending.y);
106
+ commitIfVisualWork(runtime);
107
+ if (pending.handle === 0n) {
108
+ const appCapturedHandle = runtime.getCapturedPointerHandle();
109
+ if (appCapturedHandle !== null) {
110
+ window.__effindomCallbacks?.onPointerEventWithCoords?.(
111
+ UI_EVENT_POINTER_MOVE,
112
+ appCapturedHandle,
113
+ pending.x,
114
+ pending.y,
115
+ pending.modifiers,
116
+ );
117
+ }
118
+ }
119
+ applySelectionAutoScroll(pending.x, pending.y);
120
+ scheduleEdgeAutoScrollTick();
121
+ };
122
+ const pointerMoveCoalescer = new PointerMoveCoalescer<PendingPointerMove>(processPointerMove);
123
+
124
+ const scheduleEdgeAutoScrollTick = (): void => {
125
+ if (edgeAutoScrollTickScheduled || !primaryPointerDown || activePrimaryPointerType === 'touch') {
126
+ return;
127
+ }
128
+ edgeAutoScrollTickScheduled = true;
129
+ requestAnimationFrame(() => {
130
+ edgeAutoScrollTickScheduled = false;
131
+ if (!primaryPointerDown) {
132
+ return;
133
+ }
134
+ const { x, y } = interactionState.getLastPointerPosition();
135
+ if (handleToBigInt(ui._ui_selection_autoscroll(x, y, EDGE_AUTOSCROLL_THRESHOLD)) === 0n) {
136
+ return;
137
+ }
138
+ runtime.commitFrame();
139
+ runtime.flushPendingCommit();
140
+ runtime.requestFrame();
141
+ scheduleEdgeAutoScrollTick();
142
+ });
143
+ };
144
+
145
+ const releaseCanvasPointerCapture = (pointerId: number): void => {
146
+ if (canvas.hasPointerCapture(pointerId)) {
147
+ canvas.releasePointerCapture(pointerId);
148
+ }
149
+ };
150
+
151
+ const captureCanvasPointer = (pointerId: number): void => {
152
+ try {
153
+ canvas.setPointerCapture(pointerId);
154
+ } catch {
155
+ // Synthetic touch events in tests and some browser/device combinations can reject explicit capture.
156
+ }
157
+ };
158
+
159
+ const cancelPressedPointerInteraction = (x: number, y: number): void => {
160
+ const capturedHandle = interactionState.getCapturedPointerHandle();
161
+ interactionState.setCapturedPointerHandle(null);
162
+ primaryPointerDown = false;
163
+ activePrimaryPointerId = null;
164
+ activePrimaryPointerType = null;
165
+ pointerMoveCoalescer.clear();
166
+ ui._ui_set_interaction_time(currentInteractionTimeMs());
167
+ ui._ui_on_pointer_event(UI_EVENT_POINTER_LEAVE, capturedHandle ?? 0n, x, y);
168
+ runtime.commitFrame();
169
+ };
170
+
171
+ const handleTouchPointerScroll = (
172
+ event: PointerEvent,
173
+ position: { readonly x: number; readonly y: number },
174
+ modifiers: number,
175
+ ): boolean => {
176
+ if (event.pointerType !== 'touch' || activeTouchGesture?.pointerId !== event.pointerId) {
177
+ return false;
178
+ }
179
+
180
+ const deltaFromStartX = position.x - activeTouchGesture.startX;
181
+ const deltaFromStartY = position.y - activeTouchGesture.startY;
182
+ const distanceSquared = (deltaFromStartX * deltaFromStartX) + (deltaFromStartY * deltaFromStartY);
183
+
184
+ if (activeTouchGesture.phase === 'pressed') {
185
+ if (distanceSquared < TOUCH_SCROLL_THRESHOLD * TOUCH_SCROLL_THRESHOLD) {
186
+ event.preventDefault();
187
+ return true;
188
+ }
189
+ const primaryAxis = resolvePrimaryTouchAxis(deltaFromStartX, deltaFromStartY);
190
+ activeTouchGesture = transitionTouchGesture(activeTouchGesture, {
191
+ type: 'scroll-threshold-crossed',
192
+ axis: primaryAxis,
193
+ });
194
+ if (activeTouchGesture === null) {
195
+ return true;
196
+ }
197
+ interactionState.cancelTouchTextFocusDeferral();
198
+ cancelPressedPointerInteraction(position.x, position.y);
199
+ ui._ui_touch_scroll_begin(
200
+ runtime.getHandleFromPoint(activeTouchGesture.startX, activeTouchGesture.startY),
201
+ activeTouchGesture.startX,
202
+ activeTouchGesture.startY,
203
+ );
204
+ touchGestureBreakoutTravel = { x: 0.0, y: 0.0 };
205
+ }
206
+
207
+ const prevState = activeTouchGesture;
208
+ activeTouchGesture = transitionTouchGesture(activeTouchGesture, {
209
+ type: 'move',
210
+ x: position.x,
211
+ y: position.y,
212
+ });
213
+ if (activeTouchGesture === null) {
214
+ return true;
215
+ }
216
+
217
+ const deltaX = prevState.lastX - position.x;
218
+ const deltaY = prevState.lastY - position.y;
219
+ const absDeltaX = Math.abs(deltaX);
220
+ const absDeltaY = Math.abs(deltaY);
221
+
222
+ if (absDeltaX >= TOUCH_AXIS_BREAKOUT_STEP_THRESHOLD && absDeltaY >= TOUCH_AXIS_BREAKOUT_STEP_THRESHOLD) {
223
+ touchGestureBreakoutTravel.x += absDeltaX;
224
+ touchGestureBreakoutTravel.y += absDeltaY;
225
+ } else {
226
+ touchGestureBreakoutTravel = { x: 0.0, y: 0.0 };
227
+ }
228
+
229
+ if (shouldUnlockTouchAxis(
230
+ activeTouchGesture.axisMode,
231
+ touchGestureBreakoutTravel.x,
232
+ touchGestureBreakoutTravel.y,
233
+ )) {
234
+ activeTouchGesture = transitionTouchGesture(activeTouchGesture, {
235
+ type: 'axis-unlocked',
236
+ });
237
+ if (activeTouchGesture === null) {
238
+ return true;
239
+ }
240
+ touchGestureBreakoutTravel = { x: 0.0, y: 0.0 };
241
+ }
242
+
243
+ const scrollDeltaX = activeTouchGesture.axisMode === 'x' || activeTouchGesture.axisMode === 'xy' ? deltaX : 0.0;
244
+ const scrollDeltaY = activeTouchGesture.axisMode === 'y' || activeTouchGesture.axisMode === 'xy' ? deltaY : 0.0;
245
+
246
+ if (!activeTouchGesture.startedOnTextbox && !activeTouchGesture.pullToRefreshCaptured) {
247
+ const canCapturePullToRefresh =
248
+ activeTouchGesture.pullToRefreshEligible &&
249
+ deltaFromStartY > 0.0 &&
250
+ ui._ui_touch_scroll_allows_pull_to_refresh() !== 0;
251
+ if (canCapturePullToRefresh) {
252
+ activeTouchGesture = transitionTouchGesture(activeTouchGesture, {
253
+ type: 'pull-to-refresh-captured',
254
+ }) ?? activeTouchGesture;
255
+ }
256
+ }
257
+
258
+ const appliedScrollDeltaY = activeTouchGesture.pullToRefreshCaptured ? 0.0 : scrollDeltaY;
259
+
260
+ interactionState.setPointerInsideCanvas(isPointerInsideCanvas(canvas, event));
261
+ interactionState.setLastPointerClientPosition(event.clientX, event.clientY);
262
+ interactionState.setLastPointerPosition(position.x, position.y);
263
+ interactionState.setLastPointerModifiers(modifiers);
264
+ ui._ui_set_interaction_time(currentInteractionTimeMs());
265
+ ui._ui_on_pointer_event(UI_EVENT_POINTER_MOVE, runtime.getHandleFromPoint(position.x, position.y), position.x, position.y);
266
+ ui._ui_touch_scroll_update(scrollDeltaX, appliedScrollDeltaY);
267
+
268
+ const pullToRefreshDistance = activeTouchGesture.pullToRefreshCaptured
269
+ ? Math.max(0.0, deltaFromStartY)
270
+ : 0.0;
271
+
272
+ activeTouchGesture.pullToRefreshDistance = pullToRefreshDistance;
273
+
274
+ if (activeTouchGesture.pullToRefreshCaptured) {
275
+ pullToRefresh.show(pullToRefreshDistance);
276
+ } else {
277
+ pullToRefresh.hide();
278
+ }
279
+ runtime.commitFrame();
280
+ event.preventDefault();
281
+ return true;
282
+ };
283
+
284
+ const forwardPointerEvent = (type: number, useHitTest = true) => (event: PointerEvent): void => {
285
+ const modifiers = computeModifiers(event);
286
+ const pointerInsideCanvas = type === UI_EVENT_POINTER_LEAVE ? false : isPointerInsideCanvas(canvas, event);
287
+ const position = getPointerPosition(canvas, event);
288
+ if (event.pointerType === 'touch' && event.cancelable) {
289
+ event.preventDefault();
290
+ }
291
+ if (type === UI_EVENT_POINTER_DOWN && event.button === 2) {
292
+ window.__effindomCallbacks?.onBeforeContextMenuHitTest?.();
293
+ const handle = runtime.getHandleFromPoint(position.x, position.y);
294
+ const activeTextHandle = interactionState.getActiveTextHandle();
295
+ const refocusActiveTextInputAfterContextMenu =
296
+ activeTextHandle !== null &&
297
+ handle === activeTextHandle;
298
+ suppressedContextMenuPointerId = event.pointerId;
299
+ interactionState.setPointerInsideCanvas(pointerInsideCanvas);
300
+ interactionState.setLastPointerClientPosition(event.clientX, event.clientY);
301
+ interactionState.setLastPointerPosition(position.x, position.y);
302
+ interactionState.setLastPointerModifiers(modifiers);
303
+ if (!refocusActiveTextInputAfterContextMenu) {
304
+ canvas.focus({ preventScroll: true });
305
+ }
306
+ ui._ui_set_interaction_time(currentInteractionTimeMs());
307
+ window.__effindomCallbacks?.onContextMenu?.(
308
+ handle,
309
+ position.x,
310
+ position.y,
311
+ );
312
+ if (refocusActiveTextInputAfterContextMenu) {
313
+ interactionState.refocusActiveTextInput();
314
+ }
315
+ event.preventDefault();
316
+ return;
317
+ }
318
+ if (suppressedContextMenuPointerId === event.pointerId) {
319
+ interactionState.setPointerInsideCanvas(pointerInsideCanvas);
320
+ interactionState.setLastPointerClientPosition(event.clientX, event.clientY);
321
+ interactionState.setLastPointerPosition(position.x, position.y);
322
+ interactionState.setLastPointerModifiers(modifiers);
323
+ if (type === UI_EVENT_POINTER_UP || type === UI_EVENT_POINTER_LEAVE) {
324
+ suppressedContextMenuPointerId = null;
325
+ }
326
+ event.preventDefault();
327
+ return;
328
+ }
329
+ const isTouchEvent = event.pointerType === 'touch';
330
+ const isPointerCancel = event.type === 'pointercancel';
331
+ let touchTapCandidateHandle: bigint | null = null;
332
+ let touchTapDiscarded = false;
333
+
334
+ if (type === UI_EVENT_POINTER_DOWN) {
335
+ captureCanvasPointer(event.pointerId);
336
+ primaryPointerDown = true;
337
+ activePrimaryPointerId = event.pointerId;
338
+ activePrimaryPointerType = event.pointerType;
339
+ if (isTouchEvent) {
340
+ ui._ui_clear_momentum_scroll();
341
+
342
+ activeTouchGesture = transitionTouchGesture(null, {
343
+ type: 'press-start',
344
+ pointerId: event.pointerId,
345
+ x: position.x,
346
+ y: position.y,
347
+ startedOnTextbox: false,
348
+ pendingTextHandle: null,
349
+ });
350
+ touchGestureBreakoutTravel = { x: 0.0, y: 0.0 };
351
+ }
352
+ } else if (activeTouchGesture !== null && activeTouchGesture.pointerId === event.pointerId) {
353
+ if (type === UI_EVENT_POINTER_MOVE && handleTouchPointerScroll(event, position, modifiers)) {
354
+ return;
355
+ }
356
+ if (type === UI_EVENT_POINTER_UP || type === UI_EVENT_POINTER_LEAVE) {
357
+ const pendingTapTextHandle = activeTouchGesture.pendingTapTextHandle;
358
+ const wasCancelled = isPointerCancel || type === UI_EVENT_POINTER_LEAVE;
359
+ const scrolling = activeTouchGesture.phase === 'scrolling';
360
+ const pullToRefreshDistance = activeTouchGesture.pullToRefreshDistance;
361
+
362
+ activeTouchGesture = transitionTouchGesture(activeTouchGesture, {
363
+ type: wasCancelled ? 'cancel' : 'ended',
364
+ triggered: false,
365
+ });
366
+
367
+ const triggerRefresh = scrolling && pullToRefreshDistance >= PULL_TO_REFRESH_THRESHOLD;
368
+ if (scrolling) {
369
+ interactionState.cancelTouchTextFocusDeferral();
370
+ interactionState.setCapturedPointerHandle(null);
371
+ primaryPointerDown = false;
372
+ activePrimaryPointerId = null;
373
+ activePrimaryPointerType = null;
374
+ pointerMoveCoalescer.clear();
375
+ ui._ui_touch_scroll_end();
376
+ if (triggerRefresh) {
377
+ pullToRefresh.hide(true);
378
+ window.location.reload();
379
+ } else {
380
+ pullToRefresh.hide();
381
+ }
382
+ releaseCanvasPointerCapture(event.pointerId);
383
+ event.preventDefault();
384
+ return;
385
+ }
386
+ if (wasCancelled || type !== UI_EVENT_POINTER_UP) {
387
+ interactionState.cancelTouchTextFocusDeferral();
388
+ touchTapDiscarded = true;
389
+ } else {
390
+ touchTapCandidateHandle = pendingTapTextHandle;
391
+ }
392
+ pullToRefresh.hide(true);
393
+ }
394
+ }
395
+ const capturedHandle = interactionState.getCapturedPointerHandle();
396
+ const activeTextHandle = interactionState.getActiveTextHandle();
397
+ const rawHitHandle = useHitTest ? runtime.getHandleFromPoint(position.x, position.y) : 0n;
398
+ const semanticTextboxHandle =
399
+ (type === UI_EVENT_POINTER_DOWN || activeTextHandle !== null) && useHitTest
400
+ ? findSemanticTextboxHandleAtPoint(runtime, position.x, position.y)
401
+ : 0n;
402
+ const shouldPreferSemanticTextbox =
403
+ semanticTextboxHandle !== 0n &&
404
+ (rawHitHandle === 0n || semanticRoleAtHandle(runtime, rawHitHandle) !== 'textbox') &&
405
+ (type === UI_EVENT_POINTER_DOWN ||
406
+ semanticTextboxHandle === activeTextHandle ||
407
+ semanticTextboxHandle === capturedHandle);
408
+ const hitHandle = shouldPreferSemanticTextbox ? semanticTextboxHandle : rawHitHandle;
409
+ const handle = type === UI_EVENT_POINTER_DOWN
410
+ ? hitHandle
411
+ : ((useHitTest && pointerInsideCanvas) ? hitHandle : (capturedHandle ?? hitHandle));
412
+ const refocusActiveTextInputAfterPointerDown =
413
+ type === UI_EVENT_POINTER_DOWN &&
414
+ activeTextHandle !== null &&
415
+ handle === activeTextHandle &&
416
+ !isTouchEvent;
417
+ const delayCanvasFocusUntilAfterPointerDown =
418
+ type === UI_EVENT_POINTER_DOWN &&
419
+ activeTextHandle !== null &&
420
+ handle !== activeTextHandle;
421
+ const keepTouchEditorFocusedOnPointerDown =
422
+ type === UI_EVENT_POINTER_DOWN &&
423
+ isTouchEvent &&
424
+ activeTextHandle !== null &&
425
+ handle === activeTextHandle &&
426
+ interactionState.isActiveTextInputFocused();
427
+ const refocusActiveTextInputAfterPointerUp =
428
+ type === UI_EVENT_POINTER_UP &&
429
+ activeTextHandle !== null &&
430
+ handle === activeTextHandle &&
431
+ !isTouchEvent;
432
+ const shouldCommitDeferredTouchFocus =
433
+ isTouchEvent &&
434
+ type === UI_EVENT_POINTER_UP &&
435
+ !touchTapDiscarded &&
436
+ touchTapCandidateHandle !== null &&
437
+ handle === touchTapCandidateHandle;
438
+ if (type === UI_EVENT_POINTER_UP || type === UI_EVENT_POINTER_LEAVE) {
439
+ const pending = pointerMoveCoalescer.takePending();
440
+ if (pending !== null) {
441
+ processPointerMove(pending);
442
+ }
443
+ }
444
+ if (type === UI_EVENT_POINTER_DOWN) {
445
+ if (isTouchEvent) {
446
+ const touchDownTextboxHandle = semanticRoleAtHandle(runtime, handle) === 'textbox' ? handle : 0n;
447
+ if (touchDownTextboxHandle !== 0n) {
448
+ const touchDownOnAlreadyFocusedText =
449
+ activeTextHandle !== null &&
450
+ touchDownTextboxHandle === activeTextHandle &&
451
+ interactionState.isActiveTextInputFocused();
452
+ if (activeTouchGesture !== null) {
453
+ activeTouchGesture.startedOnTextbox = true;
454
+ activeTouchGesture.pendingTapTextHandle = touchDownOnAlreadyFocusedText ? null : touchDownTextboxHandle;
455
+ }
456
+ interactionState.beginTouchTextFocusDeferral(touchDownTextboxHandle);
457
+ } else {
458
+ interactionState.cancelTouchTextFocusDeferral();
459
+ }
460
+ }
461
+ interactionState.setPointerInsideCanvas(pointerInsideCanvas);
462
+ interactionState.setLastPointerClientPosition(event.clientX, event.clientY);
463
+ interactionState.setLastPointerPosition(position.x, position.y);
464
+ interactionState.setLastPointerModifiers(modifiers);
465
+ if (!refocusActiveTextInputAfterPointerDown &&
466
+ !delayCanvasFocusUntilAfterPointerDown &&
467
+ !keepTouchEditorFocusedOnPointerDown) {
468
+ canvas.focus({ preventScroll: true });
469
+ }
470
+ interactionState.setCapturedPointerHandle(handle === 0n ? null : handle);
471
+ ui._ui_set_interaction_time(currentInteractionTimeMs());
472
+ ui._ui_on_pointer_event(type, handle, position.x, position.y);
473
+ runtime.commitFrame();
474
+ if (delayCanvasFocusUntilAfterPointerDown && interactionState.getActiveTextHandle() === null) {
475
+ canvas.focus({ preventScroll: true });
476
+ }
477
+ if (refocusActiveTextInputAfterPointerDown) {
478
+ interactionState.refocusActiveTextInput();
479
+ } else if (!isTouchEvent && interactionState.getActiveTextHandle() !== null && !interactionState.isActiveTextInputFocused()) {
480
+ interactionState.refocusActiveTextInput();
481
+ }
482
+ scheduleEdgeAutoScrollTick();
483
+ } else if (type === UI_EVENT_POINTER_MOVE) {
484
+ pointerMoveCoalescer.enqueue({
485
+ handle,
486
+ x: position.x,
487
+ y: position.y,
488
+ clientX: event.clientX,
489
+ clientY: event.clientY,
490
+ pointerInsideCanvas,
491
+ modifiers,
492
+ });
493
+ return;
494
+ } else {
495
+ interactionState.setPointerInsideCanvas(pointerInsideCanvas);
496
+ interactionState.setLastPointerClientPosition(event.clientX, event.clientY);
497
+ interactionState.setLastPointerPosition(position.x, position.y);
498
+ interactionState.setLastPointerModifiers(modifiers);
499
+ ui._ui_set_interaction_time(currentInteractionTimeMs());
500
+ ui._ui_on_pointer_event(type, handle, position.x, position.y);
501
+ runtime.commitFrame();
502
+ if (shouldCommitDeferredTouchFocus) {
503
+ interactionState.commitTouchTextFocusDeferral(handle);
504
+ } else if (refocusActiveTextInputAfterPointerUp) {
505
+ interactionState.refocusActiveTextInput();
506
+ } else if (!isTouchEvent && interactionState.getActiveTextHandle() !== null && !interactionState.isActiveTextInputFocused()) {
507
+ interactionState.refocusActiveTextInput();
508
+ }
509
+ if (isTouchEvent && type === UI_EVENT_POINTER_UP && !shouldCommitDeferredTouchFocus) {
510
+ interactionState.cancelTouchTextFocusDeferral();
511
+ }
512
+ if (handle === 0n) {
513
+ const appCapturedHandle = runtime.getCapturedPointerHandle();
514
+ if (appCapturedHandle !== null) {
515
+ window.__effindomCallbacks?.onPointerEventWithCoords?.(type, appCapturedHandle, position.x, position.y, modifiers);
516
+ }
517
+ }
518
+ scheduleEdgeAutoScrollTick();
519
+ }
520
+ if (type === UI_EVENT_POINTER_UP || type === UI_EVENT_POINTER_LEAVE) {
521
+ primaryPointerDown = false;
522
+ activePrimaryPointerId = null;
523
+ activePrimaryPointerType = null;
524
+ interactionState.setCapturedPointerHandle(null);
525
+ }
526
+ if (type === UI_EVENT_POINTER_UP || type === UI_EVENT_POINTER_LEAVE) {
527
+ releaseCanvasPointerCapture(event.pointerId);
528
+ }
529
+ };
530
+
531
+ const handleContextMenu = (event: Event): void => {
532
+ event.preventDefault();
533
+ };
534
+ const handlePointerDown = forwardPointerEvent(UI_EVENT_POINTER_DOWN);
535
+ const handlePointerUp = forwardPointerEvent(UI_EVENT_POINTER_UP);
536
+ const handlePointerMove = forwardPointerEvent(UI_EVENT_POINTER_MOVE);
537
+ const handleCapturedPointerExit = (event: PointerEvent): void => {
538
+ if (canvas.hasPointerCapture(event.pointerId)) {
539
+ if (primaryPointerDown) {
540
+ forwardPointerEvent(UI_EVENT_POINTER_MOVE, false)(event);
541
+ }
542
+ return;
543
+ }
544
+ forwardPointerEvent(UI_EVENT_POINTER_LEAVE, false)(event);
545
+ };
546
+ const handlePointerLeave = (event: PointerEvent): void => {
547
+ handleCapturedPointerExit(event);
548
+ };
549
+ const handlePointerOut = (event: PointerEvent): void => {
550
+ handleCapturedPointerExit(event);
551
+ };
552
+ const handlePointerCancel = (event: PointerEvent): void => {
553
+ forwardPointerEvent(UI_EVENT_POINTER_LEAVE, false)(event);
554
+ };
555
+ const shouldHandleWindowPointerEvent = (event: PointerEvent): boolean => {
556
+ return primaryPointerDown &&
557
+ activePrimaryPointerId !== null &&
558
+ event.pointerId === activePrimaryPointerId &&
559
+ event.target !== canvas;
560
+ };
561
+ const handleWindowPointerMove = (event: PointerEvent): void => {
562
+ if (!shouldHandleWindowPointerEvent(event)) {
563
+ return;
564
+ }
565
+ handlePointerMove(event);
566
+ };
567
+ const handleWindowPointerUp = (event: PointerEvent): void => {
568
+ if (!shouldHandleWindowPointerEvent(event)) {
569
+ return;
570
+ }
571
+ handlePointerUp(event);
572
+ };
573
+ const handleWindowPointerCancel = (event: PointerEvent): void => {
574
+ if (!shouldHandleWindowPointerEvent(event)) {
575
+ return;
576
+ }
577
+ handlePointerCancel(event);
578
+ };
579
+ const handleWheel = (event: WheelEvent): void => {
580
+ event.preventDefault();
581
+ const position = getPointerPosition(canvas, event);
582
+ interactionState.setPointerInsideCanvas(isPointerInsideCanvas(canvas, event));
583
+ interactionState.setLastPointerClientPosition(event.clientX, event.clientY);
584
+ interactionState.setLastPointerPosition(position.x, position.y);
585
+ interactionState.setLastPointerModifiers(computeModifiers(event));
586
+ ui._ui_set_interaction_time(currentInteractionTimeMs());
587
+ const handle = runtime.getHandleFromPoint(position.x, position.y);
588
+ ui._ui_on_pointer_event(UI_EVENT_POINTER_MOVE, handle, position.x, position.y);
589
+ const delta = normalizeWheelDelta(event, canvas);
590
+ ui._ui_on_wheel_event(delta.x, delta.y);
591
+ commitIfVisualWork(runtime);
592
+ };
593
+
594
+ canvas.addEventListener('contextmenu', handleContextMenu);
595
+ canvas.addEventListener('pointerdown', handlePointerDown, { passive: false });
596
+ canvas.addEventListener('pointerup', handlePointerUp, { passive: false });
597
+ canvas.addEventListener('pointermove', handlePointerMove, { passive: false });
598
+ canvas.addEventListener('pointerleave', handlePointerLeave, { passive: false });
599
+ canvas.addEventListener('pointerout', handlePointerOut, { passive: false });
600
+ canvas.addEventListener('pointercancel', handlePointerCancel, { passive: false });
601
+ window.addEventListener('pointermove', handleWindowPointerMove, { passive: false });
602
+ window.addEventListener('pointerup', handleWindowPointerUp, { passive: false });
603
+ window.addEventListener('pointercancel', handleWindowPointerCancel, { passive: false });
604
+ canvas.addEventListener('wheel', handleWheel, { passive: false });
605
+
606
+ return () => {
607
+ canvas.removeEventListener('contextmenu', handleContextMenu);
608
+ canvas.removeEventListener('pointerdown', handlePointerDown);
609
+ canvas.removeEventListener('pointerup', handlePointerUp);
610
+ canvas.removeEventListener('pointermove', handlePointerMove);
611
+ canvas.removeEventListener('pointerleave', handlePointerLeave);
612
+ canvas.removeEventListener('pointerout', handlePointerOut);
613
+ canvas.removeEventListener('pointercancel', handlePointerCancel);
614
+ window.removeEventListener('pointermove', handleWindowPointerMove);
615
+ window.removeEventListener('pointerup', handleWindowPointerUp);
616
+ window.removeEventListener('pointercancel', handleWindowPointerCancel);
617
+ canvas.removeEventListener('wheel', handleWheel);
618
+ };
619
+ }
@@ -0,0 +1,27 @@
1
+ import type { BridgeRuntime } from '../../core-types';
2
+ import { handleToBigInt } from '../utils/encoding';
3
+
4
+ function pointInSemanticBounds(
5
+ bounds: { readonly x: number; readonly y: number; readonly width: number; readonly height: number },
6
+ x: number,
7
+ y: number,
8
+ ): boolean {
9
+ return x >= bounds.x &&
10
+ x <= (bounds.x + bounds.width) &&
11
+ y >= bounds.y &&
12
+ y <= (bounds.y + bounds.height);
13
+ }
14
+
15
+ export function findSemanticTextboxHandleAtPoint(runtime: BridgeRuntime, x: number, y: number): bigint {
16
+ const tree = runtime.getSemanticTree();
17
+ for (let i = tree.length - 1; i >= 0; i -= 1) {
18
+ const node = tree[i];
19
+ if (node?.roleName !== 'textbox') {
20
+ continue;
21
+ }
22
+ if (pointInSemanticBounds(node.bounds, x, y)) {
23
+ return handleToBigInt(node.handle);
24
+ }
25
+ }
26
+ return 0n;
27
+ }