@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
@@ -16,10 +16,11 @@ import {
16
16
  import { HandlerAction } from "../core/Action";
17
17
  import { Disposable, disposeAll } from "../core/Disposable";
18
18
  import { navigateTo } from "../core/Navigation";
19
+ import { Node, PointerEventArgs, VisibilityChangedEventArgs } from "../core/Node";
19
20
  import { Theme, activeTheme } from "../core/Theme";
20
21
  import { warn } from "../core/Logger";
21
22
  import { FontFamily, FontStyle, FontWeight } from "../core/Typography";
22
- import { FlexBox, Grid, Portal, TextCore } from "../nodes";
23
+ import { Border, FlexBox, Grid, Portal, TextCore } from "../nodes";
23
24
  import { PopupPresenter } from "./internal/PopupPresenter";
24
25
 
25
26
  const MENU_WIDTH: f32 = 220.0;
@@ -27,6 +28,10 @@ const MENU_SEPARATOR_HEIGHT: f32 = 9.0;
27
28
  const MENU_EDGE_PADDING: f32 = 8.0;
28
29
  const DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA: f32 = 10.0;
29
30
 
31
+ function isPrimaryActivationPointer(event: PointerEventArgs): bool {
32
+ return event.button == 0 || event.pointerType == 2 || event.pointerType == 3;
33
+ }
34
+
30
35
  enum MenuItemKind {
31
36
  Action = 0,
32
37
  Separator = 1,
@@ -34,17 +39,20 @@ enum MenuItemKind {
34
39
 
35
40
  class ContextMenuEntry extends Grid {
36
41
  private readonly labelNode: TextCore = new TextCore("")
37
- .font(activeTheme.value.contextMenu.item.fontId, activeTheme.value.contextMenu.item.fontSize)
42
+ .fontFamily(activeTheme.value.contextMenu.item.fontFamily)
43
+ .fontSize(activeTheme.value.contextMenu.item.fontSize)
38
44
  .textColor(activeTheme.value.contextMenu.item.textColor)
39
45
  .overflow(TextOverflow.Ellipsis)
40
46
  .selectable(false) as TextCore;
41
47
  private readonly shortcutNode: TextCore = new TextCore("")
42
- .font(activeTheme.value.contextMenu.item.fontId, activeTheme.value.contextMenu.item.fontSize)
48
+ .fontFamily(activeTheme.value.contextMenu.item.fontFamily)
49
+ .fontSize(activeTheme.value.contextMenu.item.fontSize)
43
50
  .textColor(activeTheme.value.colors.textMuted)
44
51
  .textAlign(TextAlign.Left)
45
52
  .selectable(false) as TextCore;
46
53
  private readonly slot: i32;
47
54
  private hovered: bool = false;
55
+ private pressed: bool = false;
48
56
  private disabled: bool = false;
49
57
  private itemHeightValue: f32 = activeTheme.value.contextMenu.item.height;
50
58
  private paddingLeftValue: f32 = activeTheme.value.contextMenu.item.paddingLeft;
@@ -56,12 +64,10 @@ class ContextMenuEntry extends Grid {
56
64
  private shortcutTextColorValue: u32 = activeTheme.value.colors.textMuted;
57
65
  private backgroundColorValue: u32 = activeTheme.value.contextMenu.item.background;
58
66
  private hoverBackgroundColorValue: u32 = activeTheme.value.contextMenu.item.hoverBackground;
59
- private fontIdValue: u32 = activeTheme.value.contextMenu.item.fontId;
60
67
  private fontFamilyValue: FontFamily = activeTheme.value.contextMenu.item.fontFamily;
61
68
  private fontSizeValue: f32 = activeTheme.value.contextMenu.item.fontSize;
62
69
  private fontWeightValue: FontWeight = FontWeight.Regular;
63
70
  private fontStyleValue: FontStyle = FontStyle.Normal;
64
- private usesDirectFontId: bool = false;
65
71
 
66
72
  constructor(slot: i32) {
67
73
  super();
@@ -94,6 +100,8 @@ class ContextMenuEntry extends Grid {
94
100
  }
95
101
 
96
102
  item(item: MenuItem): this {
103
+ this.hovered = false;
104
+ this.pressed = false;
97
105
  this.disabled = item.disabled;
98
106
  this.semanticLabel(item.label);
99
107
  this.semanticDisabled(this.disabled);
@@ -113,12 +121,10 @@ class ContextMenuEntry extends Grid {
113
121
  textColor: u32,
114
122
  backgroundColor: u32,
115
123
  hoverBackgroundColor: u32,
116
- fontId: u32,
117
124
  fontFamily: FontFamily,
118
125
  fontSize: f32,
119
126
  fontWeight: FontWeight,
120
127
  fontStyle: FontStyle,
121
- usesDirectFontId: bool,
122
128
  ): void {
123
129
  this.itemHeightValue = itemHeight;
124
130
  this.paddingLeftValue = paddingLeft;
@@ -129,12 +135,10 @@ class ContextMenuEntry extends Grid {
129
135
  this.textColorValue = textColor;
130
136
  this.backgroundColorValue = backgroundColor;
131
137
  this.hoverBackgroundColorValue = hoverBackgroundColor;
132
- this.fontIdValue = fontId;
133
138
  this.fontFamilyValue = fontFamily;
134
139
  this.fontSizeValue = fontSize;
135
140
  this.fontWeightValue = fontWeight;
136
141
  this.fontStyleValue = fontStyle;
137
- this.usesDirectFontId = usesDirectFontId;
138
142
  this.applyTheme();
139
143
  }
140
144
 
@@ -143,21 +147,16 @@ class ContextMenuEntry extends Grid {
143
147
  this.padding(this.paddingLeftValue, this.paddingTopValue, this.paddingRightValue, this.paddingBottomValue);
144
148
  this.corners(this.cornerRadiusValue, this.cornerRadiusValue, this.cornerRadiusValue, this.cornerRadiusValue);
145
149
  this.bgColor(this.hovered && !this.disabled ? this.hoverBackgroundColorValue : this.backgroundColorValue);
146
- if (this.usesDirectFontId) {
147
- this.labelNode.font(this.fontIdValue, this.fontSizeValue);
148
- this.shortcutNode.font(this.fontIdValue, this.fontSizeValue);
149
- } else {
150
- this.labelNode
151
- .fontFamily(this.fontFamilyValue)
152
- .fontWeight(this.fontWeightValue)
153
- .fontStyle(this.fontStyleValue)
154
- .fontSize(this.fontSizeValue);
155
- this.shortcutNode
156
- .fontFamily(this.fontFamilyValue)
157
- .fontWeight(this.fontWeightValue)
158
- .fontStyle(this.fontStyleValue)
159
- .fontSize(this.fontSizeValue);
160
- }
150
+ this.labelNode
151
+ .fontFamily(this.fontFamilyValue)
152
+ .fontWeight(this.fontWeightValue)
153
+ .fontStyle(this.fontStyleValue)
154
+ .fontSize(this.fontSizeValue);
155
+ this.shortcutNode
156
+ .fontFamily(this.fontFamilyValue)
157
+ .fontWeight(this.fontWeightValue)
158
+ .fontStyle(this.fontStyleValue)
159
+ .fontSize(this.fontSizeValue);
161
160
  this.shortcutTextColorValue = activeTheme.value.colors.textMuted;
162
161
  this.labelNode.textColor(this.disabled ? this.shortcutTextColorValue : this.textColorValue);
163
162
  this.shortcutNode.textColor(this.shortcutTextColorValue);
@@ -171,14 +170,26 @@ class ContextMenuEntry extends Grid {
171
170
  }
172
171
  if (eventType == PointerEventType.Leave) {
173
172
  this.hovered = false;
173
+ this.pressed = false;
174
174
  this.applyTheme();
175
175
  return;
176
176
  }
177
177
  if (eventType == PointerEventType.Down) {
178
- if (this.disabled) {
179
- return;
178
+ this.pressed = !this.disabled;
179
+ return;
180
+ }
181
+ if (eventType == PointerEventType.Up) {
182
+ const shouldInvoke = this.pressed &&
183
+ this.hovered &&
184
+ !this.disabled;
185
+ this.pressed = false;
186
+ if (shouldInvoke) {
187
+ ContextMenu.invokeActiveSlot(this.slot);
180
188
  }
181
- ContextMenu.invokeActiveSlot(this.slot);
189
+ return;
190
+ }
191
+ if (eventType == PointerEventType.Cancel) {
192
+ this.pressed = false;
182
193
  return;
183
194
  }
184
195
  super._handlePointerEvent(eventType, x, y, modifiers);
@@ -233,6 +244,7 @@ export class MenuItem {
233
244
  readonly shortcutLabel: string | null;
234
245
  readonly disabled: bool;
235
246
  readonly targetHandle: u64;
247
+ readonly focusTargetAfterAction: bool;
236
248
  selectionStart: u32;
237
249
  selectionEnd: u32;
238
250
  private readonly kindValue: MenuItemKind;
@@ -247,6 +259,7 @@ export class MenuItem {
247
259
  targetHandle: u64 = <u64>HandleValue.Invalid,
248
260
  selectionStart: u32 = 0,
249
261
  selectionEnd: u32 = 0,
262
+ focusTargetAfterAction: bool = false,
250
263
  ) {
251
264
  this.label = label;
252
265
  this.action = action;
@@ -255,6 +268,7 @@ export class MenuItem {
255
268
  this.shortcutLabel = shortcutLabel;
256
269
  this.disabled = disabled;
257
270
  this.targetHandle = targetHandle;
271
+ this.focusTargetAfterAction = focusTargetAfterAction;
258
272
  this.selectionStart = selectionStart;
259
273
  this.selectionEnd = selectionEnd;
260
274
  }
@@ -274,6 +288,128 @@ export class MenuItem {
274
288
  }
275
289
  }
276
290
 
291
+ function writePayloadToClipboard(text: string): void {
292
+ const bytes = Uint8Array.wrap(String.UTF8.encode(text, false));
293
+ ffi.fui_copy_text(bytes.length > 0 ? bytes.dataStart : 0, <u32>bytes.length);
294
+ }
295
+
296
+ function commitFocusedTextAction(handle: u64): void {
297
+ ffi.fui_commit_text_action_focus(handle);
298
+ }
299
+
300
+ function commitFocusedTextActionIfNeeded(item: MenuItem): void {
301
+ if (item.focusTargetAfterAction && item.targetHandle != <u64>HandleValue.Invalid) {
302
+ commitFocusedTextAction(item.targetHandle);
303
+ }
304
+ }
305
+
306
+ export function runContextMenuAction(item: MenuItem): void {
307
+ if (item.disabled) {
308
+ const actionNeedsLiveSelection =
309
+ item.targetHandle != <u64>HandleValue.Invalid &&
310
+ (item.action == ContextMenuAction.CopyCurrentSelection || item.action == ContextMenuAction.CutTextSelection) &&
311
+ (ffi.fui_has_text_selection_snapshot(item.targetHandle) || ui.hasTextSelection(item.targetHandle));
312
+ if (!actionNeedsLiveSelection) {
313
+ return;
314
+ }
315
+ }
316
+ if (item.action == ContextMenuAction.CopyCurrentSelection) {
317
+ if (item.payload !== null) {
318
+ writePayloadToClipboard(changetype<string>(item.payload));
319
+ commitFocusedTextActionIfNeeded(item);
320
+ return;
321
+ }
322
+ if (item.targetHandle != <u64>HandleValue.Invalid && ffi.fui_copy_text_selection_snapshot(item.targetHandle)) {
323
+ commitFocusedTextActionIfNeeded(item);
324
+ return;
325
+ }
326
+ if (item.targetHandle != <u64>HandleValue.Invalid) {
327
+ ui.copyTextSelection(item.targetHandle);
328
+ commitFocusedTextActionIfNeeded(item);
329
+ return;
330
+ }
331
+ ui.copyCurrentSelection();
332
+ return;
333
+ }
334
+ if (item.targetHandle != <u64>HandleValue.Invalid && item.action == ContextMenuAction.UndoTextEdit) {
335
+ ui.undoTextEdit(item.targetHandle);
336
+ commitFocusedTextActionIfNeeded(item);
337
+ return;
338
+ }
339
+ if (item.targetHandle != <u64>HandleValue.Invalid && item.action == ContextMenuAction.RedoTextEdit) {
340
+ ui.redoTextEdit(item.targetHandle);
341
+ commitFocusedTextActionIfNeeded(item);
342
+ return;
343
+ }
344
+ if (item.targetHandle != <u64>HandleValue.Invalid && item.action == ContextMenuAction.CutTextSelection) {
345
+ if (item.payload !== null) {
346
+ writePayloadToClipboard(changetype<string>(item.payload));
347
+ }
348
+ if (item.selectionStart != item.selectionEnd && ffi.fui_cut_text_selection_snapshot(item.targetHandle)) {
349
+ commitFocusedTextActionIfNeeded(item);
350
+ return;
351
+ }
352
+ if (ffi.fui_cut_text_selection_snapshot(item.targetHandle)) {
353
+ commitFocusedTextActionIfNeeded(item);
354
+ return;
355
+ }
356
+ if (
357
+ item.selectionStart != item.selectionEnd &&
358
+ ffi.fui_delete_focused_text_range(item.selectionStart, item.selectionEnd)
359
+ ) {
360
+ commitFocusedTextActionIfNeeded(item);
361
+ return;
362
+ }
363
+ if (ffi.fui_cut_focused_text_selection()) {
364
+ commitFocusedTextActionIfNeeded(item);
365
+ return;
366
+ }
367
+ if (item.payload === null) {
368
+ ffi.fui_copy_text_selection_snapshot(item.targetHandle);
369
+ }
370
+ ui.cutTextSelection(item.targetHandle);
371
+ commitFocusedTextActionIfNeeded(item);
372
+ return;
373
+ }
374
+ if (item.targetHandle != <u64>HandleValue.Invalid && item.action == ContextMenuAction.PasteText) {
375
+ ui.pasteText(item.targetHandle);
376
+ commitFocusedTextActionIfNeeded(item);
377
+ return;
378
+ }
379
+ if (item.targetHandle != <u64>HandleValue.Invalid && item.action == ContextMenuAction.SelectAllText) {
380
+ ui.selectAllText(item.targetHandle);
381
+ commitFocusedTextActionIfNeeded(item);
382
+ return;
383
+ }
384
+ if (item.action == ContextMenuAction.ReloadPage) {
385
+ ffi.fui_reload_page();
386
+ return;
387
+ }
388
+ if (item.action == ContextMenuAction.NavigateBack) {
389
+ ffi.fui_navigate_back();
390
+ return;
391
+ }
392
+ if (item.action == ContextMenuAction.NavigateForward) {
393
+ ffi.fui_navigate_forward();
394
+ return;
395
+ }
396
+ if (item.payload !== null && item.action == ContextMenuAction.OpenLink) {
397
+ navigateTo(changetype<string>(item.payload), false);
398
+ return;
399
+ }
400
+ if (item.payload !== null && item.action == ContextMenuAction.OpenLinkInNewTab) {
401
+ navigateTo(changetype<string>(item.payload), true);
402
+ return;
403
+ }
404
+ if (item.payload !== null && item.action == ContextMenuAction.OpenImage) {
405
+ navigateTo(changetype<string>(item.payload), false);
406
+ return;
407
+ }
408
+ if (item.payload !== null && item.action == ContextMenuAction.OpenImageInNewTab) {
409
+ navigateTo(changetype<string>(item.payload), true);
410
+ }
411
+ }
412
+
277
413
  export class ContextMenu extends Portal implements GlobalKeyHandler {
278
414
  static readonly MAX_ITEMS: i32 = 25;
279
415
  private static activeInstance: ContextMenu | null = null;
@@ -282,13 +418,17 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
282
418
  .positionAbsolute()
283
419
  .width(MENU_WIDTH, Unit.Pixel)
284
420
  .padding(4.0, 4.0, 4.0, 4.0)
285
- .border(1.0, activeTheme.value.contextMenu.panelBorderColor, BorderStyle.Solid) as FlexBox;
421
+ .border(1.0, activeTheme.value.contextMenu.panelBorderColor) as FlexBox;
286
422
  private readonly popupPresenter!: PopupPresenter;
287
423
  private readonly entries: Array<ContextMenuEntry> = new Array<ContextMenuEntry>();
288
424
  private readonly separators: Array<ContextMenuSeparator> = new Array<ContextMenuSeparator>();
425
+ private readonly itemsValue: Array<MenuItem> = new Array<MenuItem>();
289
426
  private readonly currentItems: Array<MenuItem> = new Array<MenuItem>();
427
+ private readonly currentItemTops: Array<f32> = new Array<f32>();
428
+ private readonly currentItemHeights: Array<f32> = new Array<f32>();
290
429
  private readonly disposables: Array<Disposable> = new Array<Disposable>();
291
430
  private isMenuVisible: bool = false;
431
+ private suppressNextPointerUpActivation: bool = false;
292
432
  private keyFilterToken: u32 = 0;
293
433
  private menuWidthValue: f32 = MENU_WIDTH;
294
434
  private itemHeightValue: f32 = activeTheme.value.contextMenu.item.height;
@@ -300,12 +440,10 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
300
440
  private itemTextColorValue: u32 = activeTheme.value.contextMenu.item.textColor;
301
441
  private itemBackgroundColorValue: u32 = activeTheme.value.contextMenu.item.background;
302
442
  private itemHoverColorValue: u32 = activeTheme.value.contextMenu.item.hoverBackground;
303
- private itemFontIdValue: u32 = activeTheme.value.contextMenu.item.fontId;
304
443
  private itemFontFamilyValue: FontFamily = activeTheme.value.contextMenu.item.fontFamily;
305
444
  private itemFontSizeValue: f32 = activeTheme.value.contextMenu.item.fontSize;
306
445
  private itemFontWeightValue: FontWeight = FontWeight.Regular;
307
446
  private itemFontStyleValue: FontStyle = FontStyle.Normal;
308
- private itemUsesDirectFontIdValue: bool = false;
309
447
  private panelBackgroundColorValue: u32 = activeTheme.value.contextMenu.panelBackground;
310
448
  private panelBorderWidthValue: f32 = 1.0;
311
449
  private panelBorderColorValue: u32 = activeTheme.value.contextMenu.panelBorderColor;
@@ -329,9 +467,9 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
329
467
  private panelShadowOverridden: bool = false;
330
468
  private panelBackgroundBlurOverridden: bool = false;
331
469
  private itemMetricsOverridden: bool = false;
332
- private visibilityChangedCallback: ((visible: bool) => void) | null = null;
470
+ private visibilityChangedCallback: ((event: VisibilityChangedEventArgs) => void) | null = null;
333
471
 
334
- constructor() {
472
+ constructor(items: Array<MenuItem> | null = null) {
335
473
  super();
336
474
  this.popupPresenter = new PopupPresenter(this, this.panel);
337
475
 
@@ -340,13 +478,18 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
340
478
  this.width(100.0, Unit.Percent);
341
479
  this.height(100.0, Unit.Percent);
342
480
 
343
- this.popupPresenter.overlayNode.onClickWith(this, (menu) => menu.hide());
481
+ this.popupPresenter.overlayNode.onPointerUpWith(this, (menu, event: PointerEventArgs): void => {
482
+ menu.handleOverlayPointerUp(event);
483
+ });
344
484
  Grid.sharedSizeScope(this.panel, true);
345
485
 
346
486
  for (let index = 0; index < ContextMenu.MAX_ITEMS; ++index) {
347
487
  this.entries.push(new ContextMenuEntry(index));
348
488
  this.separators.push(new ContextMenuSeparator());
349
489
  }
490
+ if (items !== null) {
491
+ this.items(changetype<Array<MenuItem>>(items));
492
+ }
350
493
  this.applyTheme();
351
494
  this.track(activeTheme.addAction(new HandlerAction<ContextMenu, Theme>(this, (menu: ContextMenu, _theme: Theme): void => {
352
495
  menu.handleThemeChanged();
@@ -376,6 +519,15 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
376
519
  }
377
520
  }
378
521
 
522
+ static consumeOpeningPointerUpSuppression(): bool {
523
+ const menu = ContextMenu.activeInstance;
524
+ if (menu === null || !menu.suppressNextPointerUpActivation) {
525
+ return false;
526
+ }
527
+ menu.suppressNextPointerUpActivation = false;
528
+ return true;
529
+ }
530
+
379
531
  menuWidth(value: f32): this {
380
532
  if (value <= 0.0) {
381
533
  warn("Layout", "ContextMenu.menuWidth() received " + value.toString() + "; clamping to 1.0.");
@@ -405,7 +557,7 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
405
557
  return this;
406
558
  }
407
559
 
408
- onVisibilityChanged(callback: ((visible: bool) => void) | null): this {
560
+ onVisibilityChanged(callback: ((event: VisibilityChangedEventArgs) => void) | null): this {
409
561
  this.visibilityChangedCallback = callback;
410
562
  return this;
411
563
  }
@@ -417,11 +569,20 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
417
569
  return this;
418
570
  }
419
571
 
420
- panelBorder(width: f32, color: u32, style: BorderStyle = BorderStyle.Solid): this {
572
+ panelBorder(width: f32, color: u32): this {
421
573
  this.panelBorderOverridden = true;
422
574
  this.panelBorderWidthValue = width;
423
575
  this.panelBorderColorValue = color;
424
- this.panelBorderStyleValue = style;
576
+ this.panelBorderStyleValue = BorderStyle.Solid;
577
+ this.applyTheme();
578
+ return this;
579
+ }
580
+
581
+ panelBorderConfig(border: Border): this {
582
+ this.panelBorderOverridden = true;
583
+ this.panelBorderWidthValue = border.width;
584
+ this.panelBorderColorValue = border.color;
585
+ this.panelBorderStyleValue = border.style;
425
586
  this.applyTheme();
426
587
  return this;
427
588
  }
@@ -491,19 +652,9 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
491
652
  return this;
492
653
  }
493
654
 
494
- itemFont(fontId: u32, size: f32): this {
495
- this.itemFontOverridden = true;
496
- this.itemFontIdValue = fontId;
497
- this.itemFontSizeValue = size;
498
- this.itemUsesDirectFontIdValue = true;
499
- this.applyTheme();
500
- return this;
501
- }
502
-
503
655
  itemFontFamily(family: FontFamily): this {
504
656
  this.itemFontOverridden = true;
505
657
  this.itemFontFamilyValue = family;
506
- this.itemUsesDirectFontIdValue = false;
507
658
  this.applyTheme();
508
659
  return this;
509
660
  }
@@ -511,7 +662,6 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
511
662
  itemFontWeight(weight: FontWeight): this {
512
663
  this.itemFontOverridden = true;
513
664
  this.itemFontWeightValue = weight;
514
- this.itemUsesDirectFontIdValue = false;
515
665
  this.applyTheme();
516
666
  return this;
517
667
  }
@@ -519,7 +669,6 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
519
669
  itemFontStyle(style: FontStyle): this {
520
670
  this.itemFontOverridden = true;
521
671
  this.itemFontStyleValue = style;
522
- this.itemUsesDirectFontIdValue = false;
523
672
  this.applyTheme();
524
673
  return this;
525
674
  }
@@ -527,7 +676,6 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
527
676
  itemFontSize(size: f32): this {
528
677
  this.itemFontOverridden = true;
529
678
  this.itemFontSizeValue = size;
530
- this.itemUsesDirectFontIdValue = false;
531
679
  this.applyTheme();
532
680
  return this;
533
681
  }
@@ -539,15 +687,48 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
539
687
  return this;
540
688
  }
541
689
 
542
- show(items: Array<MenuItem>, x: f32, y: f32): void {
690
+ items(items: Array<MenuItem>): this {
691
+ this.itemsValue.length = 0;
692
+ for (let index = 0; index < items.length; ++index) {
693
+ this.itemsValue.push(unchecked(items[index]));
694
+ }
695
+ return this;
696
+ }
697
+
698
+ clearItems(): this {
699
+ this.itemsValue.length = 0;
700
+ return this;
701
+ }
702
+
703
+ show(target: Node | null, x: f32, y: f32): void {
704
+ this.showImpl(target, x, y, false);
705
+ }
706
+
707
+ showFromContextPointer(target: Node | null, x: f32, y: f32): void {
708
+ this.showImpl(target, x, y, true);
709
+ }
710
+
711
+ private showImpl(target: Node | null, x: f32, y: f32, suppressOpeningPointerUp: bool): void {
712
+ let absoluteX = x;
713
+ let absoluteY = y;
714
+ if (target !== null) {
715
+ const bounds = changetype<Node>(target).getBounds();
716
+ absoluteX += unchecked(bounds[0]);
717
+ absoluteY += unchecked(bounds[1]);
718
+ }
719
+
543
720
  this.clearPanel();
544
721
  this.applyTheme();
545
722
 
546
723
  this.currentItems.length = 0;
724
+ this.currentItemTops.length = 0;
725
+ this.currentItemHeights.length = 0;
547
726
  let actionCount = 0;
548
727
  let separatorCount = 0;
549
728
  let estimatedHeight: f32 = 8.0;
729
+ let contentY: f32 = 0.0;
550
730
  let lastWasSeparator = true;
731
+ const items = this.itemsValue;
551
732
  const count = items.length < ContextMenu.MAX_ITEMS ? items.length : ContextMenu.MAX_ITEMS;
552
733
  if (items.length > ContextMenu.MAX_ITEMS) {
553
734
  warn(
@@ -571,6 +752,7 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
571
752
  this.panel.addChildNode(separator);
572
753
  separatorCount += 1;
573
754
  estimatedHeight += MENU_SEPARATOR_HEIGHT;
755
+ contentY += MENU_SEPARATOR_HEIGHT;
574
756
  lastWasSeparator = true;
575
757
  continue;
576
758
  }
@@ -579,23 +761,27 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
579
761
  entry.item(item);
580
762
  entry.applyTheme();
581
763
  this.currentItems.push(item);
764
+ this.currentItemTops.push(contentY);
765
+ this.currentItemHeights.push(this.itemHeightValue);
582
766
  this.panel.addChildNode(entry);
583
767
  actionCount += 1;
584
768
  estimatedHeight += this.itemHeightValue;
769
+ contentY += this.itemHeightValue;
585
770
  lastWasSeparator = false;
586
771
  }
587
772
 
588
773
  const maxX = <f32>Math.max(0.0, ui.getViewportWidth() - this.menuWidthValue - MENU_EDGE_PADDING);
589
774
  const maxY = <f32>Math.max(0.0, ui.getViewportHeight() - estimatedHeight - MENU_EDGE_PADDING);
590
- const clampedX = <f32>Math.max(MENU_EDGE_PADDING, Math.min(x, maxX));
591
- const clampedY = <f32>Math.max(MENU_EDGE_PADDING, Math.min(y, maxY));
775
+ const clampedX = <f32>Math.max(MENU_EDGE_PADDING, Math.min(absoluteX, maxX));
776
+ const clampedY = <f32>Math.max(MENU_EDGE_PADDING, Math.min(absoluteY, maxY));
592
777
 
593
778
  this.popupPresenter.showAtPoint(clampedX, clampedY, this.menuWidthValue, estimatedHeight);
594
779
  this.isMenuVisible = true;
780
+ this.suppressNextPointerUpActivation = suppressOpeningPointerUp;
595
781
  ContextMenu.activeInstance = this;
596
782
  const visibilityChangedCallback = this.visibilityChangedCallback;
597
783
  if (visibilityChangedCallback !== null) {
598
- visibilityChangedCallback(true);
784
+ visibilityChangedCallback(new VisibilityChangedEventArgs(true));
599
785
  }
600
786
  if (this.keyFilterToken == 0) {
601
787
  this.keyFilterToken = EventRouter.pushKeyFilter(this);
@@ -608,6 +794,8 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
608
794
  }
609
795
  this.clearPanel();
610
796
  this.currentItems.length = 0;
797
+ this.currentItemTops.length = 0;
798
+ this.currentItemHeights.length = 0;
611
799
  this.popupPresenter.hide();
612
800
  this.isMenuVisible = false;
613
801
  if (ContextMenu.activeInstance === this) {
@@ -615,7 +803,7 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
615
803
  }
616
804
  const visibilityChangedCallback = this.visibilityChangedCallback;
617
805
  if (visibilityChangedCallback !== null) {
618
- visibilityChangedCallback(false);
806
+ visibilityChangedCallback(new VisibilityChangedEventArgs(false));
619
807
  }
620
808
  if (this.keyFilterToken != 0) {
621
809
  EventRouter.removeKeyFilter(this.keyFilterToken);
@@ -651,126 +839,56 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
651
839
  if (slot < 0 || slot >= this.currentItems.length) {
652
840
  return;
653
841
  }
654
- this.runAction(unchecked(this.currentItems[slot]));
842
+ runContextMenuAction(unchecked(this.currentItems[slot]));
655
843
  this.hide();
656
844
  }
657
845
 
658
- private writePayloadToClipboard(text: string): void {
659
- const bytes = Uint8Array.wrap(String.UTF8.encode(text, false));
660
- ffi.fui_copy_text(bytes.length > 0 ? bytes.dataStart : 0, <u32>bytes.length);
661
- }
662
-
663
- private commitFocusedTextAction(handle: u64): void {
664
- ffi.fui_commit_text_action_focus(handle);
665
- }
666
-
667
- private runAction(item: MenuItem): void {
668
- if (item.disabled) {
669
- const actionNeedsLiveSelection =
670
- item.targetHandle != <u64>HandleValue.Invalid &&
671
- (item.action == ContextMenuAction.CopyCurrentSelection || item.action == ContextMenuAction.CutTextSelection) &&
672
- (ffi.fui_has_text_selection_snapshot(item.targetHandle) || ui.hasTextSelection(item.targetHandle));
673
- if (!actionNeedsLiveSelection) {
674
- return;
675
- }
846
+ private handleOverlayPointerUp(event: PointerEventArgs): void {
847
+ if (!this.isMenuVisible) {
848
+ return;
676
849
  }
677
- if (item.action == ContextMenuAction.CopyCurrentSelection) {
678
- if (item.payload !== null) {
679
- this.writePayloadToClipboard(changetype<string>(item.payload));
680
- if (item.targetHandle != <u64>HandleValue.Invalid) {
681
- this.commitFocusedTextAction(item.targetHandle);
682
- }
683
- return;
684
- }
685
- if (item.targetHandle != <u64>HandleValue.Invalid && ffi.fui_copy_text_selection_snapshot(item.targetHandle)) {
686
- this.commitFocusedTextAction(item.targetHandle);
850
+ if (!isPrimaryActivationPointer(event)) {
851
+ if (ContextMenu.consumeOpeningPointerUpSuppression()) {
852
+ event.handled = true;
687
853
  return;
688
854
  }
689
- if (item.targetHandle != <u64>HandleValue.Invalid) {
690
- ui.copyTextSelection(item.targetHandle);
691
- this.commitFocusedTextAction(item.targetHandle);
692
- return;
693
- }
694
- ui.copyCurrentSelection();
695
- return;
696
- }
697
- if (item.targetHandle != <u64>HandleValue.Invalid && item.action == ContextMenuAction.UndoTextEdit) {
698
- ui.undoTextEdit(item.targetHandle);
699
- this.commitFocusedTextAction(item.targetHandle);
855
+ event.handled = true;
700
856
  return;
701
857
  }
702
- if (item.targetHandle != <u64>HandleValue.Invalid && item.action == ContextMenuAction.RedoTextEdit) {
703
- ui.redoTextEdit(item.targetHandle);
704
- this.commitFocusedTextAction(item.targetHandle);
705
- return;
706
- }
707
- if (item.targetHandle != <u64>HandleValue.Invalid && item.action == ContextMenuAction.CutTextSelection) {
708
- if (item.payload !== null) {
709
- this.writePayloadToClipboard(changetype<string>(item.payload));
710
- }
711
- if (item.selectionStart != item.selectionEnd && ffi.fui_cut_text_selection_snapshot(item.targetHandle)) {
712
- this.commitFocusedTextAction(item.targetHandle);
713
- return;
714
- }
715
- if (ffi.fui_cut_text_selection_snapshot(item.targetHandle)) {
716
- this.commitFocusedTextAction(item.targetHandle);
717
- return;
718
- }
719
- if (
720
- item.selectionStart != item.selectionEnd &&
721
- ffi.fui_delete_focused_text_range(item.selectionStart, item.selectionEnd)
722
- ) {
723
- this.commitFocusedTextAction(item.targetHandle);
724
- return;
858
+ for (let slot = 0; slot < this.currentItems.length; ++slot) {
859
+ const entry = unchecked(this.entries[slot]);
860
+ if (entry.builtHandle == <u64>HandleValue.Invalid) {
861
+ continue;
725
862
  }
726
- if (ffi.fui_cut_focused_text_selection()) {
727
- this.commitFocusedTextAction(item.targetHandle);
863
+ const bounds = entry.getBounds();
864
+ const left = unchecked(bounds[0]);
865
+ const top = unchecked(bounds[1]);
866
+ const right = left + unchecked(bounds[2]);
867
+ const bottom = top + unchecked(bounds[3]);
868
+ if (event.sceneX >= left && event.sceneX <= right && event.sceneY >= top && event.sceneY <= bottom) {
869
+ event.handled = true;
870
+ this.invokeSlot(slot);
728
871
  return;
729
872
  }
730
- if (item.payload === null) {
731
- ffi.fui_copy_text_selection_snapshot(item.targetHandle);
732
- }
733
- ui.cutTextSelection(item.targetHandle);
734
- this.commitFocusedTextAction(item.targetHandle);
735
- return;
736
- }
737
- if (item.targetHandle != <u64>HandleValue.Invalid && item.action == ContextMenuAction.PasteText) {
738
- ui.pasteText(item.targetHandle);
739
- this.commitFocusedTextAction(item.targetHandle);
740
- return;
741
- }
742
- if (item.targetHandle != <u64>HandleValue.Invalid && item.action == ContextMenuAction.SelectAllText) {
743
- ui.selectAllText(item.targetHandle);
744
- this.commitFocusedTextAction(item.targetHandle);
745
- return;
746
873
  }
747
- if (item.action == ContextMenuAction.ReloadPage) {
748
- ffi.fui_reload_page();
749
- return;
750
- }
751
- if (item.action == ContextMenuAction.NavigateBack) {
752
- ffi.fui_navigate_back();
753
- return;
754
- }
755
- if (item.action == ContextMenuAction.NavigateForward) {
756
- ffi.fui_navigate_forward();
757
- return;
758
- }
759
- if (item.payload !== null && item.action == ContextMenuAction.OpenLink) {
760
- navigateTo(changetype<string>(item.payload), false);
761
- return;
762
- }
763
- if (item.payload !== null && item.action == ContextMenuAction.OpenLinkInNewTab) {
764
- navigateTo(changetype<string>(item.payload), true);
765
- return;
766
- }
767
- if (item.payload !== null && item.action == ContextMenuAction.OpenImage) {
768
- navigateTo(changetype<string>(item.payload), false);
874
+ const localX = event.sceneX - this.popupPresenter.surfaceX;
875
+ const localY = event.sceneY - this.popupPresenter.surfaceY;
876
+ if (localX < 0.0 || localX > this.menuWidthValue || localY < 0.0) {
877
+ event.handled = true;
878
+ this.hide();
769
879
  return;
770
880
  }
771
- if (item.payload !== null && item.action == ContextMenuAction.OpenImageInNewTab) {
772
- navigateTo(changetype<string>(item.payload), true);
881
+ for (let slot = 0; slot < this.currentItems.length; ++slot) {
882
+ const top = unchecked(this.currentItemTops[slot]);
883
+ const height = unchecked(this.currentItemHeights[slot]);
884
+ if (localY >= top && localY <= top + height) {
885
+ event.handled = true;
886
+ this.invokeSlot(slot);
887
+ return;
888
+ }
773
889
  }
890
+ event.handled = true;
891
+ this.hide();
774
892
  }
775
893
 
776
894
  private applyTheme(): void {
@@ -778,7 +896,7 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
778
896
  this.panel.bgColor(this.panelBackgroundColorValue);
779
897
  this.panel.backgroundBlur(this.panelBackgroundBlurSigmaValue);
780
898
  this.panel.cornerRadius(this.panelCornerRadiusValue);
781
- this.panel.border(this.panelBorderWidthValue, this.panelBorderColorValue, this.panelBorderStyleValue);
899
+ this.panel.borderConfig(new Border(this.panelBorderWidthValue, this.panelBorderColorValue, this.panelBorderStyleValue));
782
900
  this.panel.dropShadow(
783
901
  this.panelShadowColorValue,
784
902
  0.0,
@@ -797,12 +915,10 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
797
915
  this.itemTextColorValue,
798
916
  this.itemBackgroundColorValue,
799
917
  this.itemHoverColorValue,
800
- this.itemFontIdValue,
801
918
  this.itemFontFamilyValue,
802
919
  this.itemFontSizeValue,
803
920
  this.itemFontWeightValue,
804
921
  this.itemFontStyleValue,
805
- this.itemUsesDirectFontIdValue,
806
922
  );
807
923
  unchecked(this.separators[index]).configureStyle(this.separatorColorValue);
808
924
  }
@@ -834,12 +950,10 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
834
950
  this.itemCornerRadiusValue = theme.contextMenu.item.cornerRadius;
835
951
  }
836
952
  if (!this.itemFontOverridden) {
837
- this.itemFontIdValue = theme.contextMenu.item.fontId;
838
953
  this.itemFontFamilyValue = theme.contextMenu.item.fontFamily;
839
954
  this.itemFontSizeValue = theme.contextMenu.item.fontSize;
840
955
  this.itemFontWeightValue = FontWeight.Regular;
841
956
  this.itemFontStyleValue = FontStyle.Normal;
842
- this.itemUsesDirectFontIdValue = false;
843
957
  }
844
958
  if (!this.separatorColorOverridden) {
845
959
  this.separatorColorValue = theme.contextMenu.separatorColor;
@@ -882,4 +996,4 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
882
996
  private track(disposable: Disposable): void {
883
997
  this.disposables.push(disposable);
884
998
  }
885
- }
999
+ }