@effindomv2/fui-as 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.
- package/LICENSE.md +7 -0
- package/browser/src/common-harness/host-imports.ts +430 -0
- package/browser/src/common-harness/interop.ts +39 -0
- package/browser/src/common-harness/managed-harness-bitmap-host.ts +92 -0
- package/browser/src/common-harness/managed-harness-fetch-host.ts +201 -0
- package/browser/src/common-harness/managed-harness-file-host.ts +1101 -0
- package/browser/src/common-harness/managed-harness-file-payloads.ts +143 -0
- package/browser/src/common-harness/managed-harness-file-types.ts +106 -0
- package/browser/src/common-harness/managed-harness-session.ts +15 -0
- package/browser/src/common-harness/managed-harness.ts +1323 -0
- package/browser/src/common-harness/managed-history.ts +168 -0
- package/browser/src/common-harness/persisted-restore-policy.ts +50 -0
- package/browser/src/common-harness/persisted-ui-state-controller.ts +309 -0
- package/browser/src/common-harness/text-session-bridge.ts +452 -0
- package/browser/src/common-harness/types.ts +205 -0
- package/browser/src/common-harness/ui-chrome.ts +191 -0
- package/browser/src/common-harness/ui-imports.ts +529 -0
- package/browser/src/common-harness/wasm-module-cache.ts +47 -0
- package/browser/src/common-harness.ts +27 -0
- package/browser/src/file-processing-worker.ts +89 -0
- package/browser/src/host-events.ts +97 -0
- package/browser/src/host-services.ts +203 -0
- package/browser/src/index.ts +62 -0
- package/browser/src/persisted-ui-state.ts +206 -0
- package/browser/src/routed-harness.ts +198 -0
- package/browser/src/worker-bootstrap.ts +483 -0
- package/browser/src/worker-manager.ts +230 -0
- package/browser/src/worker-types.ts +50 -0
- package/package.json +89 -0
- package/scripts/build-demo-as.sh +91 -0
- package/scripts/build.sh +325 -0
- package/scripts/generate-host-events.ts +175 -0
- package/scripts/generate-host-services.ts +157 -0
- package/src/Fui.ts +205 -0
- package/src/FuiExports.ts +55 -0
- package/src/FuiPrimitives.ts +15 -0
- package/src/FuiWorker.ts +3 -0
- package/src/FuiWorkerExports.ts +6 -0
- package/src/bindings/ui.ts +531 -0
- package/src/color.ts +86 -0
- package/src/controls/AntiSelectionArea.ts +23 -0
- package/src/controls/Button.ts +750 -0
- package/src/controls/Checkbox.ts +181 -0
- package/src/controls/ContextMenu.ts +885 -0
- package/src/controls/ControlTemplateSet.ts +37 -0
- package/src/controls/Dialog.ts +355 -0
- package/src/controls/Dropdown.ts +856 -0
- package/src/controls/Form.ts +110 -0
- package/src/controls/NavLink.ts +211 -0
- package/src/controls/Popup.ts +129 -0
- package/src/controls/ProgressBar.ts +180 -0
- package/src/controls/RadioButton.ts +135 -0
- package/src/controls/RadioGroup.ts +244 -0
- package/src/controls/SelectionArea.ts +75 -0
- package/src/controls/Slider.ts +471 -0
- package/src/controls/Switch.ts +132 -0
- package/src/controls/TextArea.ts +20 -0
- package/src/controls/TextInput.ts +7 -0
- package/src/controls/index.ts +18 -0
- package/src/controls/internal/ButtonPresenter.ts +95 -0
- package/src/controls/internal/CheckboxIndicatorPresenter.ts +93 -0
- package/src/controls/internal/DropdownChevronPresenter.ts +67 -0
- package/src/controls/internal/DropdownFieldPresenter.ts +110 -0
- package/src/controls/internal/DropdownOptionRowPresenter.ts +82 -0
- package/src/controls/internal/PopupPresenter.ts +198 -0
- package/src/controls/internal/PressableIndicatorPresenter.ts +32 -0
- package/src/controls/internal/PressableLabeledControl.ts +221 -0
- package/src/controls/internal/RadioIndicatorPresenter.ts +73 -0
- package/src/controls/internal/SliderPresenter.ts +157 -0
- package/src/controls/internal/SwitchIndicatorPresenter.ts +72 -0
- package/src/controls/internal/TextInputCore.ts +695 -0
- package/src/controls/internal/TextInputPresenter.ts +72 -0
- package/src/controls/templating.ts +54 -0
- package/src/core/Action.ts +94 -0
- package/src/core/Actions.ts +37 -0
- package/src/core/Animation.ts +412 -0
- package/src/core/Application.ts +328 -0
- package/src/core/Assets.ts +264 -0
- package/src/core/AttachedProperties.ts +32 -0
- package/src/core/Bitmap.ts +70 -0
- package/src/core/BoundCallback.ts +104 -0
- package/src/core/Callbacks.ts +17 -0
- package/src/core/ContextMenuManager.ts +466 -0
- package/src/core/DebugApi.ts +30 -0
- package/src/core/Disposable.ts +10 -0
- package/src/core/DragDropManager.ts +179 -0
- package/src/core/DragGesture.ts +184 -0
- package/src/core/DynamicAssetIds.ts +24 -0
- package/src/core/Errors.ts +48 -0
- package/src/core/EventRouter.ts +408 -0
- package/src/core/ExternalDropManager.ts +122 -0
- package/src/core/Fetch.ts +264 -0
- package/src/core/FetchFfi.ts +15 -0
- package/src/core/File.ts +1002 -0
- package/src/core/FocusAdornerManager.ts +263 -0
- package/src/core/FocusVisibility.ts +36 -0
- package/src/core/FrameScheduler.ts +28 -0
- package/src/core/KeyboardScroll.ts +161 -0
- package/src/core/KeyboardScrollTracker.ts +386 -0
- package/src/core/Logger.ts +80 -0
- package/src/core/Navigation.ts +13 -0
- package/src/core/Node.ts +1708 -0
- package/src/core/PersistedState.ts +102 -0
- package/src/core/PersistedUiState.ts +142 -0
- package/src/core/Platform.ts +219 -0
- package/src/core/Signal.ts +89 -0
- package/src/core/Theme.ts +365 -0
- package/src/core/Timers.ts +129 -0
- package/src/core/ToolTip.ts +122 -0
- package/src/core/ToolTipManager.ts +459 -0
- package/src/core/Transitions.ts +34 -0
- package/src/core/Typography.ts +204 -0
- package/src/core/Worker.ts +196 -0
- package/src/core/bind.ts +37 -0
- package/src/core/event_exports.ts +596 -0
- package/src/core/ffi.ts +728 -0
- package/src/host-services/runtime.ts +25 -0
- package/src/nodes/FlexBox.ts +789 -0
- package/src/nodes/GradientStop.ts +9 -0
- package/src/nodes/Grid.ts +183 -0
- package/src/nodes/Image.ts +189 -0
- package/src/nodes/Portal.ts +14 -0
- package/src/nodes/RichText.ts +312 -0
- package/src/nodes/ScrollBar.ts +570 -0
- package/src/nodes/ScrollBox.ts +415 -0
- package/src/nodes/ScrollState.ts +10 -0
- package/src/nodes/ScrollView.ts +511 -0
- package/src/nodes/Svg.ts +142 -0
- package/src/nodes/Text.ts +145 -0
- package/src/nodes/TextCore.ts +558 -0
- package/src/nodes/VirtualList.ts +431 -0
- package/src/nodes/helpers.ts +25 -0
- package/src/nodes/index.ts +14 -0
- package/src/tsconfig.json +7 -0
- package/src/worker/Worker.ts +169 -0
- package/src/worker/WorkerJob.ts +65 -0
- package/src/worker/ffi.ts +23 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import type { BridgeRuntime, WasmHandleLike } from '@effindomv2/runtime';
|
|
2
|
+
|
|
3
|
+
import { currentInteractionTimeMs, toBigIntHandle, toNumberHandle, zeroPointer, normalizePointer, type AppHandleLike } from './interop';
|
|
4
|
+
|
|
5
|
+
const decoder = new TextDecoder();
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const TEXTBOX_HARD_CLAMP_MAX_CODEPOINTS = 10000;
|
|
8
|
+
|
|
9
|
+
interface TextClampRange {
|
|
10
|
+
readonly start: number;
|
|
11
|
+
readonly end: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function advanceCodeUnitIndex(text: string, index: number): number {
|
|
15
|
+
const codePoint = text.codePointAt(index) ?? 0;
|
|
16
|
+
return index + (codePoint > 0xffff ? 2 : 1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isLineBreakCodeUnit(text: string, index: number): boolean {
|
|
20
|
+
if (index < 0 || index >= text.length) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const codeUnit = text.charCodeAt(index);
|
|
24
|
+
return codeUnit === 0x0a || codeUnit === 0x0d;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function collectTextboxHardLineClampRanges(text: string): readonly TextClampRange[] {
|
|
28
|
+
const ranges: TextClampRange[] = [];
|
|
29
|
+
let index = 0;
|
|
30
|
+
while (index < text.length) {
|
|
31
|
+
let lineCapEnd = index;
|
|
32
|
+
let lineEnd = index;
|
|
33
|
+
let codePointCount = 0;
|
|
34
|
+
while (lineEnd < text.length && !isLineBreakCodeUnit(text, lineEnd)) {
|
|
35
|
+
const next = advanceCodeUnitIndex(text, lineEnd);
|
|
36
|
+
if (codePointCount < TEXTBOX_HARD_CLAMP_MAX_CODEPOINTS) {
|
|
37
|
+
lineCapEnd = next;
|
|
38
|
+
}
|
|
39
|
+
codePointCount += 1;
|
|
40
|
+
lineEnd = next;
|
|
41
|
+
}
|
|
42
|
+
if (codePointCount > TEXTBOX_HARD_CLAMP_MAX_CODEPOINTS) {
|
|
43
|
+
ranges.push({ start: lineCapEnd, end: lineEnd });
|
|
44
|
+
}
|
|
45
|
+
if (lineEnd >= text.length) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
if (text.charCodeAt(lineEnd) === 0x0d && lineEnd + 1 < text.length && text.charCodeAt(lineEnd + 1) === 0x0a) {
|
|
49
|
+
index = lineEnd + 2;
|
|
50
|
+
} else {
|
|
51
|
+
index = lineEnd + 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return ranges;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function mapClampedTextIndex(index: number, ranges: readonly TextClampRange[]): number {
|
|
58
|
+
const clampedIndex = Math.max(0, index);
|
|
59
|
+
let removedBefore = 0;
|
|
60
|
+
for (const range of ranges) {
|
|
61
|
+
if (clampedIndex <= range.start) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
if (clampedIndex < range.end) {
|
|
65
|
+
return range.start - removedBefore;
|
|
66
|
+
}
|
|
67
|
+
removedBefore += range.end - range.start;
|
|
68
|
+
}
|
|
69
|
+
return clampedIndex - removedBefore;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function clampTextboxHardLines(text: string): {
|
|
73
|
+
readonly text: string;
|
|
74
|
+
mapIndex(index: number): number;
|
|
75
|
+
} {
|
|
76
|
+
const ranges = collectTextboxHardLineClampRanges(text);
|
|
77
|
+
if (ranges.length === 0) {
|
|
78
|
+
return {
|
|
79
|
+
text,
|
|
80
|
+
mapIndex: (index: number) => Math.max(0, Math.min(index, text.length)),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
let result = '';
|
|
84
|
+
let cursor = 0;
|
|
85
|
+
for (const range of ranges) {
|
|
86
|
+
result += text.slice(cursor, range.start);
|
|
87
|
+
cursor = range.end;
|
|
88
|
+
}
|
|
89
|
+
result += text.slice(cursor);
|
|
90
|
+
return {
|
|
91
|
+
text: result,
|
|
92
|
+
mapIndex: (index: number) => mapClampedTextIndex(index, ranges),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function computeReplacementEdit(previousText: string, nextText: string): {
|
|
97
|
+
readonly start: number;
|
|
98
|
+
readonly end: number;
|
|
99
|
+
readonly insertedText: string;
|
|
100
|
+
} | null {
|
|
101
|
+
if (previousText === nextText) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const sharedPrefixLimit = Math.min(previousText.length, nextText.length);
|
|
106
|
+
let prefix = 0;
|
|
107
|
+
while (prefix < sharedPrefixLimit && previousText.charCodeAt(prefix) === nextText.charCodeAt(prefix)) {
|
|
108
|
+
prefix += 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let suffix = 0;
|
|
112
|
+
while (
|
|
113
|
+
suffix < (previousText.length - prefix) &&
|
|
114
|
+
suffix < (nextText.length - prefix) &&
|
|
115
|
+
previousText.charCodeAt(previousText.length - suffix - 1) === nextText.charCodeAt(nextText.length - suffix - 1)
|
|
116
|
+
) {
|
|
117
|
+
suffix += 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
start: prefix,
|
|
122
|
+
end: previousText.length - suffix,
|
|
123
|
+
insertedText: nextText.slice(prefix, nextText.length - suffix),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function applyReplacementEdit(text: string, start: number, end: number, insertedText: string): string {
|
|
128
|
+
const clampedStart = Math.max(0, Math.min(start, text.length));
|
|
129
|
+
const clampedEnd = Math.max(clampedStart, Math.min(end, text.length));
|
|
130
|
+
return `${text.slice(0, clampedStart)}${insertedText}${text.slice(clampedEnd)}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getHiddenTextEditor(): HTMLInputElement | HTMLTextAreaElement | null {
|
|
134
|
+
const activeElement = document.activeElement;
|
|
135
|
+
if (
|
|
136
|
+
(activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement) &&
|
|
137
|
+
activeElement.dataset.effindomHiddenEditor === 'true'
|
|
138
|
+
) {
|
|
139
|
+
return activeElement;
|
|
140
|
+
}
|
|
141
|
+
const editor = document.querySelector('input[data-effindom-hidden-editor="true"], textarea[data-effindom-hidden-editor="true"]');
|
|
142
|
+
return editor instanceof HTMLInputElement || editor instanceof HTMLTextAreaElement ? editor : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface TextSessionLike {
|
|
146
|
+
readonly memory: WebAssembly.Memory;
|
|
147
|
+
readonly textBufferPtr: number;
|
|
148
|
+
readonly textBufferSize: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface FrozenTextSelectionSnapshot {
|
|
152
|
+
readonly handleKey: string;
|
|
153
|
+
readonly text: string;
|
|
154
|
+
readonly start: number;
|
|
155
|
+
readonly end: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export class TextSessionBridge {
|
|
159
|
+
private readonly latestTextByHandle = new Map<string, string>();
|
|
160
|
+
private readonly latestSelectionByHandle = new Map<string, { start: number; end: number }>();
|
|
161
|
+
private frozenTextSelectionSnapshot: FrozenTextSelectionSnapshot | null = null;
|
|
162
|
+
|
|
163
|
+
constructor(
|
|
164
|
+
private readonly getRuntime: () => BridgeRuntime,
|
|
165
|
+
private readonly getCurrentMemory: () => WebAssembly.Memory,
|
|
166
|
+
private readonly queueHarnessFrame: () => void,
|
|
167
|
+
) {}
|
|
168
|
+
|
|
169
|
+
clearState(): void {
|
|
170
|
+
this.latestTextByHandle.clear();
|
|
171
|
+
this.latestSelectionByHandle.clear();
|
|
172
|
+
this.frozenTextSelectionSnapshot = null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
readAppUtf8(ptr: number, len: number): string {
|
|
176
|
+
if (len === 0) {
|
|
177
|
+
return '';
|
|
178
|
+
}
|
|
179
|
+
return decoder.decode(new Uint8Array(this.getCurrentMemory().buffer, ptr, len));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
readAppFloats(ptr: number, count: number): Float32Array {
|
|
183
|
+
if (count === 0) {
|
|
184
|
+
return new Float32Array(0);
|
|
185
|
+
}
|
|
186
|
+
return new Float32Array(this.getCurrentMemory().buffer.slice(ptr, ptr + (count * 4)));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
readAppBytes(ptr: number, len: number): Uint8Array {
|
|
190
|
+
if (len === 0) {
|
|
191
|
+
return new Uint8Array(0);
|
|
192
|
+
}
|
|
193
|
+
return new Uint8Array(this.getCurrentMemory().buffer.slice(ptr, ptr + len));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
readAppTextParts(ptr: number, len: number): string[] {
|
|
197
|
+
if (len === 0) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
const source = new Uint8Array(this.getCurrentMemory().buffer, ptr, len);
|
|
201
|
+
if (source.byteLength < 4) {
|
|
202
|
+
throw new Error('Fetch request header payload was truncated.');
|
|
203
|
+
}
|
|
204
|
+
const dataView = new DataView(source.buffer, source.byteOffset, source.byteLength);
|
|
205
|
+
let byteOffset = 0;
|
|
206
|
+
const count = dataView.getUint32(byteOffset, true);
|
|
207
|
+
byteOffset += 4;
|
|
208
|
+
const values: string[] = [];
|
|
209
|
+
for (let index = 0; index < count; index += 1) {
|
|
210
|
+
if (byteOffset + 4 > source.byteLength) {
|
|
211
|
+
throw new Error('Fetch request header length was truncated.');
|
|
212
|
+
}
|
|
213
|
+
const partLen = dataView.getUint32(byteOffset, true);
|
|
214
|
+
byteOffset += 4;
|
|
215
|
+
if (byteOffset + partLen > source.byteLength) {
|
|
216
|
+
throw new Error('Fetch request header value was truncated.');
|
|
217
|
+
}
|
|
218
|
+
values.push(partLen > 0 ? decoder.decode(source.subarray(byteOffset, byteOffset + partLen)) : '');
|
|
219
|
+
byteOffset += partLen;
|
|
220
|
+
}
|
|
221
|
+
return values;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
writeAppFloat32(ptr: number, value: number): void {
|
|
225
|
+
const appView = new DataView(this.getCurrentMemory().buffer);
|
|
226
|
+
appView.setFloat32(ptr, value, true);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
writeAppUint32(ptr: number, value: number): void {
|
|
230
|
+
const appView = new DataView(this.getCurrentMemory().buffer);
|
|
231
|
+
appView.setUint32(ptr, value >>> 0, true);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
writeAppUtf8(ptr: number, capacity: number, text: string, context: string): number {
|
|
235
|
+
if (capacity <= 0) {
|
|
236
|
+
if (text.length === 0) {
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
throw new Error(`${context} cannot write into a zero-length host-service buffer.`);
|
|
240
|
+
}
|
|
241
|
+
const encoded = encoder.encode(text);
|
|
242
|
+
if (encoded.length > capacity) {
|
|
243
|
+
throw new Error(`${context} exceeds the provided host-service buffer.`);
|
|
244
|
+
}
|
|
245
|
+
if (encoded.length > 0) {
|
|
246
|
+
const memory = new Uint8Array(this.getCurrentMemory().buffer, ptr, encoded.length);
|
|
247
|
+
memory.set(encoded);
|
|
248
|
+
}
|
|
249
|
+
return encoded.length;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
writeTextCallbackPayload(session: TextSessionLike, text: string, context: string): number {
|
|
253
|
+
const encoded = encoder.encode(text);
|
|
254
|
+
if (encoded.length > session.textBufferSize) {
|
|
255
|
+
throw new Error(`${context} exceeds the shared AssemblyScript text buffer.`);
|
|
256
|
+
}
|
|
257
|
+
if (encoded.length > 0) {
|
|
258
|
+
const memory = new Uint8Array(session.memory.buffer, session.textBufferPtr, encoded.length);
|
|
259
|
+
memory.set(encoded);
|
|
260
|
+
}
|
|
261
|
+
return encoded.length;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
writeWorkerTextCallbackPayload(session: TextSessionLike, text: string, context: string): number {
|
|
265
|
+
return this.writeTextCallbackPayload(session, text, context);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
writeTextToSessionBuffer(session: TextSessionLike, text: string): number {
|
|
269
|
+
if (session.textBufferPtr === 0 || session.textBufferSize === 0) {
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
const encoded = encoder.encode(text);
|
|
273
|
+
const length = Math.min(encoded.length, session.textBufferSize);
|
|
274
|
+
if (length > 0) {
|
|
275
|
+
const memory = new Uint8Array(session.memory.buffer, session.textBufferPtr, length);
|
|
276
|
+
memory.set(encoded.subarray(0, length));
|
|
277
|
+
}
|
|
278
|
+
return length;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
withUiUtf8(
|
|
282
|
+
text: string,
|
|
283
|
+
callback: (ptr: WasmHandleLike | number, len: number) => void,
|
|
284
|
+
): void {
|
|
285
|
+
const runtime = this.getRuntime();
|
|
286
|
+
if (text.length === 0) {
|
|
287
|
+
callback(zeroPointer(runtime), 0);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const bytes = encoder.encode(text);
|
|
291
|
+
const ptr = runtime.ui._malloc(bytes.length);
|
|
292
|
+
const numericPtr = toNumberHandle(ptr);
|
|
293
|
+
runtime.ui.HEAPU8.set(bytes, numericPtr);
|
|
294
|
+
callback(normalizePointer(runtime, ptr), bytes.length);
|
|
295
|
+
runtime.ui._free(ptr);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
withUiGridData(
|
|
299
|
+
values: Float32Array,
|
|
300
|
+
types: Uint8Array,
|
|
301
|
+
callback: (valuesPtr: WasmHandleLike | number, typesPtr: WasmHandleLike | number) => void,
|
|
302
|
+
): void {
|
|
303
|
+
const runtime = this.getRuntime();
|
|
304
|
+
const valueBytes = new Uint8Array(values.buffer);
|
|
305
|
+
const valuePtr = valueBytes.length > 0 ? runtime.ui._malloc(valueBytes.length) : zeroPointer(runtime);
|
|
306
|
+
const valueNumericPtr = valueBytes.length > 0 ? toNumberHandle(valuePtr) : 0;
|
|
307
|
+
if (valueBytes.length > 0) {
|
|
308
|
+
runtime.ui.HEAPU8.set(valueBytes, valueNumericPtr);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const typePtr = types.length > 0 ? runtime.ui._malloc(types.length) : zeroPointer(runtime);
|
|
312
|
+
const typeNumericPtr = types.length > 0 ? toNumberHandle(typePtr) : 0;
|
|
313
|
+
if (types.length > 0) {
|
|
314
|
+
runtime.ui.HEAPU8.set(types, typeNumericPtr);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
callback(normalizePointer(runtime, valuePtr), normalizePointer(runtime, typePtr));
|
|
318
|
+
|
|
319
|
+
if (types.length > 0) {
|
|
320
|
+
runtime.ui._free(typePtr);
|
|
321
|
+
}
|
|
322
|
+
if (valueBytes.length > 0) {
|
|
323
|
+
runtime.ui._free(valuePtr);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
withUiGradientData(
|
|
328
|
+
offsets: Float32Array,
|
|
329
|
+
colors: Uint32Array,
|
|
330
|
+
callback: (offsetsPtr: WasmHandleLike | number, colorsPtr: WasmHandleLike | number) => void,
|
|
331
|
+
): void {
|
|
332
|
+
const runtime = this.getRuntime();
|
|
333
|
+
const offsetBytes = new Uint8Array(offsets.buffer);
|
|
334
|
+
const offsetPtr = offsetBytes.length > 0 ? runtime.ui._malloc(offsetBytes.length) : zeroPointer(runtime);
|
|
335
|
+
const offsetNumericPtr = offsetBytes.length > 0 ? toNumberHandle(offsetPtr) : 0;
|
|
336
|
+
if (offsetBytes.length > 0) {
|
|
337
|
+
runtime.ui.HEAPU8.set(offsetBytes, offsetNumericPtr);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const colorBytes = new Uint8Array(colors.buffer);
|
|
341
|
+
const colorPtr = colorBytes.length > 0 ? runtime.ui._malloc(colorBytes.length) : zeroPointer(runtime);
|
|
342
|
+
const colorNumericPtr = colorBytes.length > 0 ? toNumberHandle(colorPtr) : 0;
|
|
343
|
+
if (colorBytes.length > 0) {
|
|
344
|
+
runtime.ui.HEAPU8.set(colorBytes, colorNumericPtr);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
callback(normalizePointer(runtime, offsetPtr), normalizePointer(runtime, colorPtr));
|
|
348
|
+
|
|
349
|
+
if (colorBytes.length > 0) {
|
|
350
|
+
runtime.ui._free(colorPtr);
|
|
351
|
+
}
|
|
352
|
+
if (offsetBytes.length > 0) {
|
|
353
|
+
runtime.ui._free(offsetPtr);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
recordTextChanged(handle: AppHandleLike, text: string): void {
|
|
358
|
+
this.latestTextByHandle.set(toBigIntHandle(handle).toString(), text);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
recordTextReplaced(handle: AppHandleLike, start: number, end: number, text: string): void {
|
|
362
|
+
const handleKey = toBigIntHandle(handle).toString();
|
|
363
|
+
const previousText = this.latestTextByHandle.get(handleKey) ?? '';
|
|
364
|
+
this.latestTextByHandle.set(handleKey, applyReplacementEdit(previousText, start, end, text));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
recordSelectionChanged(handle: AppHandleLike, start: number, end: number): void {
|
|
368
|
+
this.latestSelectionByHandle.set(toBigIntHandle(handle).toString(), { start, end });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
getLatestText(handle: AppHandleLike): string {
|
|
372
|
+
return this.latestTextByHandle.get(toBigIntHandle(handle).toString()) ?? '';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
resolveFrozenOrLiveTextSelection(handle: AppHandleLike): FrozenTextSelectionSnapshot | null {
|
|
376
|
+
const handleKey = toBigIntHandle(handle).toString();
|
|
377
|
+
if (
|
|
378
|
+
this.frozenTextSelectionSnapshot !== null &&
|
|
379
|
+
this.frozenTextSelectionSnapshot.handleKey === handleKey &&
|
|
380
|
+
this.frozenTextSelectionSnapshot.start !== this.frozenTextSelectionSnapshot.end
|
|
381
|
+
) {
|
|
382
|
+
return this.frozenTextSelectionSnapshot;
|
|
383
|
+
}
|
|
384
|
+
const text = this.latestTextByHandle.get(handleKey) ?? '';
|
|
385
|
+
const selection = this.latestSelectionByHandle.get(handleKey) ?? null;
|
|
386
|
+
if (selection === null || selection.start === selection.end || text.length === 0) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
const start = Math.max(0, Math.min(selection.start, selection.end));
|
|
390
|
+
const end = Math.max(start, Math.min(text.length, Math.max(selection.start, selection.end)));
|
|
391
|
+
return { handleKey, text, start, end };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
freezeTextSelectionSnapshot(handle: AppHandleLike): void {
|
|
395
|
+
this.frozenTextSelectionSnapshot = this.resolveFrozenOrLiveTextSelection(handle);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
clearFrozenTextSelectionSnapshot(): void {
|
|
399
|
+
this.frozenTextSelectionSnapshot = null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
getHiddenTextEditor(): HTMLInputElement | HTMLTextAreaElement | null {
|
|
403
|
+
return getHiddenTextEditor();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
syncEditableTextToRuntime(handle: AppHandleLike, text: string, caret: number): void {
|
|
407
|
+
const runtime = this.getRuntime();
|
|
408
|
+
const handleKey = toBigIntHandle(handle).toString();
|
|
409
|
+
const previousText = this.latestTextByHandle.get(handleKey) ?? '';
|
|
410
|
+
const replacement = computeReplacementEdit(previousText, text);
|
|
411
|
+
if (replacement === null) {
|
|
412
|
+
runtime.commitFrame();
|
|
413
|
+
this.queueHarnessFrame();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const intendedText =
|
|
417
|
+
`${previousText.slice(0, replacement.start)}${replacement.insertedText}${previousText.slice(replacement.end)}`;
|
|
418
|
+
const intendedCaret = Math.max(0, Math.min(caret, intendedText.length));
|
|
419
|
+
const clamped = clampTextboxHardLines(intendedText);
|
|
420
|
+
const committedText = clamped.text;
|
|
421
|
+
const clampedCaret = clamped.mapIndex(intendedCaret);
|
|
422
|
+
const editor = getHiddenTextEditor();
|
|
423
|
+
if (editor !== null && editor.value !== committedText) {
|
|
424
|
+
editor.value = committedText;
|
|
425
|
+
editor.setSelectionRange(clampedCaret, clampedCaret, 'none');
|
|
426
|
+
}
|
|
427
|
+
const committedReplacement = computeReplacementEdit(previousText, committedText);
|
|
428
|
+
if (committedReplacement === null) {
|
|
429
|
+
runtime.commitFrame();
|
|
430
|
+
this.queueHarnessFrame();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
runtime.ui._ui_set_interaction_time(currentInteractionTimeMs());
|
|
434
|
+
this.withUiUtf8(committedReplacement.insertedText, (uiPtr, uiLen) => {
|
|
435
|
+
runtime.ui._ui_replace_text_range(
|
|
436
|
+
toBigIntHandle(handle),
|
|
437
|
+
committedReplacement.start,
|
|
438
|
+
committedReplacement.end,
|
|
439
|
+
uiPtr,
|
|
440
|
+
uiLen,
|
|
441
|
+
clampedCaret,
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
runtime.commitFrame();
|
|
445
|
+
this.queueHarnessFrame();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
updateLiveTextAfterCut(handleKey: string, text: string, caret: number): void {
|
|
449
|
+
this.latestTextByHandle.set(handleKey, text);
|
|
450
|
+
this.latestSelectionByHandle.set(handleKey, { start: caret, end: caret });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { BridgeRuntime, EffinDomRuntimeConfig, WasmHandleLike } from '@effindomv2/runtime';
|
|
2
|
+
|
|
3
|
+
import type { HostEventsDefinition } from '../host-events';
|
|
4
|
+
import type { HostServicesDefinition } from '../host-services';
|
|
5
|
+
import type { WorkerHostServicesBundleConfig } from '../worker-types';
|
|
6
|
+
|
|
7
|
+
export interface HarnessState {
|
|
8
|
+
readonly commandWordCount: number;
|
|
9
|
+
readonly commandWords: readonly number[];
|
|
10
|
+
readonly rootHandle: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface HarnessExports {
|
|
14
|
+
readonly memory: WebAssembly.Memory;
|
|
15
|
+
__flushRenders(): void;
|
|
16
|
+
__fui_capture_persisted_ui_state?(): void;
|
|
17
|
+
__fui_debug_pointer_event?(eventType: number, handle: bigint, x: number, y: number, modifiers: number): void;
|
|
18
|
+
__fui_debug_focus_changed?(handle: bigint, focused: boolean): void;
|
|
19
|
+
__fui_debug_key_event?(eventType: number, keyPtr: number, keyLen: number, modifiers: number): void;
|
|
20
|
+
__fui_debug_scroll?(
|
|
21
|
+
handle: bigint,
|
|
22
|
+
offsetX: number,
|
|
23
|
+
offsetY: number,
|
|
24
|
+
contentWidth: number,
|
|
25
|
+
contentHeight: number,
|
|
26
|
+
viewportWidth: number,
|
|
27
|
+
viewportHeight: number,
|
|
28
|
+
): void;
|
|
29
|
+
__fui_on_pointer_event(eventType: number, handle: bigint, x: number, y: number, modifiers: number): void;
|
|
30
|
+
__fui_on_external_drag_event(
|
|
31
|
+
eventType: number,
|
|
32
|
+
handle: bigint,
|
|
33
|
+
x: number,
|
|
34
|
+
y: number,
|
|
35
|
+
modifiers: number,
|
|
36
|
+
payloadPtr: number,
|
|
37
|
+
payloadLen: number,
|
|
38
|
+
): number;
|
|
39
|
+
__fui_on_fetch_complete(
|
|
40
|
+
requestId: number,
|
|
41
|
+
ok: boolean,
|
|
42
|
+
status: number,
|
|
43
|
+
payloadPtr: number,
|
|
44
|
+
payloadLen: number,
|
|
45
|
+
): void;
|
|
46
|
+
__fui_on_fetch_error(requestId: number, payloadPtr: number, payloadLen: number): void;
|
|
47
|
+
__fui_on_file_pick_result(requestId: number, status: number, payloadPtr: number, payloadLen: number): void;
|
|
48
|
+
__fui_on_file_read_result(
|
|
49
|
+
requestId: number,
|
|
50
|
+
status: number,
|
|
51
|
+
offsetBytes: bigint,
|
|
52
|
+
fileSizeBytes: bigint,
|
|
53
|
+
payloadPtr: number,
|
|
54
|
+
payloadLen: number,
|
|
55
|
+
): void;
|
|
56
|
+
__fui_on_file_save_result(
|
|
57
|
+
requestId: number,
|
|
58
|
+
status: number,
|
|
59
|
+
writtenBytes: bigint,
|
|
60
|
+
payloadPtr: number,
|
|
61
|
+
payloadLen: number,
|
|
62
|
+
): void;
|
|
63
|
+
__fui_on_file_writer_created(requestId: number, status: number, payloadPtr: number, payloadLen: number): void;
|
|
64
|
+
__fui_on_file_write_result(
|
|
65
|
+
requestId: number,
|
|
66
|
+
status: number,
|
|
67
|
+
writtenBytes: bigint,
|
|
68
|
+
totalWrittenBytes: bigint,
|
|
69
|
+
payloadPtr: number,
|
|
70
|
+
payloadLen: number,
|
|
71
|
+
): void;
|
|
72
|
+
__fui_on_file_finish_result(
|
|
73
|
+
requestId: number,
|
|
74
|
+
status: number,
|
|
75
|
+
writtenBytes: bigint,
|
|
76
|
+
payloadPtr: number,
|
|
77
|
+
payloadLen: number,
|
|
78
|
+
): void;
|
|
79
|
+
__fui_on_file_worker_process_progress(
|
|
80
|
+
requestId: number,
|
|
81
|
+
copiedBytes: bigint,
|
|
82
|
+
totalBytes: bigint,
|
|
83
|
+
payloadPtr: number,
|
|
84
|
+
payloadLen: number,
|
|
85
|
+
): void;
|
|
86
|
+
__fui_on_file_worker_process_chunk(
|
|
87
|
+
requestId: number,
|
|
88
|
+
offsetBytes: bigint,
|
|
89
|
+
fileSizeBytes: bigint,
|
|
90
|
+
payloadPtr: number,
|
|
91
|
+
payloadLen: number,
|
|
92
|
+
): void;
|
|
93
|
+
__fui_on_file_worker_process_complete(
|
|
94
|
+
requestId: number,
|
|
95
|
+
writtenBytes: bigint,
|
|
96
|
+
payloadPtr: number,
|
|
97
|
+
payloadLen: number,
|
|
98
|
+
): void;
|
|
99
|
+
__fui_on_file_worker_process_error(requestId: number, status: number, payloadPtr: number, payloadLen: number): void;
|
|
100
|
+
__fui_on_context_menu(handle: bigint, x: number, y: number): void;
|
|
101
|
+
__fui_hide_active_context_menu(): void;
|
|
102
|
+
__fui_key_buffer(): number;
|
|
103
|
+
__fui_text_buffer(): number;
|
|
104
|
+
__fui_text_buffer_size(): number;
|
|
105
|
+
__fui_on_focus_changed(handle: bigint, focused: boolean): void;
|
|
106
|
+
__fui_on_text_changed(handle: bigint, textPtr: number, textLen: number): void;
|
|
107
|
+
__fui_on_text_replaced(handle: bigint, start: number, end: number, textPtr: number, textLen: number): void;
|
|
108
|
+
__fui_on_selection_changed(handle: bigint, start: number, end: number): void;
|
|
109
|
+
__fui_on_key_event(eventType: number, keyPtr: number, keyLen: number, modifiers: number): number;
|
|
110
|
+
__fui_on_scroll(
|
|
111
|
+
handle: bigint,
|
|
112
|
+
offsetX: number,
|
|
113
|
+
offsetY: number,
|
|
114
|
+
contentWidth: number,
|
|
115
|
+
contentHeight: number,
|
|
116
|
+
viewportWidth: number,
|
|
117
|
+
viewportHeight: number,
|
|
118
|
+
): void;
|
|
119
|
+
__fui_on_cross_selection_changed(handle: bigint, textPtr: number, textLen: number): void;
|
|
120
|
+
__fui_on_route_changed(routePtr: number, routeLen: number): void;
|
|
121
|
+
__fui_on_viewport_changed(width: number, height: number): void;
|
|
122
|
+
__fui_on_system_dark_mode_changed(isDark: boolean): void;
|
|
123
|
+
__fui_on_svg_loaded(svgId: number, width: number, height: number): void;
|
|
124
|
+
__fui_on_svg_failed(svgId: number, errorPtr: number, errorLen: number): void;
|
|
125
|
+
__fui_on_texture_loaded(textureId: number, width: number, height: number): void;
|
|
126
|
+
__fui_on_texture_failed(textureId: number, errorPtr: number, errorLen: number): void;
|
|
127
|
+
__fui_on_frame(timestampMs: number): void;
|
|
128
|
+
__fui_on_timer(timerId: number): void;
|
|
129
|
+
__fui_on_worker_progress(workerId: number, textPtr: number, textLen: number): void;
|
|
130
|
+
__fui_on_worker_complete(workerId: number, textPtr: number, textLen: number): void;
|
|
131
|
+
__fui_on_worker_error(workerId: number, textPtr: number, textLen: number): void;
|
|
132
|
+
__fui_restore_persisted_ui_state?(): void;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface HarnessContext<Exports extends HarnessExports> {
|
|
136
|
+
readonly runtime: BridgeRuntime;
|
|
137
|
+
readonly exports: Exports;
|
|
138
|
+
waitForFrame(): Promise<void>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface HarnessOptions<Exports extends HarnessExports> {
|
|
142
|
+
wasmPath: string;
|
|
143
|
+
run?(exports: Exports): void;
|
|
144
|
+
onStateUpdated?(state: HarnessState): void;
|
|
145
|
+
onReady?(context: HarnessContext<Exports>): void | Promise<void>;
|
|
146
|
+
onDispose?(exports: Exports): void;
|
|
147
|
+
onError?(error: unknown): void;
|
|
148
|
+
showLoadingOverlay?: boolean;
|
|
149
|
+
hostEvents?: HostEventsDefinition;
|
|
150
|
+
hostServices?: HostServicesDefinition;
|
|
151
|
+
workerHostServices?: WorkerHostServicesBundleConfig;
|
|
152
|
+
persistedRestoreMode?: 'initial' | 'pop' | 'none';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface HarnessAppOptions<Exports extends HarnessExports> extends HarnessOptions<Exports> {
|
|
156
|
+
run(exports: Exports): void;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export type HarnessNavigationMode = 'push' | 'replace' | 'pop';
|
|
160
|
+
|
|
161
|
+
export interface HarnessController {
|
|
162
|
+
readonly runtime: BridgeRuntime;
|
|
163
|
+
waitForFrame(): Promise<void>;
|
|
164
|
+
loadApp<Exports extends HarnessExports>(options: HarnessAppOptions<Exports>): Promise<HarnessContext<Exports>>;
|
|
165
|
+
unloadApp(): Promise<void>;
|
|
166
|
+
recreateRuntime(): Promise<BridgeRuntime>;
|
|
167
|
+
setSameOriginNavigationHandler(
|
|
168
|
+
handler: ((target: URL, mode: HarnessNavigationMode) => void | Promise<void>) | null,
|
|
169
|
+
): void;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface ManagedHarnessOptions {
|
|
173
|
+
onReady?(controller: HarnessController): void | Promise<void>;
|
|
174
|
+
onError?(error: unknown): void;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface ManagedHistoryState {
|
|
178
|
+
readonly href: string;
|
|
179
|
+
readonly uiSnapshotId?: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface HarnessDebugApi {
|
|
183
|
+
flush(): Promise<void>;
|
|
184
|
+
pointerEvent(type: number, handle: WasmHandleLike, x: number, y: number, modifiers?: number): Promise<void>;
|
|
185
|
+
focusChanged(handle: WasmHandleLike, focused: boolean): Promise<void>;
|
|
186
|
+
keyEvent(type: number, key: string, modifiers?: number): Promise<void>;
|
|
187
|
+
navigateTo(target: string): Promise<void>;
|
|
188
|
+
scroll(
|
|
189
|
+
handle: WasmHandleLike,
|
|
190
|
+
offsetX: number,
|
|
191
|
+
offsetY: number,
|
|
192
|
+
contentWidth: number,
|
|
193
|
+
contentHeight: number,
|
|
194
|
+
viewportWidth: number,
|
|
195
|
+
viewportHeight: number,
|
|
196
|
+
): Promise<void>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
declare global {
|
|
200
|
+
interface Window {
|
|
201
|
+
__effindomRuntime?: EffinDomRuntimeConfig;
|
|
202
|
+
__fui_debug?: HarnessDebugApi;
|
|
203
|
+
__fuiUrlPreviewText?: string;
|
|
204
|
+
}
|
|
205
|
+
}
|