@effindomv2/fui-as 0.1.36 → 0.1.39

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 (119) hide show
  1. package/browser/routed-app-conventions.ts +1 -1
  2. package/browser/src/common-harness.ts +1 -27
  3. package/browser/src/index.ts +19 -26
  4. package/browser/src/routed-harness.ts +1 -194
  5. package/browser/src/shared-browser.ts +6 -0
  6. package/package.json +26 -16
  7. package/scripts/build.sh +51 -47
  8. package/scripts/framework-host-services.ts +1 -1
  9. package/scripts/generate-host-events.ts +15 -241
  10. package/scripts/generate-host-services.ts +13 -261
  11. package/scripts/hostgen/assemblyscript-host-events.ts +165 -0
  12. package/scripts/hostgen/assemblyscript-host-services.ts +207 -0
  13. package/scripts/hostgen/common.ts +73 -0
  14. package/scripts/hostgen/registry.ts +159 -0
  15. package/src/Fui.ts +70 -4
  16. package/src/FuiExports.ts +13 -1
  17. package/src/bindings/ui.ts +165 -8
  18. package/src/controls/Button.ts +98 -71
  19. package/src/controls/Checkbox.ts +23 -21
  20. package/src/controls/ComboBox.ts +1201 -0
  21. package/src/controls/ContextMenu.ts +279 -165
  22. package/src/controls/Dialog.ts +91 -17
  23. package/src/controls/Dropdown.ts +101 -333
  24. package/src/controls/NavLink.ts +29 -6
  25. package/src/controls/Popup.ts +10 -5
  26. package/src/controls/ProgressBar.ts +1 -2
  27. package/src/controls/RadioButton.ts +34 -13
  28. package/src/controls/RadioGroup.ts +32 -19
  29. package/src/controls/Slider.ts +21 -23
  30. package/src/controls/Switch.ts +34 -17
  31. package/src/controls/index.ts +2 -1
  32. package/src/controls/internal/ButtonPresenter.ts +5 -2
  33. package/src/controls/internal/CheckboxIndicatorPresenter.ts +10 -10
  34. package/src/controls/internal/DropdownChevronPresenter.ts +3 -3
  35. package/src/controls/internal/DropdownFieldPresenter.ts +6 -6
  36. package/src/controls/internal/DropdownOptionRowPresenter.ts +5 -4
  37. package/src/controls/internal/PressableLabeledControl.ts +4 -7
  38. package/src/controls/internal/RadioIndicatorPresenter.ts +7 -7
  39. package/src/controls/internal/SelectablePopupList.ts +463 -0
  40. package/src/controls/internal/SliderPresenter.ts +5 -5
  41. package/src/controls/internal/SwitchIndicatorPresenter.ts +63 -17
  42. package/src/controls/internal/TextInputCore.ts +307 -41
  43. package/src/controls/internal/TextInputPresenter.ts +2 -2
  44. package/src/controls/templating.ts +10 -0
  45. package/src/core/Application.ts +19 -9
  46. package/src/core/Bitmap.ts +219 -1
  47. package/src/core/ContextMenuManager.ts +128 -44
  48. package/src/core/DebugApi.ts +17 -2
  49. package/src/core/DragGesture.ts +8 -7
  50. package/src/core/EventRouter.ts +332 -12
  51. package/src/core/Fetch.ts +15 -7
  52. package/src/core/FetchFfi.ts +1 -15
  53. package/src/core/File.ts +75 -34
  54. package/src/core/FocusAdornerManager.ts +3 -3
  55. package/src/core/FocusVisibility.ts +7 -0
  56. package/src/core/FrameScheduler.ts +126 -0
  57. package/src/core/ImageSampling.ts +43 -0
  58. package/src/core/Logger.ts +2 -0
  59. package/src/core/MobileTextSelectionToolbarManager.ts +901 -0
  60. package/src/core/Node.ts +1039 -97
  61. package/src/core/Platform.ts +16 -16
  62. package/src/core/SelectionHandleAdornerManager.ts +697 -0
  63. package/src/core/Theme.ts +34 -20
  64. package/src/core/ToolTipManager.ts +3 -3
  65. package/src/core/Typography.ts +411 -19
  66. package/src/core/Worker.ts +69 -33
  67. package/src/core/event_exports.ts +123 -5
  68. package/src/core/ffi.ts +5 -746
  69. package/src/core/generated/FetchHostAbi.ts +10 -0
  70. package/src/core/generated/FrameworkHostServices.ts +1 -1
  71. package/src/core/generated/HostAbi.ts +220 -0
  72. package/src/core/generated/UiAbi.ts +356 -0
  73. package/src/core/generated/UiEnums.ts +186 -0
  74. package/src/drawing/DrawCallback.ts +34 -0
  75. package/src/drawing/DrawContext.ts +189 -0
  76. package/src/drawing/Paint.ts +34 -0
  77. package/src/drawing/Path.ts +40 -0
  78. package/src/nodes/CustomDrawable.ts +61 -0
  79. package/src/nodes/FlexBox.ts +57 -11
  80. package/src/nodes/Grid.ts +5 -5
  81. package/src/nodes/Image.ts +20 -1
  82. package/src/nodes/RichText.ts +36 -25
  83. package/src/nodes/ScrollBar.ts +2 -1
  84. package/src/nodes/ScrollBox.ts +7 -2
  85. package/src/nodes/ScrollView.ts +17 -6
  86. package/src/nodes/Svg.ts +18 -1
  87. package/src/nodes/Text.ts +1 -1
  88. package/src/nodes/TextCore.ts +115 -20
  89. package/src/nodes/index.ts +3 -1
  90. package/src/text/DynamicTextLayout.ts +275 -0
  91. package/src/text/TextLayout.ts +308 -0
  92. package/src/worker/Worker.ts +3 -36
  93. package/src/worker/WorkerJob.ts +5 -9
  94. package/src/worker/ffi.ts +0 -6
  95. package/browser/src/common-harness/host-imports.ts +0 -447
  96. package/browser/src/common-harness/interop.ts +0 -39
  97. package/browser/src/common-harness/managed-harness-bitmap-host.ts +0 -92
  98. package/browser/src/common-harness/managed-harness-fetch-host.ts +0 -201
  99. package/browser/src/common-harness/managed-harness-file-host.ts +0 -1066
  100. package/browser/src/common-harness/managed-harness-file-payloads.ts +0 -143
  101. package/browser/src/common-harness/managed-harness-file-types.ts +0 -71
  102. package/browser/src/common-harness/managed-harness-session.ts +0 -15
  103. package/browser/src/common-harness/managed-harness.ts +0 -1451
  104. package/browser/src/common-harness/managed-history.ts +0 -168
  105. package/browser/src/common-harness/persisted-restore-policy.ts +0 -50
  106. package/browser/src/common-harness/persisted-ui-state-controller.ts +0 -309
  107. package/browser/src/common-harness/text-session-bridge.ts +0 -452
  108. package/browser/src/common-harness/types.ts +0 -205
  109. package/browser/src/common-harness/ui-chrome.ts +0 -206
  110. package/browser/src/common-harness/ui-imports.ts +0 -553
  111. package/browser/src/common-harness/wasm-module-cache.ts +0 -47
  112. package/browser/src/file-processing-worker.ts +0 -89
  113. package/browser/src/host-events.ts +0 -118
  114. package/browser/src/host-services.ts +0 -483
  115. package/browser/src/persisted-ui-state.ts +0 -206
  116. package/browser/src/routed-app-conventions.ts +0 -292
  117. package/browser/src/worker-bootstrap.ts +0 -916
  118. package/browser/src/worker-manager.ts +0 -234
  119. package/browser/src/worker-types.ts +0 -68
@@ -3,9 +3,11 @@ import { ContextMenuManager } from "./ContextMenuManager";
3
3
  import { disposeAllFetchRequests } from "./Fetch";
4
4
  import { disposeAllFileRequests } from "./File";
5
5
  import { FocusAdornerManager } from "./FocusAdornerManager";
6
+ import { MobileTextSelectionToolbarManager } from "./MobileTextSelectionToolbarManager";
7
+ import { SelectionHandleAdornerManager } from "./SelectionHandleAdornerManager";
6
8
  import { ToolTipManager } from "./ToolTipManager";
7
9
  import * as ui from "../bindings/ui";
8
- import { flushCommit, markNeedsCommit, resetCommitState } from "./FrameScheduler";
10
+ import { flushCommit, fireLoadedCallbacks, markNeedsCommit, resetCommitState } from "./FrameScheduler";
9
11
  import { HandleValue, Unit } from "./ffi";
10
12
  import { Node } from "./Node";
11
13
  import { cancelAllTimers } from "./Timers";
@@ -29,12 +31,20 @@ function createEmptyPage(): Node {
29
31
  return new FlexBox().width(100.0, Unit.Percent).height(100.0, Unit.Percent);
30
32
  }
31
33
 
34
+ function disposeAppScopedAsyncWork(): void {
35
+ disposeAllFetchRequests();
36
+ disposeAllFileRequests();
37
+ disposeAllWorkers();
38
+ }
39
+
32
40
  class ApplicationShell extends FlexBox {
33
41
  constructor(root: Node) {
34
42
  super();
35
43
  this.width(100.0, Unit.Percent)
36
44
  .height(100.0, Unit.Percent)
37
45
  .child(root)
46
+ .child(SelectionHandleAdornerManager.createDefaultHost())
47
+ .child(MobileTextSelectionToolbarManager.createDefaultHost())
38
48
  .child(FocusAdornerManager.createDefaultHost())
39
49
  .child(ContextMenuManager.createDefaultMenu())
40
50
  .child(ToolTipManager.createDefaultHost());
@@ -198,10 +208,10 @@ export class Application<TPage> {
198
208
  }
199
209
 
200
210
  cancelAllTimers();
201
- disposeAllFetchRequests();
202
- disposeAllFileRequests();
203
- disposeAllWorkers();
211
+ disposeAppScopedAsyncWork();
204
212
  FocusAdornerManager.clear();
213
+ SelectionHandleAdornerManager.clear();
214
+ MobileTextSelectionToolbarManager.clear();
205
215
  ToolTipManager.clear();
206
216
  resetCommitState();
207
217
  ui.reset();
@@ -211,6 +221,7 @@ export class Application<TPage> {
211
221
  ui.setRoot(shell.builtHandle);
212
222
  mountedShell = shell;
213
223
  mountedRoot = root;
224
+ fireLoadedCallbacks();
214
225
  markNeedsCommit();
215
226
  return root;
216
227
  }
@@ -224,10 +235,10 @@ export class Application<TPage> {
224
235
  mountedShell = null;
225
236
  mountedRoot = null;
226
237
  cancelAllTimers();
227
- disposeAllFetchRequests();
228
- disposeAllFileRequests();
229
- disposeAllWorkers();
238
+ disposeAppScopedAsyncWork();
230
239
  FocusAdornerManager.clear();
240
+ SelectionHandleAdornerManager.clear();
241
+ MobileTextSelectionToolbarManager.clear();
231
242
  ToolTipManager.clear();
232
243
  resetCommitState();
233
244
  }
@@ -330,8 +341,7 @@ export function __disposeApp(): void {
330
341
  return;
331
342
  }
332
343
  app.dispose();
333
- disposeAllFetchRequests();
334
- disposeAllWorkers();
344
+ disposeAppScopedAsyncWork();
335
345
  }
336
346
 
337
347
  export function __flushRenders(): void {
@@ -1,12 +1,74 @@
1
1
  import * as ui from "../bindings/ui";
2
+ import * as ffi from "../core/ffi";
2
3
  import { markTextureAssetReady } from "./Assets";
3
4
  import { Disposable } from "./Disposable";
4
5
  import { allocateDynamicTextureId } from "./DynamicAssetIds";
6
+ import { DrawContext } from "../drawing/DrawContext";
5
7
  import { throwObjectDisposed } from "./Errors";
8
+ import { LoadedEventArgs, onLoadedWith } from "./FrameScheduler";
6
9
  import { error } from "./Logger";
10
+ import { Node } from "./Node";
11
+ import { FontFace, FontsLoadedEventArgs } from "./Typography";
12
+ import { TextLayout } from "../text/TextLayout";
7
13
 
8
14
  const FUNCTION_BITMAP_CONSTRUCTOR = "Bitmap.constructor";
9
15
 
16
+ export class BitmapTextReadyEventArgs {
17
+ readonly node: Node;
18
+
19
+ constructor(node: Node) {
20
+ this.node = node;
21
+ }
22
+ }
23
+
24
+ export type BitmapTextReadyHandler<Owner> = (owner: Owner, event: BitmapTextReadyEventArgs) => void;
25
+
26
+ class BitmapTextReadyCallback {
27
+ constructor(
28
+ private readonly node: Node,
29
+ private readonly callback: (event: BitmapTextReadyEventArgs) => void,
30
+ ) {}
31
+
32
+ handleReady(): void {
33
+ onLoadedWith<BitmapTextReadyCallback>(this, handleBitmapTextLoaded);
34
+ }
35
+
36
+ handleLoaded(): void {
37
+ Bitmap.prepareText(this.node);
38
+ this.callback(new BitmapTextReadyEventArgs(this.node));
39
+ }
40
+ }
41
+
42
+ class BitmapTextReadyHandlerAction<Owner> {
43
+ constructor(
44
+ private readonly node: Node,
45
+ private readonly owner: Owner,
46
+ private readonly handler: BitmapTextReadyHandler<Owner>,
47
+ ) {}
48
+
49
+ handleReady(): void {
50
+ onLoadedWith<BitmapTextReadyHandlerAction<Owner>>(
51
+ this,
52
+ (action: BitmapTextReadyHandlerAction<Owner>, _event: LoadedEventArgs): void => {
53
+ action.handleLoaded();
54
+ },
55
+ );
56
+ }
57
+
58
+ handleLoaded(): void {
59
+ Bitmap.prepareText(this.node);
60
+ this.handler(this.owner, new BitmapTextReadyEventArgs(this.node));
61
+ }
62
+ }
63
+
64
+ function handleBitmapTextReady(callback: BitmapTextReadyCallback): void {
65
+ callback.handleReady();
66
+ }
67
+
68
+ function handleBitmapTextLoaded(callback: BitmapTextReadyCallback, _event: LoadedEventArgs): void {
69
+ callback.handleLoaded();
70
+ }
71
+
10
72
  function throwBitmapError(category: string, message: string): void {
11
73
  error(category, message);
12
74
  throw new Error(message);
@@ -23,12 +85,20 @@ function validateBitmapDimensions(width: u32, height: u32): u32 {
23
85
  return <u32>totalBytes;
24
86
  }
25
87
 
88
+ const MAX_DIRTY_RECTS: i32 = 16;
89
+ const RECT_STRIDE: i32 = 4; // x, y, w, h
90
+
26
91
  export class Bitmap implements Disposable {
27
92
  readonly width: u32;
28
93
  readonly height: u32;
29
94
  readonly textureId: u32;
30
95
  private pixelBytes: Uint8Array;
96
+ private offscreenId: u32 = 0;
97
+ private canvasUsed: bool = false;
98
+ private drawContext: DrawContext | null = null;
31
99
  private disposed: bool = false;
100
+ private dirtyRects: StaticArray<u32> = new StaticArray<u32>(MAX_DIRTY_RECTS * RECT_STRIDE);
101
+ private dirtyCount: i32 = 0;
32
102
 
33
103
  constructor(width: u32, height: u32) {
34
104
  const byteLength = validateBitmapDimensions(width, height);
@@ -36,6 +106,7 @@ export class Bitmap implements Disposable {
36
106
  this.height = height;
37
107
  this.textureId = allocateDynamicTextureId();
38
108
  this.pixelBytes = new Uint8Array(<i32>byteLength);
109
+ this.offscreenId = ffi.fui_canvas_create_offscreen(width, height);
39
110
  }
40
111
 
41
112
  pixels(): Uint8Array {
@@ -48,9 +119,152 @@ export class Bitmap implements Disposable {
48
119
  return pixels.length > 0 ? pixels.dataStart : 0;
49
120
  }
50
121
 
122
+ /**
123
+ * Returns a `DrawContext` targeting this bitmap's off‑screen raster
124
+ * surface. Drawing commands issued on the context are accumulated on
125
+ * the off‑screen canvas and uploaded to the GPU texture on the next
126
+ * `commit()` call.
127
+ *
128
+ * The returned `DrawContext` is valid for the lifetime of the `Bitmap`.
129
+ * Do not store it beyond the `Bitmap`'s disposal.
130
+ */
131
+ canvas(): DrawContext {
132
+ this.ensureNotDisposed("Bitmap.canvas");
133
+ this.canvasUsed = true;
134
+ if (this.drawContext !== null) {
135
+ return this.drawContext!;
136
+ }
137
+ const ptr = ffi.fui_canvas_get_offscreen_ptr(this.offscreenId);
138
+ const context = new DrawContext(ptr);
139
+ this.drawContext = context;
140
+ return context;
141
+ }
142
+
143
+ /**
144
+ * Renders a retained text node into this bitmap's pixel buffer.
145
+ * Call `Bitmap.prepareText(node)` before the commit that makes the
146
+ * prepared glyphs visible in core.
147
+ *
148
+ * @param node A built Text or RichText node sized to match this bitmap's dimensions.
149
+ */
150
+ render(node: Node, x: f32 = 0.0, y: f32 = 0.0, scale: f32 = 1.0): void {
151
+ this.ensureNotDisposed("Bitmap.render");
152
+ const handle = node.builtHandle;
153
+ if (handle == <u64>0) return;
154
+ ui.renderNodeToRgba(handle, this.width, this.height, this.pixelPtr(), <u32>this.pixelBytes.length, scale, x, y);
155
+ }
156
+
157
+ renderTextLayout(layout: TextLayout, x: f32 = 0.0, y: f32 = 0.0, scale: f32 = 1.0): void {
158
+ this.ensureNotDisposed("Bitmap.renderTextLayout");
159
+ if (!layout.isReady) {
160
+ error("TextLayout", "Bitmap.renderTextLayout() called before the TextLayout was ready; register onReady/onReadyWith and render after the callback.");
161
+ return;
162
+ }
163
+ this.render(layout._drawNode(), x, y, scale);
164
+ }
165
+
166
+ /** Queue text shaping for a built text node before the next CommitFrame. */
167
+ static prepareText(node: Node): void {
168
+ ui.prepareNode(node.builtHandle);
169
+ }
170
+
171
+ onTextReady(node: Node, onReady: (event: BitmapTextReadyEventArgs) => void): this {
172
+ Bitmap.onTextReady(node, onReady);
173
+ return this;
174
+ }
175
+
176
+ onTextReadyWith<Owner>(owner: Owner, node: Node, onReady: BitmapTextReadyHandler<Owner>): this {
177
+ Bitmap.onTextReadyWith<Owner>(owner, node, onReady);
178
+ return this;
179
+ }
180
+
181
+ static onTextReady(node: Node, onReady: (event: BitmapTextReadyEventArgs) => void): void {
182
+ const callback = new BitmapTextReadyCallback(node, onReady);
183
+ FontFace.whenFontsLoadedWith<BitmapTextReadyCallback>(
184
+ node._requiredFontIds(),
185
+ callback,
186
+ handleBitmapTextReady,
187
+ );
188
+ }
189
+
190
+ static onTextReadyWith<Owner>(owner: Owner, node: Node, onReady: BitmapTextReadyHandler<Owner>): void {
191
+ const callback = new BitmapTextReadyHandlerAction<Owner>(node, owner, onReady);
192
+ FontFace.whenFontsLoadedWith<BitmapTextReadyHandlerAction<Owner>>(
193
+ node._requiredFontIds(),
194
+ callback,
195
+ (action: BitmapTextReadyHandlerAction<Owner>, _event: FontsLoadedEventArgs): void => {
196
+ action.handleReady();
197
+ },
198
+ );
199
+ }
200
+
201
+ /**
202
+ * Mark a rectangular region as dirty. Call one or more times before
203
+ * commit(). If no dirty rects are registered, commit() uploads the
204
+ * entire buffer (current behavior, backward‑compatible).
205
+ *
206
+ * @param x Left edge, clamped to [0, width).
207
+ * @param y Top edge, clamped to [0, height).
208
+ * @param w Width, clamped so x+w ≤ width.
209
+ * @param h Height, clamped so y+h ≤ height.
210
+ */
211
+ dirtyRect(x: u32, y: u32, w: u32, h: u32): void {
212
+ if (w == 0 || h == 0) return;
213
+ if (x >= this.width) return;
214
+ if (y >= this.height) return;
215
+ const cx = x;
216
+ const cy = y;
217
+ const cw = x + w > this.width ? this.width - x : w;
218
+ const ch = y + h > this.height ? this.height - y : h;
219
+ if (this.dirtyCount < MAX_DIRTY_RECTS) {
220
+ const base = this.dirtyCount * RECT_STRIDE;
221
+ unchecked(this.dirtyRects[base] = cx);
222
+ unchecked(this.dirtyRects[base + 1] = cy);
223
+ unchecked(this.dirtyRects[base + 2] = cw);
224
+ unchecked(this.dirtyRects[base + 3] = ch);
225
+ this.dirtyCount++;
226
+ }
227
+ }
228
+
229
+ clearDirtyRects(): void {
230
+ this.dirtyCount = 0;
231
+ }
232
+
233
+ hasDirtyRects(): bool {
234
+ return this.dirtyCount > 0;
235
+ }
236
+
51
237
  commit(): u32 {
52
238
  this.ensureNotDisposed("Bitmap.commit");
53
- ui.bitmapCommit(this.textureId, this.pixelBytes, this.width, this.height);
239
+ if (this.canvasUsed) {
240
+ if (this.drawContext !== null) {
241
+ this.drawContext!.flush();
242
+ }
243
+ ffi.fui_canvas_read_offscreen_pixels(
244
+ this.offscreenId, this.pixelBytes.dataStart, this.width, this.height);
245
+ }
246
+ if (this.dirtyCount > 0) {
247
+ for (let i: i32 = 0; i < this.dirtyCount; i++) {
248
+ const base = i * RECT_STRIDE;
249
+ const drx = unchecked(this.dirtyRects[base]);
250
+ const dry = unchecked(this.dirtyRects[base + 1]);
251
+ const drw = unchecked(this.dirtyRects[base + 2]);
252
+ const drh = unchecked(this.dirtyRects[base + 3]);
253
+ const rectBytes = new Uint8Array(<i32>(drw * drh * 4));
254
+ for (let row: u32 = 0; row < drh; row++) {
255
+ const srcIdx: i32 = <i32>(dry + row) * <i32>this.width * 4 + <i32>drx * 4;
256
+ const dstIdx: i32 = <i32>row * <i32>drw * 4;
257
+ memory.copy(rectBytes.dataStart + dstIdx, this.pixelBytes.dataStart + srcIdx, <i32>(drw * 4));
258
+ }
259
+ ffi.fui_bitmap_commit_dirty(
260
+ this.textureId, rectBytes.dataStart, drw * drh * 4,
261
+ this.width, this.height, drx, dry, drw, drh,
262
+ );
263
+ }
264
+ this.dirtyCount = 0;
265
+ } else {
266
+ ui.bitmapCommit(this.textureId, this.pixelBytes, this.width, this.height);
267
+ }
54
268
  markTextureAssetReady(this.textureId, <f32>this.width, <f32>this.height);
55
269
  return this.textureId;
56
270
  }
@@ -60,8 +274,12 @@ export class Bitmap implements Disposable {
60
274
  return;
61
275
  }
62
276
  this.disposed = true;
277
+ if (this.offscreenId !== 0) {
278
+ ffi.fui_canvas_destroy_offscreen(this.offscreenId);
279
+ }
63
280
  ui.bitmapRelease(this.textureId);
64
281
  this.pixelBytes = new Uint8Array(0);
282
+ this.drawContext = null;
65
283
  }
66
284
 
67
285
  private ensureNotDisposed(functionName: string): void {
@@ -4,6 +4,7 @@ import { ContextMenu, ContextMenuAction, MenuItem } from "../controls/ContextMen
4
4
  import { NavLink } from "../controls/NavLink";
5
5
  import { TextInputCore } from "../controls/internal/TextInputCore";
6
6
  import { EventRouter } from "./EventRouter";
7
+ import { MobileTextSelectionToolbarManager } from "./MobileTextSelectionToolbarManager";
7
8
  import { HandleValue, KeyModifier, PointerEventType } from "./ffi";
8
9
  import {
9
10
  formatPrimaryShortcutLabel,
@@ -13,13 +14,13 @@ import {
13
14
  getPlatformFamily,
14
15
  PlatformFamily,
15
16
  } from "./Platform";
16
- import { Node } from "./Node";
17
+ import { ContextMenuEventArgs, Node, VisibilityChangedEventArgs } from "./Node";
17
18
  import { Image } from "../nodes/Image";
18
19
  import { Svg } from "../nodes/Svg";
19
20
  import { TextCore } from "../nodes/TextCore";
20
21
 
21
- function handleMenuVisibilityChanged(visible: bool): void {
22
- ContextMenuManager.handleMenuVisibilityChanged(visible);
22
+ function handleMenuVisibilityChanged(event: VisibilityChangedEventArgs): void {
23
+ ContextMenuManager.handleMenuVisibilityChanged(event.visible);
23
24
  }
24
25
 
25
26
  function appendMenuSection(items: Array<MenuItem>, section: Array<MenuItem>): void {
@@ -96,18 +97,93 @@ export class ContextMenuManager {
96
97
  if (menu === null) {
97
98
  return;
98
99
  }
100
+ const targetNode = EventRouter.getRegisteredNode(handle);
101
+ if (this.invokeCustomContextMenuHandler(targetNode, x, y)) {
102
+ return;
103
+ }
104
+ const items = this.buildBuiltInItems(handle, x, y, true);
99
105
 
100
- this.releaseActiveMenuLinkPreview();
106
+ const navigationItems = new Array<MenuItem>();
107
+ if (ffi.fui_can_navigate_back()) {
108
+ navigationItems.push(new MenuItem("Back", ContextMenuAction.NavigateBack, null, 0, resolveHistoryShortcutLabel(false)));
109
+ }
110
+ if (ffi.fui_can_navigate_forward()) {
111
+ navigationItems.push(new MenuItem("Forward", ContextMenuAction.NavigateForward, null, 0, resolveHistoryShortcutLabel(true)));
112
+ }
113
+ navigationItems.push(
114
+ new MenuItem("Reload Page", ContextMenuAction.ReloadPage, null, 0, formatPrimaryShortcutLabel("r")),
115
+ );
116
+ appendMenuSection(items, navigationItems);
117
+ if (items.length == 0) {
118
+ this.releaseActiveMenuLinkPreview();
119
+ return;
120
+ }
121
+ menu.items(items);
122
+ menu.showFromContextPointer(null, x, y);
123
+ }
124
+
125
+ static showForLongPress(handle: u64, x: f32, y: f32): bool {
126
+ const items = this.buildBuiltInItems(handle, x, y, false);
127
+ if (items.length == 0) {
128
+ this.releaseActiveMenuLinkPreview();
129
+ return false;
130
+ }
131
+ return MobileTextSelectionToolbarManager.showItemsAt(items, x, y);
132
+ }
133
+
134
+ static canShowForHandle(handle: u64): bool {
101
135
  const targetNode = EventRouter.getRegisteredNode(handle);
102
136
  let contextNode = targetNode;
103
137
  while (contextNode !== null) {
104
138
  if (contextNode.isContextMenuDisabled) {
105
- return;
139
+ return false;
140
+ }
141
+ contextNode = contextNode.parentNode;
142
+ }
143
+ return true;
144
+ }
145
+
146
+ static hideActiveMenu(): void {
147
+ ContextMenu.hideActiveMenu();
148
+ }
149
+
150
+ static handleMenuVisibilityChanged(visible: bool): void {
151
+ if (!visible) {
152
+ this.releaseActiveMenuLinkPreview();
153
+ }
154
+ }
155
+
156
+ private static resolveActiveMenu(): ContextMenu | null {
157
+ if (this.menu !== null) {
158
+ return this.menu;
159
+ }
160
+ return this.defaultMenu;
161
+ }
162
+
163
+ private static invokeCustomContextMenuHandler(targetNode: Node | null, x: f32, y: f32): bool {
164
+ this.releaseActiveMenuLinkPreview();
165
+ let contextNode = targetNode;
166
+ while (contextNode !== null) {
167
+ if (contextNode.isContextMenuDisabled) {
168
+ return true;
106
169
  }
107
170
  const handler = contextNode.contextMenuHandler;
108
171
  if (handler !== null) {
109
- handler(targetNode, x, y);
110
- return;
172
+ handler(new ContextMenuEventArgs(targetNode, x, y));
173
+ return true;
174
+ }
175
+ contextNode = contextNode.parentNode;
176
+ }
177
+ return false;
178
+ }
179
+
180
+ private static buildBuiltInItems(handle: u64, x: f32, y: f32, clearSelectionOnBackgroundMiss: bool): Array<MenuItem> {
181
+ this.releaseActiveMenuLinkPreview();
182
+ const targetNode = EventRouter.getRegisteredNode(handle);
183
+ let contextNode = targetNode;
184
+ while (contextNode !== null) {
185
+ if (contextNode.isContextMenuDisabled) {
186
+ return new Array<MenuItem>();
111
187
  }
112
188
  contextNode = contextNode.parentNode;
113
189
  }
@@ -118,8 +194,8 @@ export class ContextMenuManager {
118
194
  link.pinPreviewForContextMenu();
119
195
  this.activeMenuLink = link;
120
196
  const linkItems = new Array<MenuItem>();
121
- linkItems.push(new MenuItem("Open Link in New Tab", ContextMenuAction.OpenLinkInNewTab, link.href));
122
- linkItems.push(new MenuItem("Open Link", ContextMenuAction.OpenLink, link.href));
197
+ linkItems.push(new MenuItem("New Tab", ContextMenuAction.OpenLinkInNewTab, link.href));
198
+ linkItems.push(new MenuItem("Open", ContextMenuAction.OpenLink, link.href));
123
199
  appendMenuSection(items, linkItems);
124
200
  }
125
201
 
@@ -132,50 +208,22 @@ export class ContextMenuManager {
132
208
  const imageUrl = this.resolveImageUrl(targetNode, x, y);
133
209
  if (imageUrl !== null && imageUrl.length > 0) {
134
210
  const imageItems = new Array<MenuItem>();
135
- imageItems.push(new MenuItem("Open Image in New Tab", ContextMenuAction.OpenImageInNewTab, imageUrl));
136
- imageItems.push(new MenuItem("Open Image", ContextMenuAction.OpenImage, imageUrl));
211
+ imageItems.push(new MenuItem("New Tab", ContextMenuAction.OpenImageInNewTab, imageUrl));
212
+ imageItems.push(new MenuItem("Open", ContextMenuAction.OpenImage, imageUrl));
137
213
  appendMenuSection(items, imageItems);
138
214
  }
139
215
 
140
216
  if (selectionHit && textTarget === null && this.currentSelectionText.length > 0) {
141
217
  const selectionItems = new Array<MenuItem>();
142
- selectionItems.push(new MenuItem("Copy", ContextMenuAction.CopyCurrentSelection));
218
+ selectionItems.push(new MenuItem("Copy", ContextMenuAction.CopyCurrentSelection, this.currentSelectionText));
143
219
  appendMenuSection(items, selectionItems);
144
- } else if (!selectionHit && textTarget === null && link === null && imageUrl === null) {
220
+ } else if (clearSelectionOnBackgroundMiss && !selectionHit && textTarget === null && link === null && imageUrl === null) {
145
221
  this.currentSelectionText = "";
146
222
  this.currentSelectionHandleHints.length = 0;
147
223
  ui.clearCurrentSelection();
148
224
  }
149
225
 
150
- const navigationItems = new Array<MenuItem>();
151
- if (ffi.fui_can_navigate_back()) {
152
- navigationItems.push(new MenuItem("Back", ContextMenuAction.NavigateBack, null, 0, resolveHistoryShortcutLabel(false)));
153
- }
154
- if (ffi.fui_can_navigate_forward()) {
155
- navigationItems.push(new MenuItem("Forward", ContextMenuAction.NavigateForward, null, 0, resolveHistoryShortcutLabel(true)));
156
- }
157
- navigationItems.push(
158
- new MenuItem("Reload Page", ContextMenuAction.ReloadPage, null, 0, formatPrimaryShortcutLabel("r")),
159
- );
160
- appendMenuSection(items, navigationItems);
161
- menu.show(items, x, y);
162
- }
163
-
164
- static hideActiveMenu(): void {
165
- ContextMenu.hideActiveMenu();
166
- }
167
-
168
- static handleMenuVisibilityChanged(visible: bool): void {
169
- if (!visible) {
170
- this.releaseActiveMenuLinkPreview();
171
- }
172
- }
173
-
174
- private static resolveActiveMenu(): ContextMenu | null {
175
- if (this.menu !== null) {
176
- return this.menu;
177
- }
178
- return this.defaultMenu;
226
+ return items;
179
227
  }
180
228
 
181
229
  private static resolveNavLink(node: Node | null): NavLink | null {
@@ -227,6 +275,8 @@ export class ContextMenuManager {
227
275
 
228
276
  const selectionStart = this.resolveSelectionStart(target);
229
277
  const selectionEnd = this.resolveSelectionEnd(target);
278
+ const selectionStartByte = this.resolveSelectionStartByte(target);
279
+ const selectionEndByte = this.resolveSelectionEndByte(target);
230
280
  const content = this.resolveTextContent(target);
231
281
  const hasSelection = ui.hasTextSelection(handle) || ffi.fui_has_text_selection_snapshot(handle) || selectionStart != selectionEnd;
232
282
  const hasText = content.length > 0;
@@ -243,6 +293,9 @@ export class ContextMenuManager {
243
293
  formatUndoShortcutLabel(),
244
294
  !ui.canUndoTextEdit(handle),
245
295
  handle,
296
+ 0,
297
+ 0,
298
+ true,
246
299
  ),
247
300
  );
248
301
  items.push(
@@ -254,6 +307,9 @@ export class ContextMenuManager {
254
307
  formatRedoShortcutLabel(),
255
308
  !ui.canRedoTextEdit(handle),
256
309
  handle,
310
+ 0,
311
+ 0,
312
+ true,
257
313
  ),
258
314
  );
259
315
  items.push(MenuItem.separator());
@@ -266,7 +322,10 @@ export class ContextMenuManager {
266
322
  formatPrimaryShortcutLabel("x"),
267
323
  !hasSelection,
268
324
  handle,
269
- ).withSelectionRange(selectionStart, selectionEnd),
325
+ 0,
326
+ 0,
327
+ true,
328
+ ).withSelectionRange(selectionStartByte, selectionEndByte),
270
329
  );
271
330
  items.push(
272
331
  new MenuItem(
@@ -277,6 +336,9 @@ export class ContextMenuManager {
277
336
  formatPrimaryShortcutLabel("c"),
278
337
  !hasSelection,
279
338
  handle,
339
+ 0,
340
+ 0,
341
+ true,
280
342
  ),
281
343
  );
282
344
  items.push(
@@ -288,6 +350,9 @@ export class ContextMenuManager {
288
350
  formatPrimaryShortcutLabel("v"),
289
351
  false,
290
352
  handle,
353
+ 0,
354
+ 0,
355
+ true,
291
356
  ),
292
357
  );
293
358
  items.push(
@@ -299,6 +364,9 @@ export class ContextMenuManager {
299
364
  formatPrimaryShortcutLabel("a"),
300
365
  !hasText,
301
366
  handle,
367
+ 0,
368
+ 0,
369
+ true,
302
370
  ),
303
371
  );
304
372
  return items;
@@ -309,7 +377,7 @@ export class ContextMenuManager {
309
377
  new MenuItem(
310
378
  "Copy",
311
379
  ContextMenuAction.CopyCurrentSelection,
312
- null,
380
+ currentSelectionText,
313
381
  0,
314
382
  formatPrimaryShortcutLabel("c"),
315
383
  ),
@@ -368,6 +436,22 @@ export class ContextMenuManager {
368
436
  return target.selectionEnd;
369
437
  }
370
438
 
439
+ private static resolveSelectionStartByte(target: TextCore): u32 {
440
+ const parent = target.parentNode;
441
+ if (parent instanceof TextInputCore) {
442
+ return changetype<TextInputCore>(parent).selectionStartByteOffset;
443
+ }
444
+ return target.selectionStart;
445
+ }
446
+
447
+ private static resolveSelectionEndByte(target: TextCore): u32 {
448
+ const parent = target.parentNode;
449
+ if (parent instanceof TextInputCore) {
450
+ return changetype<TextInputCore>(parent).selectionEndByteOffset;
451
+ }
452
+ return target.selectionEnd;
453
+ }
454
+
371
455
  private static resolveTextContent(target: TextCore): string {
372
456
  const parent = target.parentNode;
373
457
  if (parent instanceof TextInputCore) {
@@ -1,12 +1,27 @@
1
1
  import {
2
2
  __fui_on_focus_changed,
3
3
  __fui_on_key_event,
4
- __fui_on_pointer_event,
4
+ __fui_on_pointer_event_with_metadata,
5
5
  __fui_on_scroll,
6
6
  } from "./event_exports";
7
+ import { PointerType } from "./Node";
7
8
 
8
9
  export function __fui_debug_pointer_event(eventType: u32, handle: u64, x: f32, y: f32, modifiers: u32 = 0): void {
9
- __fui_on_pointer_event(eventType, handle, x, y, modifiers);
10
+ __fui_on_pointer_event_with_metadata(
11
+ eventType,
12
+ handle,
13
+ x,
14
+ y,
15
+ modifiers,
16
+ -1,
17
+ <u32>PointerType.Mouse,
18
+ 0,
19
+ 0,
20
+ 0.0,
21
+ 0.0,
22
+ 0.0,
23
+ 0,
24
+ );
10
25
  }
11
26
 
12
27
  export function __fui_debug_focus_changed(handle: u64, focused: bool): void {