@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,315 @@
1
+ import type {
2
+ BridgeFontRegistration,
3
+ BridgeFontStackRegistration,
4
+ BridgeLoaderInfo,
5
+ BridgeRuntime,
6
+ CoreModule,
7
+ UiModule,
8
+ } from '../core-types';
9
+ import type { BridgeInteractionState } from './local-types';
10
+ import { resetBridgeLogs } from './utils/assets';
11
+ import { normalizeBackendType, handleToBigInt } from './utils/encoding';
12
+ import { executeCommandBuffer, extractCommandBuffer } from './utils/heap';
13
+ import { setActiveRenderer } from './utils/backends';
14
+ import { getCanvasSizeSource, readCanvasLogicalSize } from './events';
15
+ import { AssetManager } from './runtime/asset-manager';
16
+ import { IncrementalFontManager } from './runtime/font-manager';
17
+ import { FindController } from './runtime/find-controller';
18
+ import { OpenCanvasApiAdapter } from './runtime/open-canvas-api';
19
+ import { SemanticController } from './runtime/semantic-controller';
20
+ import { TextDocumentController } from './runtime/text-documents';
21
+
22
+ const DEFAULT_LOGICAL_WIDTH = 320;
23
+ const DEFAULT_LOGICAL_HEIGHT = 220;
24
+ const UI_EVENT_POINTER_ENTER = 4;
25
+ const UI_EVENT_POINTER_LEAVE = 5;
26
+
27
+ export function createBridgeRuntime(
28
+ core: CoreModule,
29
+ ui: UiModule,
30
+ canvas: HTMLCanvasElement,
31
+ interactionState: BridgeInteractionState,
32
+ loaderInfo: BridgeLoaderInfo,
33
+ ): { runtime: BridgeRuntime; destroy(): void } {
34
+ let logicalWidth = DEFAULT_LOGICAL_WIDTH;
35
+ let logicalHeight = DEFAULT_LOGICAL_HEIGHT;
36
+ let needsCommit = false;
37
+ let appFrameHandler: ((timestampMs: number) => void) | null = null;
38
+ let frameRequester: (() => void) | null = null;
39
+ let runtime!: BridgeRuntime;
40
+
41
+ const fontManager = new IncrementalFontManager(core, ui, interactionState.logs, () => runtime.commitFrame());
42
+ const assetManager = new AssetManager(core, fontManager, () => runtime.commitFrame());
43
+ const textDocuments = new TextDocumentController(ui);
44
+ const semanticController = new SemanticController(canvas, ui, interactionState, textDocuments);
45
+ const findController = new FindController({
46
+ canvas,
47
+ ui,
48
+ textDocuments,
49
+ commitFrame: () => runtime.commitFrame(),
50
+ flushPendingCommit: () => runtime.flushPendingCommit(),
51
+ });
52
+ const openCanvasApiAdapter = new OpenCanvasApiAdapter({
53
+ ui,
54
+ semantic: semanticController,
55
+ find: findController,
56
+ textDocuments,
57
+ commitFrame: () => runtime.commitFrame(),
58
+ flushPendingCommit: () => runtime.flushPendingCommit(),
59
+ });
60
+
61
+ const syncSemanticAndFindState = (): void => {
62
+ semanticController.syncSemanticState();
63
+ findController.syncDocuments();
64
+ };
65
+
66
+ const dispatchAppPointerEvent = (eventType: number, handle: bigint, x: number, y: number, modifiers = 0): void => {
67
+ if (handle === 0n) {
68
+ return;
69
+ }
70
+ window.__effindomCallbacks?.onPointerEventWithCoords?.(eventType, handle, x, y, modifiers);
71
+ };
72
+
73
+ const isLastPointerStillOverCanvas = (): boolean => {
74
+ const { x, y } = interactionState.getLastPointerClientPosition();
75
+ if (x === null || y === null) {
76
+ return false;
77
+ }
78
+ const rect = canvas.getBoundingClientRect();
79
+ return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
80
+ };
81
+
82
+ const syncAppPointerHover = (): void => {
83
+ const { x, y } = interactionState.getLastPointerPosition();
84
+ const previousHandle = interactionState.getLastInteractivePointerHandle();
85
+ const capturedHandle = interactionState.getCapturedPointerHandle();
86
+ if (capturedHandle !== null) {
87
+ if (previousHandle === capturedHandle) {
88
+ return;
89
+ }
90
+ if (previousHandle !== null) {
91
+ dispatchAppPointerEvent(UI_EVENT_POINTER_LEAVE, previousHandle, x, y);
92
+ }
93
+ interactionState.setLastInteractivePointerHandle(capturedHandle);
94
+ dispatchAppPointerEvent(UI_EVENT_POINTER_ENTER, capturedHandle, x, y);
95
+ return;
96
+ }
97
+ const pointerInsideCanvas = interactionState.isPointerInsideCanvas() || isLastPointerStillOverCanvas();
98
+ if (!pointerInsideCanvas) {
99
+ if (previousHandle !== null) {
100
+ interactionState.setLastInteractivePointerHandle(null);
101
+ dispatchAppPointerEvent(UI_EVENT_POINTER_LEAVE, previousHandle, x, y);
102
+ }
103
+ return;
104
+ }
105
+
106
+ const hitHandle = handleToBigInt(core._ed_hit_test(x, y));
107
+ const currentHandle = hitHandle === 0n ? null : hitHandle;
108
+ if (currentHandle === previousHandle) {
109
+ return;
110
+ }
111
+ if (previousHandle !== null) {
112
+ dispatchAppPointerEvent(UI_EVENT_POINTER_LEAVE, previousHandle, x, y);
113
+ }
114
+ interactionState.setLastInteractivePointerHandle(currentHandle);
115
+ if (currentHandle !== null) {
116
+ dispatchAppPointerEvent(UI_EVENT_POINTER_ENTER, currentHandle, x, y);
117
+ }
118
+ };
119
+
120
+ const updateCanvasSize = (): void => {
121
+ const dpr = Math.max(1, window.devicePixelRatio || 1);
122
+ const size = readCanvasLogicalSize(canvas);
123
+ logicalWidth = size.width;
124
+ logicalHeight = size.height;
125
+ const physicalWidth = Math.round(logicalWidth * dpr);
126
+ const physicalHeight = Math.round(logicalHeight * dpr);
127
+
128
+ canvas.style.width = `${String(logicalWidth)}px`;
129
+ canvas.style.height = `${String(logicalHeight)}px`;
130
+ if (physicalWidth !== canvas.width || physicalHeight !== canvas.height) {
131
+ canvas.width = physicalWidth;
132
+ canvas.height = physicalHeight;
133
+ }
134
+
135
+ semanticController.syncSize(logicalWidth, logicalHeight);
136
+ findController.syncSize(logicalWidth, logicalHeight);
137
+ core._ed_resize(physicalWidth, physicalHeight, dpr);
138
+ ui._ui_resize_window(logicalWidth, logicalHeight);
139
+ syncSemanticAndFindState();
140
+ setActiveRenderer(loaderInfo, normalizeBackendType(core._ed_get_backend_type()));
141
+ };
142
+
143
+ runtime = {
144
+ core,
145
+ ui,
146
+ canvas,
147
+ openCanvasApi: openCanvasApiAdapter.getApi(),
148
+ logs: interactionState.logs,
149
+ updateCanvasSize,
150
+ extractCommandBuffer: () => extractCommandBuffer(ui),
151
+ executeCommandBuffer: (words: Uint32Array) => {
152
+ executeCommandBuffer(core, words);
153
+ },
154
+ syncCommandBufferToCore: () => {
155
+ const words = extractCommandBuffer(ui);
156
+ executeCommandBuffer(core, words);
157
+ syncSemanticAndFindState();
158
+ syncAppPointerHover();
159
+ return words;
160
+ },
161
+ flushPendingCommit: () => {
162
+ if (!needsCommit && !interactionState.hasPendingTextMutations()) {
163
+ return null;
164
+ }
165
+ if (interactionState.hasPendingTextMutations()) {
166
+ if (needsCommit) {
167
+ needsCommit = false;
168
+ runtime.syncCommandBufferToCore();
169
+ }
170
+ if (interactionState.materializePendingTextMutations()) {
171
+ ui._ui_commit_frame();
172
+ needsCommit = true;
173
+ }
174
+ }
175
+ if (!needsCommit) {
176
+ return null;
177
+ }
178
+ needsCommit = false;
179
+ return runtime.syncCommandBufferToCore();
180
+ },
181
+ hasPendingCommit: () => needsCommit || interactionState.hasPendingTextMutations(),
182
+ commitFrame: () => {
183
+ if (interactionState.hasPendingTextMutations()) {
184
+ if (needsCommit) {
185
+ runtime.flushPendingCommit();
186
+ }
187
+ if (interactionState.materializePendingTextMutations()) {
188
+ ui._ui_commit_frame();
189
+ needsCommit = true;
190
+ frameRequester?.();
191
+ return;
192
+ }
193
+ }
194
+ if (needsCommit) {
195
+ runtime.flushPendingCommit();
196
+ }
197
+ ui._ui_commit_frame();
198
+ needsCommit = true;
199
+ frameRequester?.();
200
+ },
201
+ requestFrame: () => {
202
+ frameRequester?.();
203
+ },
204
+ setFrameRequester: (requester: (() => void) | null) => {
205
+ frameRequester = requester;
206
+ },
207
+ getSemanticTree: () => semanticController.getSemanticTree(),
208
+ getActiveTextHandle: () => interactionState.getActiveTextHandle(),
209
+ getCapturedPointerHandle: () => interactionState.getCapturedPointerHandle(),
210
+ setCapturedPointerHandle: (handle: bigint | null) => {
211
+ interactionState.setCapturedPointerHandle(handle);
212
+ },
213
+ setAppFrameHandler: (handler: ((timestampMs: number) => void) | null) => {
214
+ appFrameHandler = handler;
215
+ frameRequester?.();
216
+ },
217
+ runAppFrameHandler: (timestampMs: number) => {
218
+ appFrameHandler?.(timestampMs);
219
+ },
220
+ uiHasPendingVisualWork: () => ui._ui_has_pending_visual_work() !== 0,
221
+ uiNeedsAnimationFrame: () => ui._ui_needs_animation_frame() !== 0,
222
+ getHandleFromPoint: (x: number, y: number) => handleToBigInt(core._ed_hit_test(x, y)),
223
+ clearPointerHover: () => {
224
+ interactionState.setLastInteractivePointerHandle(null);
225
+ },
226
+ refreshPointerHover: () => {
227
+ syncAppPointerHover();
228
+ },
229
+ getFindDocuments: () => findController.getFindDocuments(),
230
+ activateFindMatch: (match, reveal = true) => findController.activateFindMatch(match, reveal),
231
+ syncFindSelection: (clearOnMissing = false) => findController.syncFindSelection(clearOnMissing),
232
+ clearFindMatch: () => findController.clearFindMatch(),
233
+ ensureFont: async (fontId: number) => {
234
+ await fontManager.ensureFont(fontId);
235
+ },
236
+ ensureBuiltInFont: async (fontId: number) => {
237
+ await fontManager.ensureBuiltInFont(fontId);
238
+ },
239
+ isFontLoaded: (fontId: number, url?: string) => fontManager.isFontLoaded(fontId, url),
240
+ getIncrementalFontState: (fontId: number) => fontManager.getIncrementalFontState(fontId),
241
+ getIncrementalFontCacheState: () => fontManager.getIncrementalFontCacheState(),
242
+ getIncrementalFontPolicy: () => fontManager.getIncrementalFontPolicy(),
243
+ setIncrementalFontPolicy: (policy) => {
244
+ fontManager.setIncrementalFontPolicy(policy);
245
+ },
246
+ getClipboardFontUrl: (fontId: number) => fontManager.getClipboardFontUrl(fontId),
247
+ registerLazyFont: (fontId: number, url: string) => {
248
+ fontManager.registerLazyFont(fontId, url);
249
+ },
250
+ registerFontFallback: (fontId: number, fallbackFontId: number) => {
251
+ fontManager.registerFontFallback(fontId, fallbackFontId);
252
+ },
253
+ handleMissingFontCoverage: (fontId: number, coverageKind: number, sampleText: string) => {
254
+ fontManager.handleMissingFontCoverage(fontId, coverageKind, sampleText);
255
+ },
256
+ loadFont: async (fontId: number, url: string) => {
257
+ await fontManager.loadFont(fontId, url);
258
+ },
259
+ registerFont: async (font: BridgeFontRegistration) => {
260
+ await fontManager.registerFont(font);
261
+ },
262
+ registerFontStack: async (stack: BridgeFontStackRegistration) => {
263
+ await fontManager.registerFontStack(stack);
264
+ },
265
+ loadSvg: async (svgId: number, url: string) => {
266
+ return await assetManager.loadSvg(svgId, url);
267
+ },
268
+ loadTexture: async (textureId: number, url: string) => {
269
+ return await assetManager.loadTexture(textureId, url);
270
+ },
271
+ releaseSvg: (svgId: number) => {
272
+ assetManager.releaseSvg(svgId);
273
+ },
274
+ releaseTexture: (textureId: number) => {
275
+ assetManager.releaseTexture(textureId);
276
+ },
277
+ replayLoadedAssets: async () => {
278
+ await assetManager.replayLoadedAssets();
279
+ },
280
+ resetLogs: () => {
281
+ resetBridgeLogs(interactionState.logs);
282
+ },
283
+ resetAppSession: () => {
284
+ interactionState.resetAppSession();
285
+ },
286
+ };
287
+
288
+ const refreshCanvas = (): void => {
289
+ runtime.updateCanvasSize();
290
+ runtime.commitFrame();
291
+ };
292
+
293
+ const canvasSizeSource = getCanvasSizeSource(canvas);
294
+ const resizeObserver = typeof ResizeObserver !== 'undefined'
295
+ ? new ResizeObserver(() => {
296
+ refreshCanvas();
297
+ })
298
+ : null;
299
+ if (resizeObserver !== null) {
300
+ resizeObserver.observe(canvasSizeSource);
301
+ }
302
+
303
+ return {
304
+ runtime,
305
+ destroy: () => {
306
+ resizeObserver?.disconnect();
307
+ runtime.setFrameRequester(null);
308
+ runtime.clearPointerHover();
309
+ openCanvasApiAdapter.destroy();
310
+ findController.destroy();
311
+ semanticController.destroy();
312
+ canvas.style.cursor = 'default';
313
+ },
314
+ };
315
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Touch gesture state machine.
3
+ * Models the lifecycle of touch interactions from pointer-down through scroll/tap completion.
4
+ */
5
+
6
+ export type TouchGesturePhase = 'pressed' | 'scrolling';
7
+
8
+ export interface TouchGestureState {
9
+ pointerId: number;
10
+ phase: TouchGesturePhase;
11
+ startX: number;
12
+ startY: number;
13
+ lastX: number;
14
+ lastY: number;
15
+ startedOnTextbox: boolean;
16
+ pendingTapTextHandle: bigint | null;
17
+ axisMode: 'x' | 'y' | 'xy' | null;
18
+ pullToRefreshEligible: boolean;
19
+ pullToRefreshCaptured: boolean;
20
+ pullToRefreshDistance: number;
21
+ // Note: breakoutTravel is tracked separately outside the reducer to avoid churn;
22
+ // see touchGestureBreakoutTravel in events.ts
23
+ }
24
+
25
+ export type TouchGestureEvent =
26
+ | {
27
+ type: 'press-start';
28
+ pointerId: number;
29
+ x: number;
30
+ y: number;
31
+ startedOnTextbox: boolean;
32
+ pendingTextHandle: bigint | null;
33
+ }
34
+ | {
35
+ type: 'move';
36
+ x: number;
37
+ y: number;
38
+ }
39
+ | {
40
+ type: 'scroll-threshold-crossed';
41
+ axis: 'x' | 'y';
42
+ }
43
+ | {
44
+ type: 'axis-unlocked';
45
+ }
46
+ | {
47
+ type: 'pull-to-refresh-captured';
48
+ }
49
+ | {
50
+ type: 'pull-to-refresh-released';
51
+ }
52
+ | {
53
+ type: 'cancel';
54
+ }
55
+ | {
56
+ type: 'ended';
57
+ triggered: boolean;
58
+ };
59
+
60
+ /**
61
+ * Transition reducer for touch gesture state.
62
+ * Returns the new state after applying an event, or null if the gesture ended.
63
+ */
64
+ export function transitionTouchGesture(
65
+ state: TouchGestureState | null,
66
+ event: TouchGestureEvent,
67
+ ): TouchGestureState | null {
68
+ if (state === null) {
69
+ if (event.type === 'press-start') {
70
+ return {
71
+ pointerId: event.pointerId,
72
+ phase: 'pressed',
73
+ startX: event.x,
74
+ startY: event.y,
75
+ lastX: event.x,
76
+ lastY: event.y,
77
+ startedOnTextbox: event.startedOnTextbox,
78
+ pendingTapTextHandle: event.pendingTextHandle,
79
+ axisMode: null,
80
+ pullToRefreshEligible: false,
81
+ pullToRefreshCaptured: false,
82
+ pullToRefreshDistance: 0.0,
83
+ };
84
+ }
85
+ return null;
86
+ }
87
+
88
+ switch (state.phase) {
89
+ case 'pressed': {
90
+ switch (event.type) {
91
+ case 'scroll-threshold-crossed': {
92
+ const primaryAxis = event.axis;
93
+ return {
94
+ ...state,
95
+ phase: 'scrolling',
96
+ axisMode: primaryAxis,
97
+ pullToRefreshEligible: primaryAxis === 'y',
98
+ pendingTapTextHandle: null,
99
+ };
100
+ }
101
+ case 'move': {
102
+ return {
103
+ ...state,
104
+ lastX: event.x,
105
+ lastY: event.y,
106
+ };
107
+ }
108
+ case 'cancel':
109
+ case 'ended': {
110
+ return null;
111
+ }
112
+ default: {
113
+ return state;
114
+ }
115
+ }
116
+ }
117
+
118
+ case 'scrolling': {
119
+ switch (event.type) {
120
+ case 'move': {
121
+ return {
122
+ ...state,
123
+ lastX: event.x,
124
+ lastY: event.y,
125
+ };
126
+ }
127
+ case 'axis-unlocked': {
128
+ return {
129
+ ...state,
130
+ axisMode: 'xy',
131
+ pullToRefreshEligible: false,
132
+ pullToRefreshCaptured: false,
133
+ pullToRefreshDistance: 0.0,
134
+ };
135
+ }
136
+ case 'pull-to-refresh-captured': {
137
+ return {
138
+ ...state,
139
+ pullToRefreshCaptured: true,
140
+ };
141
+ }
142
+ case 'pull-to-refresh-released': {
143
+ return {
144
+ ...state,
145
+ pullToRefreshCaptured: false,
146
+ pullToRefreshDistance: 0.0,
147
+ };
148
+ }
149
+ case 'cancel':
150
+ case 'ended': {
151
+ return null;
152
+ }
153
+ default: {
154
+ return state;
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }