@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,628 @@
1
+ import type {
2
+ BridgeLogs,
3
+ BridgeRuntime,
4
+ FocusEventLog,
5
+ SelectionChangeLog,
6
+ WasmHandleLike,
7
+ } from '../../core-types';
8
+ import type { BridgeInteractionState } from '../local-types';
9
+ import { codeUnitIndexToUtf8ByteOffset, utf8ByteLength } from './text-encoding';
10
+ import {
11
+ applyUtf8ByteReplacementEdit,
12
+ buildClampedTextboxEdit,
13
+ buildHiddenEditorWindow,
14
+ clearHiddenTextEditor,
15
+ createHiddenTextEditor,
16
+ type HiddenEditorWindow,
17
+ type HiddenTextEditor,
18
+ type PendingLocalReplacementEcho,
19
+ type PendingLocalSelectionEcho,
20
+ summarizeTextChange,
21
+ utf8ByteOffsetToCodeUnitIndex,
22
+ } from './editor-model';
23
+ import { createEditorMutationController } from './editor-mutations';
24
+ import { handleToBigInt } from '../utils/encoding';
25
+ import { writeUtf8ToHeap } from '../utils/heap';
26
+
27
+ export interface EditorSession extends BridgeInteractionState {
28
+ handleClipboardRead(handle: WasmHandleLike): void;
29
+ handleFocusChanged(handle: WasmHandleLike, isFocused: boolean): void;
30
+ handleRequestSemanticAnnouncement(handle: WasmHandleLike): void;
31
+ handleSelectionChanged(handle: WasmHandleLike, start: number, end: number): void;
32
+ handleTextChanged(handle: WasmHandleLike, text: string): void;
33
+ handleTextReplaced(handle: WasmHandleLike, start: number, end: number, text: string): void;
34
+ }
35
+
36
+ const CARET_BLINK_INTERVAL_MS = 500;
37
+
38
+ function currentInteractionTimeMs(): bigint {
39
+ return BigInt(Math.floor(performance.now()));
40
+ }
41
+
42
+ export function createEditorSession(
43
+ runtimeRef: { current: BridgeRuntime | null },
44
+ logs: BridgeLogs,
45
+ ): EditorSession {
46
+ const textByHandle = Object.create(null) as Record<string, string>;
47
+ const selectionsByHandle = Object.create(null) as Record<string, { start: number; end: number }>;
48
+ const hiddenInput = createHiddenTextEditor(false) as HTMLInputElement;
49
+ const hiddenTextarea = createHiddenTextEditor(true) as HTMLTextAreaElement;
50
+ let activeTextHandle: bigint | null = null;
51
+ let lastPointerClientX: number | null = null;
52
+ let lastPointerClientY: number | null = null;
53
+ let lastPointerX = 0;
54
+ let lastPointerY = 0;
55
+ let lastPointerModifiers = 0;
56
+ let lastInteractivePointerHandle: bigint | null = null;
57
+ let capturedPointerHandle: bigint | null = null;
58
+ let pointerInsideCanvas = false;
59
+ let appSessionVersion = 0;
60
+ let activeTextEditable = false;
61
+ let activeTextMultiline = false;
62
+ let activeEditorWindow: HiddenEditorWindow = { text: '', docStart: 0, docEnd: 0, textStart: 0, textEnd: 0 };
63
+ const textByteLengthsByHandle = Object.create(null) as Record<string, number>;
64
+ const pendingCaretRevealByHandle = Object.create(null) as Record<string, boolean>;
65
+ let pendingCaretRevealFrame: number | null = null;
66
+ let pendingLocalReplacementEcho: PendingLocalReplacementEcho | null = null;
67
+ let pendingLocalSelectionEcho: PendingLocalSelectionEcho | null = null;
68
+ let deferredTouchFocusHandle: string | null = null;
69
+ let caretBlinkTimer: ReturnType<typeof setTimeout> | null = null;
70
+ let focusedHandle: string | null = null;
71
+ const pendingSemanticAnnouncements = new Set<string>();
72
+ window.__bridgeLogs = logs;
73
+ window.__bridgeTextByHandle = textByHandle;
74
+ window.__bridgeSelectionsByHandle = selectionsByHandle;
75
+ window.__bridgeActiveEditorWindow = { handle: null, ...activeEditorWindow };
76
+
77
+ const getActiveEditor = (): HiddenTextEditor => (activeTextMultiline ? hiddenTextarea : hiddenInput);
78
+
79
+ const isActiveEditorFocused = (): boolean => document.activeElement === getActiveEditor();
80
+
81
+ const queueSemanticAnnouncement = (handleKey: string): void => {
82
+ pendingSemanticAnnouncements.add(handleKey);
83
+ runtimeRef.current?.requestFrame();
84
+ };
85
+
86
+ const clampSelectionToText = (
87
+ length: number,
88
+ selection: { start: number; end: number },
89
+ ): { start: number; end: number } => ({
90
+ start: Math.max(0, Math.min(selection.start, length)),
91
+ end: Math.max(0, Math.min(selection.end, length)),
92
+ });
93
+
94
+ const clearCaretBlinkTimer = (): void => {
95
+ if (caretBlinkTimer !== null) {
96
+ clearTimeout(caretBlinkTimer);
97
+ caretBlinkTimer = null;
98
+ }
99
+ };
100
+
101
+ const shouldRunCaretBlinkTimer = (): boolean => {
102
+ if (activeTextHandle === null || !isActiveEditorFocused()) {
103
+ return false;
104
+ }
105
+ const handleKey = activeTextHandle.toString();
106
+ const text = textByHandle[handleKey] ?? '';
107
+ const textByteLength = textByteLengthsByHandle[handleKey] ?? utf8ByteLength(text);
108
+ const selection = selectionsByHandle[handleKey] ?? { start: textByteLength, end: textByteLength };
109
+ const { start, end } = clampSelectionToText(textByteLength, selection);
110
+ return start === end;
111
+ };
112
+
113
+ const armCaretBlinkTimer = (): void => {
114
+ if (caretBlinkTimer !== null) {
115
+ return;
116
+ }
117
+ caretBlinkTimer = setTimeout(() => {
118
+ caretBlinkTimer = null;
119
+ if (!shouldRunCaretBlinkTimer()) {
120
+ return;
121
+ }
122
+ runtimeRef.current?.requestFrame();
123
+ armCaretBlinkTimer();
124
+ }, CARET_BLINK_INTERVAL_MS);
125
+ };
126
+
127
+ const updateCaretBlinkTimer = (resetPhase = false): void => {
128
+ if (!shouldRunCaretBlinkTimer()) {
129
+ clearCaretBlinkTimer();
130
+ return;
131
+ }
132
+ if (resetPhase) {
133
+ clearCaretBlinkTimer();
134
+ }
135
+ armCaretBlinkTimer();
136
+ };
137
+
138
+ const syncActiveEditorWindowDebug = (handle: string | null): void => {
139
+ window.__bridgeActiveEditorWindow = {
140
+ handle,
141
+ text: activeEditorWindow.text,
142
+ docStart: activeEditorWindow.docStart,
143
+ docEnd: activeEditorWindow.docEnd,
144
+ };
145
+ };
146
+
147
+ const clearActiveEditorWindow = (): void => {
148
+ activeEditorWindow = { text: '', docStart: 0, docEnd: 0, textStart: 0, textEnd: 0 };
149
+ syncActiveEditorWindowDebug(null);
150
+ };
151
+
152
+ const clearPendingCaretReveal = (): void => {
153
+ if (pendingCaretRevealFrame !== null) {
154
+ cancelAnimationFrame(pendingCaretRevealFrame);
155
+ pendingCaretRevealFrame = null;
156
+ }
157
+ for (const key of Object.keys(pendingCaretRevealByHandle)) {
158
+ delete pendingCaretRevealByHandle[key];
159
+ }
160
+ };
161
+
162
+ const updateActiveEditorWindowText = (text: string): void => {
163
+ activeEditorWindow = {
164
+ text,
165
+ docStart: activeEditorWindow.docStart,
166
+ docEnd: activeEditorWindow.docStart + utf8ByteLength(text),
167
+ textStart: activeEditorWindow.textStart,
168
+ textEnd: activeEditorWindow.textStart + text.length,
169
+ };
170
+ syncActiveEditorWindowDebug(activeTextHandle?.toString() ?? null);
171
+ };
172
+
173
+ const detachBridgeTextInput = (): void => {
174
+ hiddenInput.blur();
175
+ hiddenTextarea.blur();
176
+ };
177
+
178
+ const getTextboxState = (
179
+ handleKey: string,
180
+ ): { isTextbox: boolean; isEditable: boolean; isMultiline: boolean } => {
181
+ const runtime = runtimeRef.current;
182
+ if (runtime === null) {
183
+ return { isTextbox: false, isEditable: false, isMultiline: false };
184
+ }
185
+ const node = runtime.getSemanticTree().find((entry) => entry.handle === handleKey);
186
+ if (node?.roleName !== 'textbox') {
187
+ return { isTextbox: false, isEditable: false, isMultiline: false };
188
+ }
189
+ return {
190
+ isTextbox: true,
191
+ isEditable: node.state.readonly !== true,
192
+ isMultiline: node.state.multiline === true,
193
+ };
194
+ };
195
+
196
+ const syncFocusedInputState = (): void => {
197
+ if (activeTextHandle === null) {
198
+ clearCaretBlinkTimer();
199
+ clearActiveEditorWindow();
200
+ detachBridgeTextInput();
201
+ clearHiddenTextEditor(hiddenInput);
202
+ clearHiddenTextEditor(hiddenTextarea);
203
+ return;
204
+ }
205
+
206
+ const activeEditor = getActiveEditor();
207
+ const handleKey = activeTextHandle.toString();
208
+ const text = textByHandle[handleKey] ?? '';
209
+ const textByteLength = textByteLengthsByHandle[handleKey] ?? utf8ByteLength(text);
210
+ textByteLengthsByHandle[handleKey] = textByteLength;
211
+ const selection = selectionsByHandle[handleKey] ?? { start: textByteLength, end: textByteLength };
212
+ const { start: startByte, end: endByte } = clampSelectionToText(textByteLength, selection);
213
+ const start = utf8ByteOffsetToCodeUnitIndex(text, startByte, textByteLength);
214
+ const end = utf8ByteOffsetToCodeUnitIndex(text, endByte, textByteLength);
215
+ const direction = start === end ? 'none' : (startByte < endByte ? 'forward' : 'backward');
216
+ const normalizedStart = Math.min(start, end);
217
+ const normalizedEnd = Math.max(start, end);
218
+ activeEditorWindow = buildHiddenEditorWindow(text, normalizedStart, normalizedEnd, textByteLength, activeEditorWindow);
219
+ syncActiveEditorWindowDebug(handleKey);
220
+ const localStart = normalizedStart - activeEditorWindow.textStart;
221
+ const localEnd = normalizedEnd - activeEditorWindow.textStart;
222
+ if (activeEditor.value !== activeEditorWindow.text) {
223
+ activeEditor.value = activeEditorWindow.text;
224
+ }
225
+ activeEditor.readOnly = !activeTextEditable;
226
+ activeEditor.setSelectionRange(localStart, localEnd, direction);
227
+ updateCaretBlinkTimer();
228
+ };
229
+
230
+ const mutationController = createEditorMutationController({
231
+ runtimeRef,
232
+ textByHandle,
233
+ selectionsByHandle,
234
+ textByteLengthsByHandle,
235
+ getActiveEditor,
236
+ getActiveEditorWindow: () => activeEditorWindow,
237
+ getActiveTextEditable: () => activeTextEditable,
238
+ getActiveTextHandle: () => activeTextHandle,
239
+ isActiveEditorFocused,
240
+ setPendingLocalReplacementEcho: (value) => {
241
+ pendingLocalReplacementEcho = value;
242
+ },
243
+ setPendingLocalSelectionEcho: (value) => {
244
+ pendingLocalSelectionEcho = value;
245
+ },
246
+ syncFocusedInputState,
247
+ updateActiveEditorWindowText,
248
+ });
249
+
250
+ const clearPendingTextMutations = (): void => {
251
+ mutationController.clearPendingTextMutations();
252
+ };
253
+
254
+ const focusHiddenEditorNow = (): void => {
255
+ getActiveEditor().focus({ preventScroll: true });
256
+ syncFocusedInputState();
257
+ updateCaretBlinkTimer(true);
258
+ };
259
+
260
+ const refocusActiveTextInput = (): void => {
261
+ if (activeTextHandle === null) {
262
+ if (deferredTouchFocusHandle === null) {
263
+ return;
264
+ }
265
+ const textboxState = getTextboxState(deferredTouchFocusHandle);
266
+ if (!textboxState.isTextbox) {
267
+ return;
268
+ }
269
+ activeTextHandle = handleToBigInt(deferredTouchFocusHandle);
270
+ activeTextEditable = textboxState.isEditable;
271
+ activeTextMultiline = textboxState.isMultiline;
272
+ syncFocusedInputState();
273
+ focusHiddenEditorNow();
274
+ return;
275
+ }
276
+ focusHiddenEditorNow();
277
+ };
278
+
279
+ const beginTouchTextFocusDeferral = (handle: bigint): void => {
280
+ deferredTouchFocusHandle = handle.toString();
281
+ };
282
+
283
+ const cancelTouchTextFocusDeferral = (): void => {
284
+ deferredTouchFocusHandle = null;
285
+ };
286
+
287
+ const commitTouchTextFocusDeferral = (handle: bigint): void => {
288
+ const handleKey = handle.toString();
289
+ if (deferredTouchFocusHandle !== handleKey) {
290
+ return;
291
+ }
292
+ deferredTouchFocusHandle = null;
293
+ if (activeTextHandle?.toString() !== handleKey) {
294
+ return;
295
+ }
296
+ if (!isActiveEditorFocused()) {
297
+ focusHiddenEditorNow();
298
+ }
299
+ };
300
+
301
+ const scheduleCaretRevealReplay = (handleKey: string): void => {
302
+ if (!pendingCaretRevealByHandle[handleKey] || activeTextHandle?.toString() !== handleKey) {
303
+ return;
304
+ }
305
+ if (pendingCaretRevealFrame !== null) {
306
+ return;
307
+ }
308
+ pendingCaretRevealFrame = requestAnimationFrame(() => {
309
+ pendingCaretRevealFrame = null;
310
+ if (!pendingCaretRevealByHandle[handleKey] || activeTextHandle?.toString() !== handleKey) {
311
+ return;
312
+ }
313
+ delete pendingCaretRevealByHandle[handleKey];
314
+ const runtime = runtimeRef.current;
315
+ if (runtime === null) {
316
+ return;
317
+ }
318
+ const text = textByHandle[handleKey] ?? '';
319
+ const textByteLength = utf8ByteLength(text);
320
+ const selection = selectionsByHandle[handleKey] ?? { start: textByteLength, end: textByteLength };
321
+ const { start, end } = clampSelectionToText(textByteLength, selection);
322
+ runtime.ui._ui_set_text_selection_range(handleToBigInt(handleKey), start, end);
323
+ });
324
+ };
325
+
326
+ const clearRecordMap = <T>(record: Record<string, T>): void => {
327
+ for (const key of Object.keys(record)) {
328
+ delete record[key];
329
+ }
330
+ };
331
+
332
+ const resetAppSession = (): void => {
333
+ appSessionVersion += 1;
334
+ clearRecordMap(textByHandle);
335
+ clearRecordMap(textByteLengthsByHandle);
336
+ clearRecordMap(selectionsByHandle);
337
+ clearPendingCaretReveal();
338
+ mutationController.reset();
339
+ pendingLocalReplacementEcho = null;
340
+ pendingLocalSelectionEcho = null;
341
+ deferredTouchFocusHandle = null;
342
+ clearCaretBlinkTimer();
343
+ activeTextHandle = null;
344
+ activeTextEditable = false;
345
+ activeTextMultiline = false;
346
+ clearActiveEditorWindow();
347
+ focusedHandle = null;
348
+ pendingSemanticAnnouncements.clear();
349
+ lastInteractivePointerHandle = null;
350
+ capturedPointerHandle = null;
351
+ hiddenInput.value = '';
352
+ hiddenInput.setSelectionRange(0, 0, 'none');
353
+ hiddenTextarea.value = '';
354
+ hiddenTextarea.setSelectionRange(0, 0, 'none');
355
+ detachBridgeTextInput();
356
+ };
357
+ mutationController.attachHiddenEditorListeners(hiddenInput);
358
+ mutationController.attachHiddenEditorListeners(hiddenTextarea);
359
+
360
+ const handleFocusChanged = (handle: WasmHandleLike, isFocused: boolean): void => {
361
+ const handleKey = handle.toString();
362
+ const entry: FocusEventLog = { handle: handleKey, isFocused };
363
+ logs.focusEvents.push(entry);
364
+ if (isFocused) {
365
+ focusedHandle = handleKey;
366
+ } else if (focusedHandle === handleKey) {
367
+ focusedHandle = null;
368
+ }
369
+ if (isFocused) {
370
+ const textboxState = getTextboxState(handleKey);
371
+ if (!textboxState.isTextbox) {
372
+ deferredTouchFocusHandle = null;
373
+ delete pendingCaretRevealByHandle[handleKey];
374
+ activeTextHandle = null;
375
+ activeTextEditable = false;
376
+ activeTextMultiline = false;
377
+ syncFocusedInputState();
378
+ updateCaretBlinkTimer();
379
+ queueSemanticAnnouncement(handleKey);
380
+ return;
381
+ }
382
+ activeTextHandle = handleToBigInt(handle);
383
+ activeTextEditable = textboxState.isEditable;
384
+ activeTextMultiline = textboxState.isMultiline;
385
+ syncFocusedInputState();
386
+ if (deferredTouchFocusHandle === handleKey) {
387
+ updateCaretBlinkTimer();
388
+ queueSemanticAnnouncement(handleKey);
389
+ return;
390
+ }
391
+ window.setTimeout(() => {
392
+ if (activeTextHandle !== null && activeTextHandle.toString() === handleKey) {
393
+ focusHiddenEditorNow();
394
+ }
395
+ }, 0);
396
+ } else if (activeTextHandle !== null && activeTextHandle.toString() === handleKey) {
397
+ if (deferredTouchFocusHandle === handleKey) {
398
+ updateCaretBlinkTimer();
399
+ return;
400
+ }
401
+ deferredTouchFocusHandle = null;
402
+ runtimeRef.current?.flushPendingCommit();
403
+ delete pendingCaretRevealByHandle[handleKey];
404
+ pendingLocalReplacementEcho = null;
405
+ pendingLocalSelectionEcho = null;
406
+ clearPendingTextMutations();
407
+ activeTextHandle = null;
408
+ activeTextEditable = false;
409
+ activeTextMultiline = false;
410
+ syncFocusedInputState();
411
+ }
412
+ updateCaretBlinkTimer();
413
+ if (isFocused) {
414
+ queueSemanticAnnouncement(handleKey);
415
+ }
416
+ };
417
+
418
+ const handleTextChanged = (handle: WasmHandleLike, text: string): void => {
419
+ const handleKey = handle.toString();
420
+ if (pendingLocalReplacementEcho !== null && pendingLocalReplacementEcho.handle === handleKey) {
421
+ pendingLocalReplacementEcho = null;
422
+ }
423
+ textByHandle[handleKey] = text;
424
+ textByteLengthsByHandle[handleKey] = utf8ByteLength(text);
425
+ logs.textChanges.push(summarizeTextChange(handleKey, text));
426
+ if (activeTextHandle !== null && activeTextHandle.toString() === handleKey) {
427
+ pendingCaretRevealByHandle[handleKey] = true;
428
+ if (!isActiveEditorFocused()) {
429
+ focusHiddenEditorNow();
430
+ updateCaretBlinkTimer(true);
431
+ return;
432
+ }
433
+ syncFocusedInputState();
434
+ updateCaretBlinkTimer(true);
435
+ }
436
+ };
437
+
438
+ const handleRequestSemanticAnnouncement = (handle: WasmHandleLike): void => {
439
+ queueSemanticAnnouncement(handle.toString());
440
+ };
441
+
442
+ const handleTextReplaced = (handle: WasmHandleLike, start: number, end: number, text: string): void => {
443
+ const handleKey = handle.toString();
444
+ const previousText = textByHandle[handleKey] ?? '';
445
+ const previousTextByteLength = textByteLengthsByHandle[handleKey] ?? utf8ByteLength(previousText);
446
+ const isLocalEcho = pendingLocalReplacementEcho !== null
447
+ && pendingLocalReplacementEcho.handle === handleKey
448
+ && pendingLocalReplacementEcho.start === start
449
+ && pendingLocalReplacementEcho.end === end
450
+ && pendingLocalReplacementEcho.text === text
451
+ && activeTextHandle !== null
452
+ && activeTextHandle.toString() === handleKey
453
+ && isActiveEditorFocused();
454
+ const nextText = isLocalEcho
455
+ ? previousText
456
+ : applyUtf8ByteReplacementEdit(previousText, start, end, text);
457
+ if (!isLocalEcho) {
458
+ textByHandle[handleKey] = nextText;
459
+ textByteLengthsByHandle[handleKey] = previousTextByteLength - (end - start) + utf8ByteLength(text);
460
+ }
461
+ if (isLocalEcho) {
462
+ pendingLocalReplacementEcho = null;
463
+ }
464
+ logs.textChanges.push(summarizeTextChange(handleKey, nextText));
465
+ if (activeTextHandle !== null && activeTextHandle.toString() === handleKey) {
466
+ pendingCaretRevealByHandle[handleKey] = true;
467
+ if (!isActiveEditorFocused()) {
468
+ focusHiddenEditorNow();
469
+ updateCaretBlinkTimer(true);
470
+ return;
471
+ }
472
+ if (!isLocalEcho) {
473
+ syncFocusedInputState();
474
+ }
475
+ updateCaretBlinkTimer(true);
476
+ }
477
+ };
478
+
479
+ const handleSelectionChanged = (handle: WasmHandleLike, start: number, end: number): void => {
480
+ const handleKey = handle.toString();
481
+ const isLocalEcho = pendingLocalSelectionEcho !== null
482
+ && pendingLocalSelectionEcho.handle === handleKey
483
+ && pendingLocalSelectionEcho.start === start
484
+ && pendingLocalSelectionEcho.end === end
485
+ && activeTextHandle !== null
486
+ && activeTextHandle.toString() === handleKey
487
+ && isActiveEditorFocused();
488
+ selectionsByHandle[handleKey] = { start, end };
489
+ if (pendingLocalSelectionEcho !== null && pendingLocalSelectionEcho.handle === handleKey) {
490
+ pendingLocalSelectionEcho = null;
491
+ }
492
+ const entry: SelectionChangeLog = { handle: handleKey, start, end };
493
+ logs.selectionChanges.push(entry);
494
+ if (activeTextHandle !== null && activeTextHandle.toString() === handleKey) {
495
+ if (!isActiveEditorFocused()) {
496
+ focusHiddenEditorNow();
497
+ updateCaretBlinkTimer(true);
498
+ return;
499
+ }
500
+ if (!isLocalEcho) {
501
+ syncFocusedInputState();
502
+ }
503
+ updateCaretBlinkTimer(true);
504
+ scheduleCaretRevealReplay(handleKey);
505
+ }
506
+ };
507
+
508
+ const handleClipboardRead = (handle: WasmHandleLike): void => {
509
+ const runtime = runtimeRef.current;
510
+ if (runtime === null) {
511
+ return;
512
+ }
513
+ const handleValue = handleToBigInt(handle);
514
+ const requestSessionVersion = appSessionVersion;
515
+ logs.clipboardReadRequests.push(handleValue.toString());
516
+ void navigator.clipboard.readText().then((text) => {
517
+ if (requestSessionVersion !== appSessionVersion) {
518
+ return;
519
+ }
520
+ const handleKey = handleValue.toString();
521
+ const currentText = textByHandle[handleKey] ?? '';
522
+ const currentTextByteLength = textByteLengthsByHandle[handleKey] ?? utf8ByteLength(currentText);
523
+ const selection = selectionsByHandle[handleKey] ?? { start: currentTextByteLength, end: currentTextByteLength };
524
+ const rangeStart = Math.max(0, Math.min(selection.start, selection.end));
525
+ const rangeEnd = Math.max(rangeStart, Math.min(currentTextByteLength, Math.max(selection.start, selection.end)));
526
+ const clampedEdit = buildClampedTextboxEdit(
527
+ currentText,
528
+ rangeStart,
529
+ rangeEnd,
530
+ text,
531
+ rangeStart + utf8ByteLength(text),
532
+ );
533
+ textByHandle[handleKey] = clampedEdit.fullNextText;
534
+ textByteLengthsByHandle[handleKey] = utf8ByteLength(clampedEdit.fullNextText);
535
+ selectionsByHandle[handleKey] = { start: clampedEdit.caretByte, end: clampedEdit.caretByte };
536
+ if (activeTextHandle !== null && activeTextHandle.toString() === handleKey) {
537
+ syncFocusedInputState();
538
+ }
539
+ if (clampedEdit.replacement === null) {
540
+ return;
541
+ }
542
+ runtime.ui._ui_set_interaction_time(currentInteractionTimeMs());
543
+ const replacementStartByte = codeUnitIndexToUtf8ByteOffset(currentText, clampedEdit.replacement.start);
544
+ const replacementEndByte = codeUnitIndexToUtf8ByteOffset(currentText, clampedEdit.replacement.end);
545
+ pendingLocalReplacementEcho = {
546
+ handle: handleKey,
547
+ start: replacementStartByte,
548
+ end: replacementEndByte,
549
+ text: clampedEdit.replacement.insertedText,
550
+ };
551
+ pendingLocalSelectionEcho = {
552
+ handle: handleKey,
553
+ start: clampedEdit.caretByte,
554
+ end: clampedEdit.caretByte,
555
+ };
556
+ const heapString = writeUtf8ToHeap(runtime.ui, clampedEdit.replacement.insertedText);
557
+ try {
558
+ runtime.ui._ui_replace_text_range(
559
+ handleValue,
560
+ replacementStartByte,
561
+ replacementEndByte,
562
+ heapString.ptr,
563
+ heapString.len,
564
+ clampedEdit.caretByte,
565
+ );
566
+ } finally {
567
+ heapString.dispose();
568
+ }
569
+ runtime.commitFrame();
570
+ }).catch(() => undefined);
571
+ };
572
+
573
+ return {
574
+ logs,
575
+ textByHandle,
576
+ selectionsByHandle,
577
+ hasPendingTextMutations: mutationController.hasPendingTextMutations,
578
+ materializePendingTextMutations: mutationController.materializePendingTextMutations,
579
+ getActiveTextEditable: () => activeTextEditable,
580
+ getActiveTextHandle: () => activeTextHandle,
581
+ getActiveTextMultiline: () => activeTextMultiline,
582
+ getCapturedPointerHandle: () => capturedPointerHandle,
583
+ getLastPointerClientPosition: () => ({ x: lastPointerClientX, y: lastPointerClientY }),
584
+ getLastPointerPosition: () => ({ x: lastPointerX, y: lastPointerY }),
585
+ getLastPointerModifiers: () => lastPointerModifiers,
586
+ getLastInteractivePointerHandle: () => lastInteractivePointerHandle,
587
+ isActiveTextInputFocused: isActiveEditorFocused,
588
+ isPointerInsideCanvas: () => pointerInsideCanvas,
589
+ applyActiveTextDeletion: mutationController.applyActiveTextDeletion,
590
+ beginTouchTextFocusDeferral,
591
+ cancelTouchTextFocusDeferral,
592
+ commitTouchTextFocusDeferral,
593
+ refocusActiveTextInput,
594
+ resetAppSession,
595
+ consumePendingSemanticAnnouncements: () => {
596
+ const handles = Array.from(pendingSemanticAnnouncements.values());
597
+ pendingSemanticAnnouncements.clear();
598
+ return handles;
599
+ },
600
+ getFocusedHandle: () => focusedHandle,
601
+ setCapturedPointerHandle: (handle: bigint | null) => {
602
+ capturedPointerHandle = handle;
603
+ },
604
+ setLastPointerClientPosition: (x: number, y: number) => {
605
+ lastPointerClientX = x;
606
+ lastPointerClientY = y;
607
+ },
608
+ setLastPointerModifiers: (modifiers: number) => {
609
+ lastPointerModifiers = modifiers;
610
+ },
611
+ setLastPointerPosition: (x: number, y: number) => {
612
+ lastPointerX = x;
613
+ lastPointerY = y;
614
+ },
615
+ setLastInteractivePointerHandle: (handle: bigint | null) => {
616
+ lastInteractivePointerHandle = handle;
617
+ },
618
+ setPointerInsideCanvas: (flag: boolean) => {
619
+ pointerInsideCanvas = flag;
620
+ },
621
+ handleClipboardRead,
622
+ handleFocusChanged,
623
+ handleRequestSemanticAnnouncement,
624
+ handleSelectionChanged,
625
+ handleTextChanged,
626
+ handleTextReplaced,
627
+ };
628
+ }
@@ -0,0 +1,23 @@
1
+ import type {
2
+ BridgeLogs,
3
+ FocusEventLog,
4
+ PointerEventLog,
5
+ ScrollEventLog,
6
+ SelectionChangeLog,
7
+ TextChangeLog,
8
+ } from '../../core-types';
9
+
10
+ export function createBridgeLogs(): BridgeLogs {
11
+ return {
12
+ pointerEvents: [] as PointerEventLog[],
13
+ focusEvents: [] as FocusEventLog[],
14
+ textChanges: [] as TextChangeLog[],
15
+ selectionChanges: [] as SelectionChangeLog[],
16
+ crossSelectionChanges: [] as { areaHandle: string; text: string }[],
17
+ clipboardWrites: [] as string[],
18
+ clipboardReadRequests: [] as string[],
19
+ scrollEvents: [] as ScrollEventLog[],
20
+ missingFontCoverageRequests: [] as { fontId: number; coverageKind: number; sampleText: string }[],
21
+ incrementalFontPackageRequests: [] as { primaryFontId: number; coverageKind: number; packageId: string; segmentIds: readonly string[]; sampleText: string }[],
22
+ } satisfies BridgeLogs;
23
+ }
@@ -0,0 +1,51 @@
1
+ export function utf8ByteLengthForCodePoint(codePoint: number): number {
2
+ if (codePoint <= 0x7f) {
3
+ return 1;
4
+ }
5
+ if (codePoint <= 0x7ff) {
6
+ return 2;
7
+ }
8
+ if (codePoint <= 0xffff) {
9
+ return 3;
10
+ }
11
+ return 4;
12
+ }
13
+
14
+ export function advanceCodeUnitIndex(text: string, index: number): number {
15
+ const codePoint = text.codePointAt(index) ?? 0;
16
+ return index + (codePoint > 0xffff ? 2 : 1);
17
+ }
18
+
19
+ export function retreatCodeUnitIndex(text: string, index: number): number {
20
+ const clampedIndex = Math.max(0, Math.min(index, text.length));
21
+ if (clampedIndex <= 0) {
22
+ return 0;
23
+ }
24
+ const previousIndex = clampedIndex - 1;
25
+ const previousUnit = text.charCodeAt(previousIndex);
26
+ if (
27
+ previousIndex > 0 &&
28
+ previousUnit >= 0xdc00 &&
29
+ previousUnit <= 0xdfff
30
+ ) {
31
+ const leadUnit = text.charCodeAt(previousIndex - 1);
32
+ if (leadUnit >= 0xd800 && leadUnit <= 0xdbff) {
33
+ return previousIndex - 1;
34
+ }
35
+ }
36
+ return previousIndex;
37
+ }
38
+
39
+ export function codeUnitIndexToUtf8ByteOffset(text: string, index: number): number {
40
+ const clampedIndex = Math.max(0, Math.min(index, text.length));
41
+ let byteOffset = 0;
42
+ for (let current = 0; current < clampedIndex; current = advanceCodeUnitIndex(text, current)) {
43
+ const codePoint = text.codePointAt(current) ?? 0;
44
+ byteOffset += utf8ByteLengthForCodePoint(codePoint);
45
+ }
46
+ return byteOffset;
47
+ }
48
+
49
+ export function utf8ByteLength(text: string): number {
50
+ return codeUnitIndexToUtf8ByteOffset(text, text.length);
51
+ }