@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.
Files changed (137) hide show
  1. package/LICENSE.md +7 -0
  2. package/browser/src/common-harness/host-imports.ts +430 -0
  3. package/browser/src/common-harness/interop.ts +39 -0
  4. package/browser/src/common-harness/managed-harness-bitmap-host.ts +92 -0
  5. package/browser/src/common-harness/managed-harness-fetch-host.ts +201 -0
  6. package/browser/src/common-harness/managed-harness-file-host.ts +1101 -0
  7. package/browser/src/common-harness/managed-harness-file-payloads.ts +143 -0
  8. package/browser/src/common-harness/managed-harness-file-types.ts +106 -0
  9. package/browser/src/common-harness/managed-harness-session.ts +15 -0
  10. package/browser/src/common-harness/managed-harness.ts +1323 -0
  11. package/browser/src/common-harness/managed-history.ts +168 -0
  12. package/browser/src/common-harness/persisted-restore-policy.ts +50 -0
  13. package/browser/src/common-harness/persisted-ui-state-controller.ts +309 -0
  14. package/browser/src/common-harness/text-session-bridge.ts +452 -0
  15. package/browser/src/common-harness/types.ts +205 -0
  16. package/browser/src/common-harness/ui-chrome.ts +191 -0
  17. package/browser/src/common-harness/ui-imports.ts +529 -0
  18. package/browser/src/common-harness/wasm-module-cache.ts +47 -0
  19. package/browser/src/common-harness.ts +27 -0
  20. package/browser/src/file-processing-worker.ts +89 -0
  21. package/browser/src/host-events.ts +97 -0
  22. package/browser/src/host-services.ts +203 -0
  23. package/browser/src/index.ts +62 -0
  24. package/browser/src/persisted-ui-state.ts +206 -0
  25. package/browser/src/routed-harness.ts +198 -0
  26. package/browser/src/worker-bootstrap.ts +483 -0
  27. package/browser/src/worker-manager.ts +230 -0
  28. package/browser/src/worker-types.ts +50 -0
  29. package/package.json +89 -0
  30. package/scripts/build-demo-as.sh +91 -0
  31. package/scripts/build.sh +325 -0
  32. package/scripts/generate-host-events.ts +175 -0
  33. package/scripts/generate-host-services.ts +157 -0
  34. package/src/Fui.ts +205 -0
  35. package/src/FuiExports.ts +55 -0
  36. package/src/FuiPrimitives.ts +15 -0
  37. package/src/FuiWorker.ts +3 -0
  38. package/src/FuiWorkerExports.ts +6 -0
  39. package/src/bindings/ui.ts +531 -0
  40. package/src/color.ts +86 -0
  41. package/src/controls/AntiSelectionArea.ts +23 -0
  42. package/src/controls/Button.ts +750 -0
  43. package/src/controls/Checkbox.ts +181 -0
  44. package/src/controls/ContextMenu.ts +885 -0
  45. package/src/controls/ControlTemplateSet.ts +37 -0
  46. package/src/controls/Dialog.ts +355 -0
  47. package/src/controls/Dropdown.ts +856 -0
  48. package/src/controls/Form.ts +110 -0
  49. package/src/controls/NavLink.ts +211 -0
  50. package/src/controls/Popup.ts +129 -0
  51. package/src/controls/ProgressBar.ts +180 -0
  52. package/src/controls/RadioButton.ts +135 -0
  53. package/src/controls/RadioGroup.ts +244 -0
  54. package/src/controls/SelectionArea.ts +75 -0
  55. package/src/controls/Slider.ts +471 -0
  56. package/src/controls/Switch.ts +132 -0
  57. package/src/controls/TextArea.ts +20 -0
  58. package/src/controls/TextInput.ts +7 -0
  59. package/src/controls/index.ts +18 -0
  60. package/src/controls/internal/ButtonPresenter.ts +95 -0
  61. package/src/controls/internal/CheckboxIndicatorPresenter.ts +93 -0
  62. package/src/controls/internal/DropdownChevronPresenter.ts +67 -0
  63. package/src/controls/internal/DropdownFieldPresenter.ts +110 -0
  64. package/src/controls/internal/DropdownOptionRowPresenter.ts +82 -0
  65. package/src/controls/internal/PopupPresenter.ts +198 -0
  66. package/src/controls/internal/PressableIndicatorPresenter.ts +32 -0
  67. package/src/controls/internal/PressableLabeledControl.ts +221 -0
  68. package/src/controls/internal/RadioIndicatorPresenter.ts +73 -0
  69. package/src/controls/internal/SliderPresenter.ts +157 -0
  70. package/src/controls/internal/SwitchIndicatorPresenter.ts +72 -0
  71. package/src/controls/internal/TextInputCore.ts +695 -0
  72. package/src/controls/internal/TextInputPresenter.ts +72 -0
  73. package/src/controls/templating.ts +54 -0
  74. package/src/core/Action.ts +94 -0
  75. package/src/core/Actions.ts +37 -0
  76. package/src/core/Animation.ts +412 -0
  77. package/src/core/Application.ts +328 -0
  78. package/src/core/Assets.ts +264 -0
  79. package/src/core/AttachedProperties.ts +32 -0
  80. package/src/core/Bitmap.ts +70 -0
  81. package/src/core/BoundCallback.ts +104 -0
  82. package/src/core/Callbacks.ts +17 -0
  83. package/src/core/ContextMenuManager.ts +466 -0
  84. package/src/core/DebugApi.ts +30 -0
  85. package/src/core/Disposable.ts +10 -0
  86. package/src/core/DragDropManager.ts +179 -0
  87. package/src/core/DragGesture.ts +184 -0
  88. package/src/core/DynamicAssetIds.ts +24 -0
  89. package/src/core/Errors.ts +48 -0
  90. package/src/core/EventRouter.ts +408 -0
  91. package/src/core/ExternalDropManager.ts +122 -0
  92. package/src/core/Fetch.ts +264 -0
  93. package/src/core/FetchFfi.ts +15 -0
  94. package/src/core/File.ts +1002 -0
  95. package/src/core/FocusAdornerManager.ts +263 -0
  96. package/src/core/FocusVisibility.ts +36 -0
  97. package/src/core/FrameScheduler.ts +28 -0
  98. package/src/core/KeyboardScroll.ts +161 -0
  99. package/src/core/KeyboardScrollTracker.ts +386 -0
  100. package/src/core/Logger.ts +80 -0
  101. package/src/core/Navigation.ts +13 -0
  102. package/src/core/Node.ts +1708 -0
  103. package/src/core/PersistedState.ts +102 -0
  104. package/src/core/PersistedUiState.ts +142 -0
  105. package/src/core/Platform.ts +219 -0
  106. package/src/core/Signal.ts +89 -0
  107. package/src/core/Theme.ts +365 -0
  108. package/src/core/Timers.ts +129 -0
  109. package/src/core/ToolTip.ts +122 -0
  110. package/src/core/ToolTipManager.ts +459 -0
  111. package/src/core/Transitions.ts +34 -0
  112. package/src/core/Typography.ts +204 -0
  113. package/src/core/Worker.ts +196 -0
  114. package/src/core/bind.ts +37 -0
  115. package/src/core/event_exports.ts +596 -0
  116. package/src/core/ffi.ts +728 -0
  117. package/src/host-services/runtime.ts +25 -0
  118. package/src/nodes/FlexBox.ts +789 -0
  119. package/src/nodes/GradientStop.ts +9 -0
  120. package/src/nodes/Grid.ts +183 -0
  121. package/src/nodes/Image.ts +189 -0
  122. package/src/nodes/Portal.ts +14 -0
  123. package/src/nodes/RichText.ts +312 -0
  124. package/src/nodes/ScrollBar.ts +570 -0
  125. package/src/nodes/ScrollBox.ts +415 -0
  126. package/src/nodes/ScrollState.ts +10 -0
  127. package/src/nodes/ScrollView.ts +511 -0
  128. package/src/nodes/Svg.ts +142 -0
  129. package/src/nodes/Text.ts +145 -0
  130. package/src/nodes/TextCore.ts +558 -0
  131. package/src/nodes/VirtualList.ts +431 -0
  132. package/src/nodes/helpers.ts +25 -0
  133. package/src/nodes/index.ts +14 -0
  134. package/src/tsconfig.json +7 -0
  135. package/src/worker/Worker.ts +169 -0
  136. package/src/worker/WorkerJob.ts +65 -0
  137. package/src/worker/ffi.ts +23 -0
@@ -0,0 +1,885 @@
1
+ import * as ui from "../bindings/ui";
2
+ import { EventRouter, GlobalKeyHandler } from "../core/EventRouter";
3
+ import * as ffi from "../core/ffi";
4
+ import {
5
+ BorderStyle,
6
+ CursorStyle,
7
+ GridUnit,
8
+ HandleValue,
9
+ KeyEventType,
10
+ PointerEventType,
11
+ SemanticRole,
12
+ TextAlign,
13
+ TextOverflow,
14
+ Unit,
15
+ } from "../core/ffi";
16
+ import { HandlerAction } from "../core/Action";
17
+ import { Disposable, disposeAll } from "../core/Disposable";
18
+ import { navigateTo } from "../core/Navigation";
19
+ import { Theme, activeTheme } from "../core/Theme";
20
+ import { warn } from "../core/Logger";
21
+ import { FontFamily, FontStyle, FontWeight } from "../core/Typography";
22
+ import { FlexBox, Grid, Portal, TextCore } from "../nodes";
23
+ import { PopupPresenter } from "./internal/PopupPresenter";
24
+
25
+ const MENU_WIDTH: f32 = 220.0;
26
+ const MENU_SEPARATOR_HEIGHT: f32 = 9.0;
27
+ const MENU_EDGE_PADDING: f32 = 8.0;
28
+ const DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA: f32 = 10.0;
29
+
30
+ enum MenuItemKind {
31
+ Action = 0,
32
+ Separator = 1,
33
+ }
34
+
35
+ class ContextMenuEntry extends Grid {
36
+ private readonly labelNode: TextCore = new TextCore("")
37
+ .font(activeTheme.value.contextMenu.item.fontId, activeTheme.value.contextMenu.item.fontSize)
38
+ .textColor(activeTheme.value.contextMenu.item.textColor)
39
+ .overflow(TextOverflow.Ellipsis)
40
+ .selectable(false) as TextCore;
41
+ private readonly shortcutNode: TextCore = new TextCore("")
42
+ .font(activeTheme.value.contextMenu.item.fontId, activeTheme.value.contextMenu.item.fontSize)
43
+ .textColor(activeTheme.value.colors.textMuted)
44
+ .textAlign(TextAlign.Left)
45
+ .selectable(false) as TextCore;
46
+ private readonly slot: i32;
47
+ private hovered: bool = false;
48
+ private disabled: bool = false;
49
+ private itemHeightValue: f32 = activeTheme.value.contextMenu.item.height;
50
+ private paddingLeftValue: f32 = activeTheme.value.contextMenu.item.paddingLeft;
51
+ private paddingTopValue: f32 = activeTheme.value.contextMenu.item.paddingTop;
52
+ private paddingRightValue: f32 = activeTheme.value.contextMenu.item.paddingRight;
53
+ private paddingBottomValue: f32 = activeTheme.value.contextMenu.item.paddingBottom;
54
+ private cornerRadiusValue: f32 = activeTheme.value.contextMenu.item.cornerRadius;
55
+ private textColorValue: u32 = activeTheme.value.contextMenu.item.textColor;
56
+ private shortcutTextColorValue: u32 = activeTheme.value.colors.textMuted;
57
+ private backgroundColorValue: u32 = activeTheme.value.contextMenu.item.background;
58
+ private hoverBackgroundColorValue: u32 = activeTheme.value.contextMenu.item.hoverBackground;
59
+ private fontIdValue: u32 = activeTheme.value.contextMenu.item.fontId;
60
+ private fontFamilyValue: FontFamily = activeTheme.value.contextMenu.item.fontFamily;
61
+ private fontSizeValue: f32 = activeTheme.value.contextMenu.item.fontSize;
62
+ private fontWeightValue: FontWeight = FontWeight.Regular;
63
+ private fontStyleValue: FontStyle = FontStyle.Normal;
64
+ private usesDirectFontId: bool = false;
65
+
66
+ constructor(slot: i32) {
67
+ super();
68
+ this.slot = slot;
69
+ this.width(100.0, Unit.Percent);
70
+ this.height(this.itemHeightValue, Unit.Pixel);
71
+ this.padding(this.paddingLeftValue, this.paddingTopValue, this.paddingRightValue, this.paddingBottomValue);
72
+ this.cursor(CursorStyle.Pointer);
73
+ this.semanticRole(SemanticRole.Button);
74
+ this.requireInteractive();
75
+
76
+ const columnValues = new Array<f32>();
77
+ columnValues.push(1.0);
78
+ columnValues.push(0.0);
79
+ const columnTypes = new Array<GridUnit>();
80
+ columnTypes.push(GridUnit.Star);
81
+ columnTypes.push(GridUnit.Auto);
82
+ const rowValues = new Array<f32>();
83
+ rowValues.push(1.0);
84
+ const rowTypes = new Array<GridUnit>();
85
+ rowTypes.push(GridUnit.Star);
86
+
87
+ this.columns(2, columnValues, columnTypes);
88
+ this.rows(1, rowValues, rowTypes);
89
+ this.columnSharedSizeGroup(1, "ContextMenuShortcutColumn");
90
+
91
+ this.placeChild(this.labelNode, 0, 0);
92
+ this.placeChild(this.shortcutNode, 0, 1);
93
+ this.applyTheme();
94
+ }
95
+
96
+ item(item: MenuItem): this {
97
+ this.disabled = item.disabled;
98
+ this.semanticLabel(item.label);
99
+ this.semanticDisabled(this.disabled);
100
+ this.cursor(this.disabled ? CursorStyle.Default : CursorStyle.Pointer);
101
+ this.shortcutNode.text(item.shortcutLabel !== null ? changetype<string>(item.shortcutLabel) : "");
102
+ this.labelNode.text(item.label);
103
+ return this;
104
+ }
105
+
106
+ configureStyle(
107
+ itemHeight: f32,
108
+ paddingLeft: f32,
109
+ paddingTop: f32,
110
+ paddingRight: f32,
111
+ paddingBottom: f32,
112
+ cornerRadius: f32,
113
+ textColor: u32,
114
+ backgroundColor: u32,
115
+ hoverBackgroundColor: u32,
116
+ fontId: u32,
117
+ fontFamily: FontFamily,
118
+ fontSize: f32,
119
+ fontWeight: FontWeight,
120
+ fontStyle: FontStyle,
121
+ usesDirectFontId: bool,
122
+ ): void {
123
+ this.itemHeightValue = itemHeight;
124
+ this.paddingLeftValue = paddingLeft;
125
+ this.paddingTopValue = paddingTop;
126
+ this.paddingRightValue = paddingRight;
127
+ this.paddingBottomValue = paddingBottom;
128
+ this.cornerRadiusValue = cornerRadius;
129
+ this.textColorValue = textColor;
130
+ this.backgroundColorValue = backgroundColor;
131
+ this.hoverBackgroundColorValue = hoverBackgroundColor;
132
+ this.fontIdValue = fontId;
133
+ this.fontFamilyValue = fontFamily;
134
+ this.fontSizeValue = fontSize;
135
+ this.fontWeightValue = fontWeight;
136
+ this.fontStyleValue = fontStyle;
137
+ this.usesDirectFontId = usesDirectFontId;
138
+ this.applyTheme();
139
+ }
140
+
141
+ applyTheme(): void {
142
+ this.height(this.itemHeightValue, Unit.Pixel);
143
+ this.padding(this.paddingLeftValue, this.paddingTopValue, this.paddingRightValue, this.paddingBottomValue);
144
+ this.corners(this.cornerRadiusValue, this.cornerRadiusValue, this.cornerRadiusValue, this.cornerRadiusValue);
145
+ 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
+ }
161
+ this.shortcutTextColorValue = activeTheme.value.colors.textMuted;
162
+ this.labelNode.textColor(this.disabled ? this.shortcutTextColorValue : this.textColorValue);
163
+ this.shortcutNode.textColor(this.shortcutTextColorValue);
164
+ }
165
+
166
+ _handlePointerEvent(eventType: PointerEventType, x: f32, y: f32, modifiers: u32 = 0): void {
167
+ if (eventType == PointerEventType.Enter) {
168
+ this.hovered = !this.disabled;
169
+ this.applyTheme();
170
+ return;
171
+ }
172
+ if (eventType == PointerEventType.Leave) {
173
+ this.hovered = false;
174
+ this.applyTheme();
175
+ return;
176
+ }
177
+ if (eventType == PointerEventType.Down) {
178
+ if (this.disabled) {
179
+ return;
180
+ }
181
+ ContextMenu.invokeActiveSlot(this.slot);
182
+ return;
183
+ }
184
+ super._handlePointerEvent(eventType, x, y, modifiers);
185
+ }
186
+ }
187
+
188
+ class ContextMenuSeparator extends FlexBox {
189
+ private readonly line: FlexBox = new FlexBox()
190
+ .width(100.0, Unit.Percent)
191
+ .height(1.0, Unit.Pixel) as FlexBox;
192
+ private lineColorValue: u32 = activeTheme.value.contextMenu.separatorColor;
193
+
194
+ constructor() {
195
+ super();
196
+ this.width(100.0, Unit.Percent);
197
+ this.height(MENU_SEPARATOR_HEIGHT, Unit.Pixel);
198
+ this.padding(0.0, 4.0, 0.0, 4.0);
199
+ this.child(this.line);
200
+ this.applyTheme();
201
+ }
202
+
203
+ configureStyle(color: u32): void {
204
+ this.lineColorValue = color;
205
+ this.applyTheme();
206
+ }
207
+
208
+ applyTheme(): void {
209
+ this.line.bgColor(this.lineColorValue);
210
+ }
211
+ }
212
+
213
+ export enum ContextMenuAction {
214
+ CopyCurrentSelection = 0,
215
+ ReloadPage = 1,
216
+ OpenLink = 2,
217
+ OpenLinkInNewTab = 3,
218
+ NavigateBack = 4,
219
+ NavigateForward = 5,
220
+ UndoTextEdit = 6,
221
+ RedoTextEdit = 7,
222
+ CutTextSelection = 8,
223
+ PasteText = 9,
224
+ SelectAllText = 10,
225
+ OpenImage = 11,
226
+ OpenImageInNewTab = 12,
227
+ }
228
+
229
+ export class MenuItem {
230
+ readonly label: string;
231
+ readonly action: ContextMenuAction;
232
+ readonly payload: string | null;
233
+ readonly shortcutLabel: string | null;
234
+ readonly disabled: bool;
235
+ readonly targetHandle: u64;
236
+ selectionStart: u32;
237
+ selectionEnd: u32;
238
+ private readonly kindValue: MenuItemKind;
239
+
240
+ constructor(
241
+ label: string,
242
+ action: ContextMenuAction,
243
+ payload: string | null = null,
244
+ kind: MenuItemKind = MenuItemKind.Action,
245
+ shortcutLabel: string | null = null,
246
+ disabled: bool = false,
247
+ targetHandle: u64 = <u64>HandleValue.Invalid,
248
+ selectionStart: u32 = 0,
249
+ selectionEnd: u32 = 0,
250
+ ) {
251
+ this.label = label;
252
+ this.action = action;
253
+ this.payload = payload;
254
+ this.kindValue = kind;
255
+ this.shortcutLabel = shortcutLabel;
256
+ this.disabled = disabled;
257
+ this.targetHandle = targetHandle;
258
+ this.selectionStart = selectionStart;
259
+ this.selectionEnd = selectionEnd;
260
+ }
261
+
262
+ withSelectionRange(start: u32, end: u32): MenuItem {
263
+ this.selectionStart = start;
264
+ this.selectionEnd = end;
265
+ return this;
266
+ }
267
+
268
+ static separator(): MenuItem {
269
+ return new MenuItem("", ContextMenuAction.ReloadPage, null, MenuItemKind.Separator);
270
+ }
271
+
272
+ get isSeparator(): bool {
273
+ return this.kindValue == MenuItemKind.Separator;
274
+ }
275
+ }
276
+
277
+ export class ContextMenu extends Portal implements GlobalKeyHandler {
278
+ static readonly MAX_ITEMS: i32 = 25;
279
+ private static activeInstance: ContextMenu | null = null;
280
+
281
+ private readonly panel: FlexBox = new FlexBox()
282
+ .positionAbsolute()
283
+ .width(MENU_WIDTH, Unit.Pixel)
284
+ .padding(4.0, 4.0, 4.0, 4.0)
285
+ .border(1.0, activeTheme.value.contextMenu.panelBorderColor, BorderStyle.Solid) as FlexBox;
286
+ private readonly popupPresenter!: PopupPresenter;
287
+ private readonly entries: Array<ContextMenuEntry> = new Array<ContextMenuEntry>();
288
+ private readonly separators: Array<ContextMenuSeparator> = new Array<ContextMenuSeparator>();
289
+ private readonly currentItems: Array<MenuItem> = new Array<MenuItem>();
290
+ private readonly disposables: Array<Disposable> = new Array<Disposable>();
291
+ private isMenuVisible: bool = false;
292
+ private keyFilterToken: u32 = 0;
293
+ private menuWidthValue: f32 = MENU_WIDTH;
294
+ private itemHeightValue: f32 = activeTheme.value.contextMenu.item.height;
295
+ private itemPaddingLeftValue: f32 = activeTheme.value.contextMenu.item.paddingLeft;
296
+ private itemPaddingTopValue: f32 = activeTheme.value.contextMenu.item.paddingTop;
297
+ private itemPaddingRightValue: f32 = activeTheme.value.contextMenu.item.paddingRight;
298
+ private itemPaddingBottomValue: f32 = activeTheme.value.contextMenu.item.paddingBottom;
299
+ private itemCornerRadiusValue: f32 = activeTheme.value.contextMenu.item.cornerRadius;
300
+ private itemTextColorValue: u32 = activeTheme.value.contextMenu.item.textColor;
301
+ private itemBackgroundColorValue: u32 = activeTheme.value.contextMenu.item.background;
302
+ private itemHoverColorValue: u32 = activeTheme.value.contextMenu.item.hoverBackground;
303
+ private itemFontIdValue: u32 = activeTheme.value.contextMenu.item.fontId;
304
+ private itemFontFamilyValue: FontFamily = activeTheme.value.contextMenu.item.fontFamily;
305
+ private itemFontSizeValue: f32 = activeTheme.value.contextMenu.item.fontSize;
306
+ private itemFontWeightValue: FontWeight = FontWeight.Regular;
307
+ private itemFontStyleValue: FontStyle = FontStyle.Normal;
308
+ private itemUsesDirectFontIdValue: bool = false;
309
+ private panelBackgroundColorValue: u32 = activeTheme.value.contextMenu.panelBackground;
310
+ private panelBorderWidthValue: f32 = 1.0;
311
+ private panelBorderColorValue: u32 = activeTheme.value.contextMenu.panelBorderColor;
312
+ private panelBorderStyleValue: BorderStyle = BorderStyle.Solid;
313
+ private panelCornerRadiusValue: f32 = activeTheme.value.contextMenu.panelCornerRadius;
314
+ private separatorColorValue: u32 = activeTheme.value.contextMenu.separatorColor;
315
+ private panelShadowColorValue: u32 = activeTheme.value.contextMenu.panelShadowColor;
316
+ private panelShadowOffsetYValue: f32 = activeTheme.value.contextMenu.shadowOffsetY;
317
+ private panelShadowBlurValue: f32 = activeTheme.value.contextMenu.shadowBlur;
318
+ private panelShadowSpreadValue: f32 = activeTheme.value.contextMenu.shadowSpread;
319
+ private panelBackgroundBlurSigmaValue: f32 = DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA;
320
+ private panelBackgroundOverridden: bool = false;
321
+ private panelBorderOverridden: bool = false;
322
+ private panelCornerRadiusOverridden: bool = false;
323
+ private itemTextColorOverridden: bool = false;
324
+ private itemBackgroundOverridden: bool = false;
325
+ private itemHoverColorOverridden: bool = false;
326
+ private itemCornerRadiusOverridden: bool = false;
327
+ private itemFontOverridden: bool = false;
328
+ private separatorColorOverridden: bool = false;
329
+ private panelShadowOverridden: bool = false;
330
+ private panelBackgroundBlurOverridden: bool = false;
331
+ private itemMetricsOverridden: bool = false;
332
+ private visibilityChangedCallback: ((visible: bool) => void) | null = null;
333
+
334
+ constructor() {
335
+ super();
336
+ this.popupPresenter = new PopupPresenter(this, this.panel);
337
+
338
+ this.positionAbsolute();
339
+ this.position(0.0, 0.0);
340
+ this.width(100.0, Unit.Percent);
341
+ this.height(100.0, Unit.Percent);
342
+
343
+ this.popupPresenter.overlayNode.onClickWith(this, (menu) => menu.hide());
344
+ Grid.sharedSizeScope(this.panel, true);
345
+
346
+ for (let index = 0; index < ContextMenu.MAX_ITEMS; ++index) {
347
+ this.entries.push(new ContextMenuEntry(index));
348
+ this.separators.push(new ContextMenuSeparator());
349
+ }
350
+ this.applyTheme();
351
+ this.track(activeTheme.addAction(new HandlerAction<ContextMenu, Theme>(this, (menu: ContextMenu, _theme: Theme): void => {
352
+ menu.handleThemeChanged();
353
+ })));
354
+ }
355
+
356
+ build(): u64 {
357
+ const handle = super.build();
358
+ for (let index = 0; index < this.entries.length; ++index) {
359
+ unchecked(this.entries[index]).build();
360
+ unchecked(this.separators[index]).build();
361
+ }
362
+ return handle;
363
+ }
364
+
365
+ static hideActiveMenu(): void {
366
+ const menu = ContextMenu.activeInstance;
367
+ if (menu !== null) {
368
+ menu.hide();
369
+ }
370
+ }
371
+
372
+ static invokeActiveSlot(slot: i32): void {
373
+ const menu = ContextMenu.activeInstance;
374
+ if (menu !== null) {
375
+ menu.invokeSlot(slot);
376
+ }
377
+ }
378
+
379
+ menuWidth(value: f32): this {
380
+ if (value <= 0.0) {
381
+ warn("Layout", "ContextMenu.menuWidth() received " + value.toString() + "; clamping to 1.0.");
382
+ }
383
+ this.menuWidthValue = value > 0.0 ? value : 1.0;
384
+ this.applyTheme();
385
+ return this;
386
+ }
387
+
388
+ itemHeight(value: f32): this {
389
+ this.itemMetricsOverridden = true;
390
+ if (value <= 0.0) {
391
+ warn("Layout", "ContextMenu.itemHeight() received " + value.toString() + "; clamping to 1.0.");
392
+ }
393
+ this.itemHeightValue = value > 0.0 ? value : 1.0;
394
+ this.applyTheme();
395
+ return this;
396
+ }
397
+
398
+ itemPadding(left: f32, top: f32 = left, right: f32 = left, bottom: f32 = top): this {
399
+ this.itemMetricsOverridden = true;
400
+ this.itemPaddingLeftValue = left;
401
+ this.itemPaddingTopValue = top;
402
+ this.itemPaddingRightValue = right;
403
+ this.itemPaddingBottomValue = bottom;
404
+ this.applyTheme();
405
+ return this;
406
+ }
407
+
408
+ onVisibilityChanged(callback: ((visible: bool) => void) | null): this {
409
+ this.visibilityChangedCallback = callback;
410
+ return this;
411
+ }
412
+
413
+ panelColor(color: u32): this {
414
+ this.panelBackgroundOverridden = true;
415
+ this.panelBackgroundColorValue = color;
416
+ this.applyTheme();
417
+ return this;
418
+ }
419
+
420
+ panelBorder(width: f32, color: u32, style: BorderStyle = BorderStyle.Solid): this {
421
+ this.panelBorderOverridden = true;
422
+ this.panelBorderWidthValue = width;
423
+ this.panelBorderColorValue = color;
424
+ this.panelBorderStyleValue = style;
425
+ this.applyTheme();
426
+ return this;
427
+ }
428
+
429
+ panelCornerRadius(radius: f32): this {
430
+ this.panelCornerRadiusOverridden = true;
431
+ this.panelCornerRadiusValue = radius > 0.0 ? radius : 0.0;
432
+ this.applyTheme();
433
+ return this;
434
+ }
435
+
436
+ panelShadow(color: u32, offsetY: f32 = 12.0, blurSigma: f32 = 28.0, spread: f32 = 0.0): this {
437
+ this.panelShadowOverridden = true;
438
+ this.panelShadowColorValue = color;
439
+ this.panelShadowOffsetYValue = offsetY;
440
+ this.panelShadowBlurValue = blurSigma;
441
+ this.panelShadowSpreadValue = spread;
442
+ this.applyTheme();
443
+ return this;
444
+ }
445
+
446
+ panelBackgroundBlur(sigma: f32): this {
447
+ this.panelBackgroundBlurOverridden = true;
448
+ if (sigma < 0.0) {
449
+ warn("Layout", "ContextMenu.panelBackgroundBlur() received " + sigma.toString() + "; clamping to 0.0.");
450
+ }
451
+ this.panelBackgroundBlurSigmaValue = sigma >= 0.0 ? sigma : 0.0;
452
+ this.applyTheme();
453
+ return this;
454
+ }
455
+
456
+ backdropColor(color: u32): this {
457
+ this.popupPresenter.backdropColor(color);
458
+ return this;
459
+ }
460
+
461
+ backgroundBlur(sigma: f32): this {
462
+ this.popupPresenter.backgroundBlur(sigma);
463
+ return this;
464
+ }
465
+
466
+ itemColor(color: u32): this {
467
+ this.itemBackgroundOverridden = true;
468
+ this.itemBackgroundColorValue = color;
469
+ this.applyTheme();
470
+ return this;
471
+ }
472
+
473
+ itemHoverColor(color: u32): this {
474
+ this.itemHoverColorOverridden = true;
475
+ this.itemHoverColorValue = color;
476
+ this.applyTheme();
477
+ return this;
478
+ }
479
+
480
+ itemTextColor(color: u32): this {
481
+ this.itemTextColorOverridden = true;
482
+ this.itemTextColorValue = color;
483
+ this.applyTheme();
484
+ return this;
485
+ }
486
+
487
+ itemCornerRadius(radius: f32): this {
488
+ this.itemCornerRadiusOverridden = true;
489
+ this.itemCornerRadiusValue = radius > 0.0 ? radius : 0.0;
490
+ this.applyTheme();
491
+ return this;
492
+ }
493
+
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
+ itemFontFamily(family: FontFamily): this {
504
+ this.itemFontOverridden = true;
505
+ this.itemFontFamilyValue = family;
506
+ this.itemUsesDirectFontIdValue = false;
507
+ this.applyTheme();
508
+ return this;
509
+ }
510
+
511
+ itemFontWeight(weight: FontWeight): this {
512
+ this.itemFontOverridden = true;
513
+ this.itemFontWeightValue = weight;
514
+ this.itemUsesDirectFontIdValue = false;
515
+ this.applyTheme();
516
+ return this;
517
+ }
518
+
519
+ itemFontStyle(style: FontStyle): this {
520
+ this.itemFontOverridden = true;
521
+ this.itemFontStyleValue = style;
522
+ this.itemUsesDirectFontIdValue = false;
523
+ this.applyTheme();
524
+ return this;
525
+ }
526
+
527
+ itemFontSize(size: f32): this {
528
+ this.itemFontOverridden = true;
529
+ this.itemFontSizeValue = size;
530
+ this.itemUsesDirectFontIdValue = false;
531
+ this.applyTheme();
532
+ return this;
533
+ }
534
+
535
+ separatorColor(color: u32): this {
536
+ this.separatorColorOverridden = true;
537
+ this.separatorColorValue = color;
538
+ this.applyTheme();
539
+ return this;
540
+ }
541
+
542
+ show(items: Array<MenuItem>, x: f32, y: f32): void {
543
+ this.clearPanel();
544
+ this.applyTheme();
545
+
546
+ this.currentItems.length = 0;
547
+ let actionCount = 0;
548
+ let separatorCount = 0;
549
+ let estimatedHeight: f32 = 8.0;
550
+ let lastWasSeparator = true;
551
+ const count = items.length < ContextMenu.MAX_ITEMS ? items.length : ContextMenu.MAX_ITEMS;
552
+ if (items.length > ContextMenu.MAX_ITEMS) {
553
+ warn(
554
+ "Layout",
555
+ "ContextMenu.show() received " +
556
+ items.length.toString() +
557
+ " items; truncating to " +
558
+ ContextMenu.MAX_ITEMS.toString() +
559
+ ".",
560
+ );
561
+ }
562
+
563
+ for (let index = 0; index < count; ++index) {
564
+ const item = unchecked(items[index]);
565
+ if (item.isSeparator) {
566
+ if (lastWasSeparator || index == count - 1) {
567
+ continue;
568
+ }
569
+ const separator = unchecked(this.separators[separatorCount]);
570
+ separator.applyTheme();
571
+ this.panel.addChildNode(separator);
572
+ separatorCount += 1;
573
+ estimatedHeight += MENU_SEPARATOR_HEIGHT;
574
+ lastWasSeparator = true;
575
+ continue;
576
+ }
577
+
578
+ const entry = unchecked(this.entries[actionCount]);
579
+ entry.item(item);
580
+ entry.applyTheme();
581
+ this.currentItems.push(item);
582
+ this.panel.addChildNode(entry);
583
+ actionCount += 1;
584
+ estimatedHeight += this.itemHeightValue;
585
+ lastWasSeparator = false;
586
+ }
587
+
588
+ const maxX = <f32>Math.max(0.0, ui.getViewportWidth() - this.menuWidthValue - MENU_EDGE_PADDING);
589
+ 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));
592
+
593
+ this.popupPresenter.showAtPoint(clampedX, clampedY, this.menuWidthValue, estimatedHeight);
594
+ this.isMenuVisible = true;
595
+ ContextMenu.activeInstance = this;
596
+ const visibilityChangedCallback = this.visibilityChangedCallback;
597
+ if (visibilityChangedCallback !== null) {
598
+ visibilityChangedCallback(true);
599
+ }
600
+ if (this.keyFilterToken == 0) {
601
+ this.keyFilterToken = EventRouter.pushKeyFilter(this);
602
+ }
603
+ }
604
+
605
+ hide(): void {
606
+ if (!this.isMenuVisible && !this.popupPresenter.isOpen) {
607
+ return;
608
+ }
609
+ this.clearPanel();
610
+ this.currentItems.length = 0;
611
+ this.popupPresenter.hide();
612
+ this.isMenuVisible = false;
613
+ if (ContextMenu.activeInstance === this) {
614
+ ContextMenu.activeInstance = null;
615
+ }
616
+ const visibilityChangedCallback = this.visibilityChangedCallback;
617
+ if (visibilityChangedCallback !== null) {
618
+ visibilityChangedCallback(false);
619
+ }
620
+ if (this.keyFilterToken != 0) {
621
+ EventRouter.removeKeyFilter(this.keyFilterToken);
622
+ this.keyFilterToken = 0;
623
+ }
624
+ }
625
+
626
+ dispose(): void {
627
+ this.hide();
628
+ disposeAll(this.disposables);
629
+ this.popupPresenter.dispose();
630
+ for (let index = 0; index < this.entries.length; ++index) {
631
+ const entry = unchecked(this.entries[index]);
632
+ if (entry.builtHandle != <u64>HandleValue.Invalid) {
633
+ entry.dispose();
634
+ }
635
+ const separator = unchecked(this.separators[index]);
636
+ if (separator.builtHandle != <u64>HandleValue.Invalid) {
637
+ separator.dispose();
638
+ }
639
+ }
640
+ super.dispose();
641
+ }
642
+
643
+ private clearPanel(): void {
644
+ for (let index = 0; index < this.entries.length; ++index) {
645
+ this.panel.removeChildNode(unchecked(this.entries[index]));
646
+ this.panel.removeChildNode(unchecked(this.separators[index]));
647
+ }
648
+ }
649
+
650
+ private invokeSlot(slot: i32): void {
651
+ if (slot < 0 || slot >= this.currentItems.length) {
652
+ return;
653
+ }
654
+ this.runAction(unchecked(this.currentItems[slot]));
655
+ this.hide();
656
+ }
657
+
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
+ }
676
+ }
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);
687
+ return;
688
+ }
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);
700
+ return;
701
+ }
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;
725
+ }
726
+ if (ffi.fui_cut_focused_text_selection()) {
727
+ this.commitFocusedTextAction(item.targetHandle);
728
+ return;
729
+ }
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
+ }
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);
769
+ return;
770
+ }
771
+ if (item.payload !== null && item.action == ContextMenuAction.OpenImageInNewTab) {
772
+ navigateTo(changetype<string>(item.payload), true);
773
+ }
774
+ }
775
+
776
+ private applyTheme(): void {
777
+ this.panel.width(this.menuWidthValue, Unit.Pixel);
778
+ this.panel.bgColor(this.panelBackgroundColorValue);
779
+ this.panel.backgroundBlur(this.panelBackgroundBlurSigmaValue);
780
+ this.panel.cornerRadius(this.panelCornerRadiusValue);
781
+ this.panel.border(this.panelBorderWidthValue, this.panelBorderColorValue, this.panelBorderStyleValue);
782
+ this.panel.dropShadow(
783
+ this.panelShadowColorValue,
784
+ 0.0,
785
+ this.panelShadowOffsetYValue,
786
+ this.panelShadowBlurValue,
787
+ this.panelShadowSpreadValue,
788
+ );
789
+ for (let index = 0; index < this.entries.length; ++index) {
790
+ unchecked(this.entries[index]).configureStyle(
791
+ this.itemHeightValue,
792
+ this.itemPaddingLeftValue,
793
+ this.itemPaddingTopValue,
794
+ this.itemPaddingRightValue,
795
+ this.itemPaddingBottomValue,
796
+ this.itemCornerRadiusValue,
797
+ this.itemTextColorValue,
798
+ this.itemBackgroundColorValue,
799
+ this.itemHoverColorValue,
800
+ this.itemFontIdValue,
801
+ this.itemFontFamilyValue,
802
+ this.itemFontSizeValue,
803
+ this.itemFontWeightValue,
804
+ this.itemFontStyleValue,
805
+ this.itemUsesDirectFontIdValue,
806
+ );
807
+ unchecked(this.separators[index]).configureStyle(this.separatorColorValue);
808
+ }
809
+ }
810
+
811
+ private handleThemeChanged(): void {
812
+ const theme = activeTheme.value;
813
+ if (!this.panelBackgroundOverridden) {
814
+ this.panelBackgroundColorValue = theme.colors.surface;
815
+ }
816
+ if (!this.panelBorderOverridden) {
817
+ this.panelBorderWidthValue = 1.0;
818
+ this.panelBorderColorValue = theme.colors.border;
819
+ this.panelBorderStyleValue = BorderStyle.Solid;
820
+ }
821
+ if (!this.panelCornerRadiusOverridden) {
822
+ this.panelCornerRadiusValue = theme.contextMenu.panelCornerRadius;
823
+ }
824
+ if (!this.itemTextColorOverridden) {
825
+ this.itemTextColorValue = theme.contextMenu.item.textColor;
826
+ }
827
+ if (!this.itemBackgroundOverridden) {
828
+ this.itemBackgroundColorValue = theme.contextMenu.item.background;
829
+ }
830
+ if (!this.itemHoverColorOverridden) {
831
+ this.itemHoverColorValue = theme.contextMenu.item.hoverBackground;
832
+ }
833
+ if (!this.itemCornerRadiusOverridden) {
834
+ this.itemCornerRadiusValue = theme.contextMenu.item.cornerRadius;
835
+ }
836
+ if (!this.itemFontOverridden) {
837
+ this.itemFontIdValue = theme.contextMenu.item.fontId;
838
+ this.itemFontFamilyValue = theme.contextMenu.item.fontFamily;
839
+ this.itemFontSizeValue = theme.contextMenu.item.fontSize;
840
+ this.itemFontWeightValue = FontWeight.Regular;
841
+ this.itemFontStyleValue = FontStyle.Normal;
842
+ this.itemUsesDirectFontIdValue = false;
843
+ }
844
+ if (!this.separatorColorOverridden) {
845
+ this.separatorColorValue = theme.contextMenu.separatorColor;
846
+ }
847
+ if (!this.panelShadowOverridden) {
848
+ this.panelShadowColorValue = theme.contextMenu.panelShadowColor;
849
+ this.panelShadowOffsetYValue = theme.contextMenu.shadowOffsetY;
850
+ this.panelShadowBlurValue = theme.contextMenu.shadowBlur;
851
+ this.panelShadowSpreadValue = theme.contextMenu.shadowSpread;
852
+ }
853
+ if (!this.panelBackgroundBlurOverridden) {
854
+ this.panelBackgroundBlurSigmaValue = DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA;
855
+ }
856
+ if (!this.itemMetricsOverridden) {
857
+ this.itemHeightValue = theme.contextMenu.item.height;
858
+ this.itemPaddingLeftValue = theme.contextMenu.item.paddingLeft;
859
+ this.itemPaddingTopValue = theme.contextMenu.item.paddingTop;
860
+ this.itemPaddingRightValue = theme.contextMenu.item.paddingRight;
861
+ this.itemPaddingBottomValue = theme.contextMenu.item.paddingBottom;
862
+ }
863
+ if (!this.panelBackgroundOverridden) {
864
+ this.panelBackgroundColorValue = theme.contextMenu.panelBackground;
865
+ }
866
+ if (!this.panelBorderOverridden) {
867
+ this.panelBorderWidthValue = 1.0;
868
+ this.panelBorderColorValue = theme.contextMenu.panelBorderColor;
869
+ this.panelBorderStyleValue = BorderStyle.Solid;
870
+ }
871
+ this.applyTheme();
872
+ }
873
+
874
+ handleGlobalKeyEvent(eventType: KeyEventType, key: string, _modifiers: u32): bool {
875
+ if (eventType == KeyEventType.Down && key == "Escape") {
876
+ this.hide();
877
+ return true;
878
+ }
879
+ return false;
880
+ }
881
+
882
+ private track(disposable: Disposable): void {
883
+ this.disposables.push(disposable);
884
+ }
885
+ }