@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,856 @@
1
+ import * as ui from "../bindings/ui";
2
+ import { HandlerAction } from "../core/Action";
3
+ import { Callback2, Handler2 } from "../core/BoundCallback";
4
+ import { Disposable, disposeAll } from "../core/Disposable";
5
+ import { EventRouter, GlobalKeyHandler } from "../core/EventRouter";
6
+ import { FocusAdornerManager } from "../core/FocusAdornerManager";
7
+ import { keyboardFocusVisible } from "../core/FocusVisibility";
8
+ import {
9
+ AlignItems,
10
+ BorderStyle,
11
+ CursorStyle,
12
+ FlexDirection,
13
+ KeyEventType,
14
+ PointerEventType,
15
+ SemanticRole,
16
+ Unit,
17
+ } from "../core/ffi";
18
+ import { Theme, activeTheme } from "../core/Theme";
19
+ import { warn } from "../core/Logger";
20
+ import { Node } from "../core/Node";
21
+ import { PersistedInt32Codec, PersistedValueState } from "../core/PersistedState";
22
+ import { FlexBox, Portal, ScrollBarVisibility, ScrollBox } from "../nodes";
23
+ import { bind2 } from "../core/bind";
24
+ import { getControlTemplates } from "./ControlTemplateSet";
25
+ import {
26
+ defaultDropdownChevronTemplate,
27
+ DropdownChevronPresenter,
28
+ DropdownChevronTemplate,
29
+ DropdownChevronVisualState,
30
+ } from "./internal/DropdownChevronPresenter";
31
+ import {
32
+ defaultDropdownFieldTemplate,
33
+ DropdownFieldPresenter,
34
+ DropdownFieldTemplate,
35
+ DropdownFieldVisualState,
36
+ } from "./internal/DropdownFieldPresenter";
37
+ import {
38
+ defaultDropdownOptionRowTemplate,
39
+ DropdownOptionRowPresenter,
40
+ DropdownOptionRowTemplate,
41
+ DropdownOptionRowVisualState,
42
+ } from "./internal/DropdownOptionRowPresenter";
43
+ import { PopupPresenter } from "./internal/PopupPresenter";
44
+
45
+ const PANEL_EDGE_PADDING: f32 = 8.0;
46
+ const OPTION_HEIGHT: f32 = 34.0;
47
+ const PANEL_PADDING: f32 = 4.0;
48
+ const UNLIMITED_VISIBLE_ITEMS: i32 = 0;
49
+ const DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA: f32 = 10.0;
50
+ const DROPDOWN_PERSISTED_CODEC = new PersistedInt32Codec();
51
+
52
+ function isActivationKey(key: string): bool {
53
+ return key == "Enter" || key == " " || key == "ArrowDown";
54
+ }
55
+
56
+ class PersistedDropdownState extends PersistedValueState<Dropdown, i32> {
57
+ constructor() {
58
+ super("dropdown-selected-index", DROPDOWN_PERSISTED_CODEC, 1);
59
+ }
60
+
61
+ protected shouldCaptureValue(node: Dropdown): bool {
62
+ return node.selectedIndex >= 0;
63
+ }
64
+
65
+ protected captureValue(node: Dropdown): i32 {
66
+ return node.selectedIndex;
67
+ }
68
+
69
+ protected restoreValue(node: Dropdown, value: i32): void {
70
+ node._applyPersistedSelectedIndex(value);
71
+ }
72
+ }
73
+
74
+ const DROPDOWN_PERSISTED_STATE = new PersistedDropdownState();
75
+
76
+ function createFieldPresenter(template: DropdownFieldTemplate | null): DropdownFieldPresenter {
77
+ if (template !== null) {
78
+ return template.create();
79
+ }
80
+ const templateSet = getControlTemplates();
81
+ const appTemplate = templateSet !== null ? templateSet.dropdownField : null;
82
+ return (appTemplate === null ? defaultDropdownFieldTemplate : appTemplate).create();
83
+ }
84
+
85
+ function createChevronPresenter(template: DropdownChevronTemplate | null): DropdownChevronPresenter {
86
+ if (template !== null) {
87
+ return template.create();
88
+ }
89
+ const templateSet = getControlTemplates();
90
+ const appTemplate = templateSet !== null ? templateSet.dropdownChevron : null;
91
+ return (appTemplate === null ? defaultDropdownChevronTemplate : appTemplate).create();
92
+ }
93
+
94
+ function createOptionRowPresenter(template: DropdownOptionRowTemplate | null): DropdownOptionRowPresenter {
95
+ if (template !== null) {
96
+ return template.create();
97
+ }
98
+ const templateSet = getControlTemplates();
99
+ const appTemplate = templateSet !== null ? templateSet.dropdownOptionRow : null;
100
+ return (appTemplate === null ? defaultDropdownOptionRowTemplate : appTemplate).create();
101
+ }
102
+
103
+ export class DropdownItem {
104
+ constructor(readonly value: string, readonly label: string = value) {}
105
+ }
106
+
107
+ class DropdownOptionNode extends FlexBox {
108
+ private presenter: DropdownOptionRowPresenter;
109
+ private owner: Dropdown | null = null;
110
+ private slotIndex: i32 = -1;
111
+ private currentLabel: string = "";
112
+
113
+ constructor(template: DropdownOptionRowTemplate | null) {
114
+ super();
115
+ this.presenter = createOptionRowPresenter(template);
116
+ this.semanticRole(SemanticRole.ListItem);
117
+ this.width(100.0, Unit.Percent);
118
+ this.cursor(CursorStyle.Pointer);
119
+ this.focusable(false);
120
+ this.requireInteractive();
121
+ this.child(this.presenter.root);
122
+ this.syncPresenterLayout();
123
+ }
124
+
125
+ bindOwner(owner: Dropdown, slotIndex: i32): this {
126
+ this.owner = owner;
127
+ this.slotIndex = slotIndex;
128
+ return this;
129
+ }
130
+
131
+ label(label: string): this {
132
+ this.currentLabel = label;
133
+ this.semanticLabel(label);
134
+ this.presenter.labelNode.text(label);
135
+ return this;
136
+ }
137
+
138
+ template(template: DropdownOptionRowTemplate | null): void {
139
+ const previousPresenter = this.presenter;
140
+ const nextPresenter = createOptionRowPresenter(template);
141
+ this.presenter = nextPresenter;
142
+ this.removeChildNode(previousPresenter.root);
143
+ this.addChildNode(nextPresenter.root);
144
+ previousPresenter.root.dispose();
145
+ nextPresenter.labelNode.text(this.currentLabel);
146
+ this.syncPresenterLayout();
147
+ }
148
+
149
+ get rowHeight(): f32 {
150
+ return this.presenter.metrics.height;
151
+ }
152
+
153
+ applyTheme(theme: Theme, highlighted: bool, selected: bool, enabled: bool): void {
154
+ this.semanticSelected(selected);
155
+ this.semanticDisabled(!enabled);
156
+ this.presenter.apply(theme, new DropdownOptionRowVisualState(highlighted, selected, enabled));
157
+ }
158
+
159
+ _handlePointerEvent(eventType: PointerEventType, x: f32, y: f32, modifiers: u32 = 0): void {
160
+ super._handlePointerEvent(eventType, x, y, modifiers);
161
+ const owner = this.owner;
162
+ if (owner === null) {
163
+ return;
164
+ }
165
+ if (eventType == PointerEventType.Enter) {
166
+ owner.highlightIndex(this.slotIndex);
167
+ return;
168
+ }
169
+ if (eventType == PointerEventType.Up) {
170
+ owner.selectHighlighted();
171
+ }
172
+ }
173
+
174
+ private syncPresenterLayout(): void {
175
+ this.height(this.presenter.metrics.height, Unit.Pixel);
176
+ this.presenter.root
177
+ .width(100.0, Unit.Percent)
178
+ .height(100.0, Unit.Percent);
179
+ }
180
+ }
181
+
182
+ export class Dropdown extends FlexBox implements GlobalKeyHandler {
183
+ private static activeInstance: Dropdown | null = null;
184
+
185
+ private fieldTemplateValue: DropdownFieldTemplate | null = null;
186
+ private chevronTemplateValue: DropdownChevronTemplate | null = null;
187
+ private optionRowTemplateValue: DropdownOptionRowTemplate | null = null;
188
+ private fieldPresenter: DropdownFieldPresenter;
189
+ private chevronPresenter: DropdownChevronPresenter;
190
+ private readonly popupRoot: Portal;
191
+ private readonly panelNode: FlexBox;
192
+ private readonly popupPresenter: PopupPresenter;
193
+ private readonly popupScrollBox: ScrollBox;
194
+ private readonly optionsHost: FlexBox;
195
+ private readonly optionNodes: Array<DropdownOptionNode> = new Array<DropdownOptionNode>();
196
+ private readonly itemsValue: Array<DropdownItem> = new Array<DropdownItem>();
197
+ private readonly disposables: Array<Disposable> = new Array<Disposable>();
198
+ private disposed: bool = false;
199
+ private openState: bool = false;
200
+ private pointerPressedState: bool = false;
201
+ private hoveredState: bool = false;
202
+ private focusedState: bool = false;
203
+ private keyFilterToken: u32 = 0;
204
+ private selectedIndexValue: i32 = -1;
205
+ private highlightedIndexValue: i32 = -1;
206
+ private maxVisibleItemsValue: i32 = UNLIMITED_VISIBLE_ITEMS;
207
+ private popupWidthValue: f32 = 0.0;
208
+ private popupPanelColorValue: u32 = 0x00000000;
209
+ private popupPanelBackgroundBlurSigmaValue: f32 = DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA;
210
+ private popupPanelColorOverridden: bool = false;
211
+ private popupPanelBackgroundBlurOverridden: bool = false;
212
+ private changedCallback: ((item: DropdownItem, index: i32) => void) | null = null;
213
+ private changedBinding: Callback2<DropdownItem, i32> | null = null;
214
+
215
+ constructor() {
216
+ super();
217
+ const fieldPresenter = createFieldPresenter(null);
218
+ const chevronPresenter = createChevronPresenter(null);
219
+ const popupRoot = new Portal()
220
+ .positionAbsolute()
221
+ .position(0.0, 0.0)
222
+ .width(100.0, Unit.Percent)
223
+ .height(100.0, Unit.Percent) as Portal;
224
+ const popupScrollBox = new ScrollBox()
225
+ .scrollEnabledX(false)
226
+ .scrollEnabledY(true)
227
+ .horizontalScrollbarVisibility(ScrollBarVisibility.Never)
228
+ .verticalScrollbarVisibility(ScrollBarVisibility.Auto);
229
+ const optionsHost = new FlexBox()
230
+ .flexDirection(FlexDirection.Column);
231
+ const panelNode = new FlexBox()
232
+ .positionAbsolute()
233
+ .flexDirection(FlexDirection.Column);
234
+ const popupPresenter = new PopupPresenter(popupRoot, panelNode);
235
+ this.fieldPresenter = fieldPresenter;
236
+ this.chevronPresenter = chevronPresenter;
237
+ this.popupRoot = popupRoot;
238
+ this.panelNode = panelNode;
239
+ this.popupPresenter = popupPresenter;
240
+ this.popupScrollBox = popupScrollBox;
241
+ this.optionsHost = optionsHost;
242
+ popupPresenter.overlayNode.onClickWith(this, (dropdown) => dropdown.close());
243
+ optionsHost.semanticRole(SemanticRole.List);
244
+ optionsHost.semanticLabel("Dropdown options");
245
+ popupScrollBox.child(optionsHost);
246
+ panelNode.child(popupScrollBox);
247
+ this.semanticRole(SemanticRole.ComboBox);
248
+ this.focusable(true);
249
+ this.requireInteractive();
250
+ this.reflectSemanticDisabledFromEnabled();
251
+ this.cursor(CursorStyle.Pointer);
252
+ this.flexDirection(FlexDirection.Row);
253
+ this.alignItems(AlignItems.Center);
254
+ fieldPresenter.root.flexGrow(1.0);
255
+ fieldPresenter.chevronHost.child(chevronPresenter.root);
256
+ this.child(fieldPresenter.root);
257
+ this.child(this.popupRoot);
258
+ this.track(activeTheme.addAction(new HandlerAction<Dropdown, Theme>(this, (dropdown: Dropdown, _theme: Theme): void => {
259
+ dropdown.handleThemeChanged();
260
+ })));
261
+ this.track(keyboardFocusVisible.addAction(new HandlerAction<Dropdown, bool>(this, (dropdown: Dropdown, _visible: bool): void => {
262
+ dropdown.handleThemeChanged();
263
+ })));
264
+ this.semanticExpanded(false);
265
+ this.setDefaultSemanticLabel("Dropdown");
266
+ this.handleThemeChanged();
267
+ this.persistState(DROPDOWN_PERSISTED_STATE);
268
+ }
269
+
270
+ get selectedIndex(): i32 {
271
+ return this.selectedIndexValue;
272
+ }
273
+
274
+ items(items: Array<DropdownItem>): this {
275
+ this.clearItems();
276
+ for (let index = 0; index < items.length; ++index) {
277
+ this.itemsValue.push(unchecked(items[index]));
278
+ }
279
+ if (this.selectedIndexValue >= this.itemsValue.length) {
280
+ this.selectedIndexValue = this.itemsValue.length > 0 ? 0 : -1;
281
+ } else if (this.selectedIndexValue < 0 && this.itemsValue.length > 0) {
282
+ this.selectedIndexValue = 0;
283
+ }
284
+ this.ensureOptionNodes();
285
+ this.syncValueLabel();
286
+ this.handleThemeChanged();
287
+ return this;
288
+ }
289
+
290
+ onChanged(callback: ((item: DropdownItem, index: i32) => void) | null): this {
291
+ this.changedCallback = callback;
292
+ this.changedBinding = null;
293
+ return this;
294
+ }
295
+
296
+ bindChanged<Owner>(owner: Owner, handler: Handler2<Owner, DropdownItem, i32>): this {
297
+ this.changedCallback = null;
298
+ this.changedBinding = bind2<Owner, DropdownItem, i32>(owner, handler);
299
+ return this;
300
+ }
301
+
302
+ onChangedWith<Owner>(owner: Owner, handler: Handler2<Owner, DropdownItem, i32>): this {
303
+ this.bindChanged(owner, handler);
304
+ return this;
305
+ }
306
+
307
+ maxVisibleItems(count: i32): this {
308
+ if (count <= 0) {
309
+ warn("Layout", "Dropdown.maxVisibleItems() received " + count.toString() + "; using unlimited visible items.");
310
+ }
311
+ this.maxVisibleItemsValue = count > 0 ? count : UNLIMITED_VISIBLE_ITEMS;
312
+ this.refreshPanelLayout();
313
+ return this;
314
+ }
315
+
316
+ popupWidth(value: f32): this {
317
+ if (value <= 0.0) {
318
+ warn("Layout", "Dropdown.popupWidth() received " + value.toString() + "; clamping to 0.0.");
319
+ }
320
+ this.popupWidthValue = value > 0.0 ? value : 0.0;
321
+ this.refreshPanelLayout();
322
+ return this;
323
+ }
324
+
325
+ popupPanelColor(color: u32): this {
326
+ this.popupPanelColorOverridden = true;
327
+ this.popupPanelColorValue = color;
328
+ this.panelNode.bgColor(color);
329
+ return this;
330
+ }
331
+
332
+ popupPanelBackgroundBlur(sigma: f32): this {
333
+ this.popupPanelBackgroundBlurOverridden = true;
334
+ if (sigma < 0.0) {
335
+ warn("Layout", "Dropdown.popupPanelBackgroundBlur() received " + sigma.toString() + "; clamping to 0.0.");
336
+ }
337
+ this.popupPanelBackgroundBlurSigmaValue = sigma >= 0.0 ? sigma : 0.0;
338
+ this.panelNode.backgroundBlur(this.popupPanelBackgroundBlurSigmaValue);
339
+ return this;
340
+ }
341
+
342
+ fieldTemplate(template: DropdownFieldTemplate | null): this {
343
+ this.close();
344
+ this.fieldTemplateValue = template;
345
+ const nextFieldPresenter = createFieldPresenter(template);
346
+ const nextChevronPresenter = createChevronPresenter(this.chevronTemplateValue);
347
+ this.replaceFieldPresenter(nextFieldPresenter, nextChevronPresenter);
348
+ this.syncValueLabel();
349
+ this.handleThemeChanged();
350
+ return this;
351
+ }
352
+
353
+ chevronTemplate(template: DropdownChevronTemplate | null): this {
354
+ this.close();
355
+ this.chevronTemplateValue = template;
356
+ const previousPresenter = this.chevronPresenter;
357
+ const nextPresenter = createChevronPresenter(template);
358
+ this.chevronPresenter = nextPresenter;
359
+ this.fieldPresenter.chevronHost.removeChildNode(previousPresenter.root);
360
+ this.fieldPresenter.chevronHost.addChildNode(nextPresenter.root);
361
+ previousPresenter.root.dispose();
362
+ this.handleThemeChanged();
363
+ return this;
364
+ }
365
+
366
+ optionRowTemplate(template: DropdownOptionRowTemplate | null): this {
367
+ this.close();
368
+ this.optionRowTemplateValue = template;
369
+ for (let index = 0; index < this.optionNodes.length; ++index) {
370
+ unchecked(this.optionNodes[index]).template(template);
371
+ }
372
+ this.refreshPanelLayout();
373
+ this.syncOptionVisuals();
374
+ return this;
375
+ }
376
+
377
+ selectIndex(index: i32): this {
378
+ this.setSelectedIndex(index, false);
379
+ return this;
380
+ }
381
+
382
+ _applyPersistedSelectedIndex(index: i32): void {
383
+ this.setSelectedIndex(index, true);
384
+ }
385
+
386
+ dispose(): void {
387
+ this.close();
388
+ this.disposeControl();
389
+ super.dispose();
390
+ }
391
+
392
+ static hideActiveDropdown(): void {
393
+ const dropdown = Dropdown.activeInstance;
394
+ if (dropdown !== null) {
395
+ dropdown.close();
396
+ }
397
+ }
398
+
399
+ highlightIndex(index: i32): void {
400
+ if (index < 0 || index >= this.itemsValue.length || this.highlightedIndexValue == index) {
401
+ if (index < 0 || index >= this.itemsValue.length) {
402
+ warn("Layout", "Dropdown.highlightIndex() received " + index.toString() + " outside the available item range.");
403
+ }
404
+ return;
405
+ }
406
+ this.highlightedIndexValue = index;
407
+ this.syncOptionVisuals();
408
+ this.ensureHighlightedVisible();
409
+ }
410
+
411
+ selectHighlighted(): void {
412
+ if (this.highlightedIndexValue < 0 || this.highlightedIndexValue >= this.itemsValue.length) {
413
+ return;
414
+ }
415
+ this.setSelectedIndex(this.highlightedIndexValue, true);
416
+ this.close();
417
+ }
418
+
419
+ handleGlobalKeyEvent(eventType: KeyEventType, key: string, modifiers: u32): bool {
420
+ if (!this.openState || modifiers != 0) {
421
+ return false;
422
+ }
423
+ if (eventType != KeyEventType.Down) {
424
+ return false;
425
+ }
426
+ if (key == "Escape") {
427
+ this.close();
428
+ return true;
429
+ }
430
+ if (key == "Enter") {
431
+ this.selectHighlighted();
432
+ return true;
433
+ }
434
+ if (key == "Home") {
435
+ this.highlightIndex(0);
436
+ return true;
437
+ }
438
+ if (key == "End") {
439
+ this.highlightIndex(this.itemsValue.length - 1);
440
+ return true;
441
+ }
442
+ if (key == "ArrowDown") {
443
+ this.moveHighlight(1);
444
+ return true;
445
+ }
446
+ if (key == "ArrowUp") {
447
+ this.moveHighlight(-1);
448
+ return true;
449
+ }
450
+ return false;
451
+ }
452
+
453
+ _handlePointerEvent(eventType: PointerEventType, x: f32, y: f32, modifiers: u32 = 0): void {
454
+ super._handlePointerEvent(eventType, x, y, modifiers);
455
+ if (!this.isEnabled) {
456
+ return;
457
+ }
458
+ if (eventType == PointerEventType.Enter) {
459
+ this.hoveredState = true;
460
+ this.handleThemeChanged();
461
+ return;
462
+ }
463
+ if (eventType == PointerEventType.Leave) {
464
+ this.pointerPressedState = false;
465
+ this.hoveredState = false;
466
+ this.handleThemeChanged();
467
+ return;
468
+ }
469
+ if (eventType == PointerEventType.Down) {
470
+ this.pointerPressedState = true;
471
+ this.handleThemeChanged();
472
+ return;
473
+ }
474
+ if (eventType == PointerEventType.Up && this.pointerPressedState) {
475
+ this.pointerPressedState = false;
476
+ if (this.openState) {
477
+ this.close();
478
+ } else {
479
+ this.open();
480
+ }
481
+ this.handleThemeChanged();
482
+ }
483
+ }
484
+
485
+ _handleKeyEvent(eventType: KeyEventType, key: string, modifiers: u32): bool {
486
+ const callbackHandled = super._handleKeyEvent(eventType, key, modifiers);
487
+ if (!this.isEnabled || modifiers != 0 || eventType != KeyEventType.Down) {
488
+ return callbackHandled;
489
+ }
490
+ if (!this.openState && isActivationKey(key)) {
491
+ this.open();
492
+ return true;
493
+ }
494
+ if (!this.openState && key == "ArrowUp") {
495
+ this.open();
496
+ this.moveHighlight(-1);
497
+ return true;
498
+ }
499
+ return callbackHandled;
500
+ }
501
+
502
+ _handleFocusChanged(focused: bool): void {
503
+ super._handleFocusChanged(focused);
504
+ this.focusedState = focused;
505
+ if (!focused && !this.openState) {
506
+ this.pointerPressedState = false;
507
+ }
508
+ this.handleThemeChanged();
509
+ }
510
+
511
+ protected _onEffectiveEnabledChanged(_isEnabled: bool): void {
512
+ if (!this.isEnabled) {
513
+ this.pointerPressedState = false;
514
+ this.hoveredState = false;
515
+ this.close();
516
+ }
517
+ this.handleThemeChanged();
518
+ }
519
+
520
+ private setSelectedIndex(index: i32, emit: bool): void {
521
+ if (index == -1) {
522
+ this.selectedIndexValue = -1;
523
+ this.highlightedIndexValue = -1;
524
+ this.syncValueLabel();
525
+ this.handleThemeChanged();
526
+ return;
527
+ }
528
+ if (this.itemsValue.length == 0) {
529
+ if (index != -1) {
530
+ warn("Layout", "Dropdown.selectIndex() received " + index.toString() + " before any items were assigned.");
531
+ }
532
+ return;
533
+ }
534
+ const clampedIndex = index < 0
535
+ ? 0
536
+ : (index >= this.itemsValue.length ? this.itemsValue.length - 1 : index);
537
+ if (clampedIndex != index) {
538
+ warn(
539
+ "Layout",
540
+ "Dropdown.selectIndex() received " +
541
+ index.toString() +
542
+ "; clamping to " +
543
+ clampedIndex.toString() +
544
+ ".",
545
+ );
546
+ }
547
+ const changed = this.selectedIndexValue != clampedIndex;
548
+ this.selectedIndexValue = clampedIndex;
549
+ this.highlightedIndexValue = clampedIndex;
550
+ this.syncValueLabel();
551
+ this.handleThemeChanged();
552
+ if (emit && changed) {
553
+ this.requestSemanticAnnouncement();
554
+ this.emitSelectionChanged();
555
+ }
556
+ }
557
+
558
+ private emitSelectionChanged(): void {
559
+ if (this.selectedIndexValue < 0 || this.selectedIndexValue >= this.itemsValue.length) {
560
+ return;
561
+ }
562
+ const item = unchecked(this.itemsValue[this.selectedIndexValue]);
563
+ const callback = this.changedCallback;
564
+ if (callback !== null) {
565
+ callback(item, this.selectedIndexValue);
566
+ }
567
+ const binding = this.changedBinding;
568
+ if (binding !== null) {
569
+ binding.invoke(item, this.selectedIndexValue);
570
+ }
571
+ }
572
+
573
+ private open(): void {
574
+ if (this.openState || this.itemsValue.length == 0 || this.builtHandle == 0) {
575
+ return;
576
+ }
577
+ this.ensureOptionNodes();
578
+ this.rebuildPanel();
579
+ const bounds = this.tryGetViewportBounds();
580
+ if (bounds !== null) {
581
+ const width = unchecked(bounds[2]);
582
+ const height = unchecked(bounds[3]);
583
+ this.positionPanel(unchecked(bounds[0]), unchecked(bounds[1]), width, height);
584
+ }
585
+ this.openState = true;
586
+ Dropdown.activeInstance = this;
587
+ this.semanticExpanded(true);
588
+ this.requestSemanticAnnouncement();
589
+ if (this.selectedIndexValue >= 0) {
590
+ this.highlightedIndexValue = this.selectedIndexValue;
591
+ } else if (this.itemsValue.length > 0) {
592
+ this.highlightedIndexValue = 0;
593
+ }
594
+ this.syncOptionVisuals();
595
+ if (this.keyFilterToken == 0) {
596
+ this.keyFilterToken = EventRouter.pushKeyFilter(this);
597
+ }
598
+ this.handleThemeChanged();
599
+ }
600
+
601
+ private tryGetViewportBounds(): Float32Array | null {
602
+ if (this.builtHandle == 0) {
603
+ return null;
604
+ }
605
+ return ui.tryGetBounds(this.builtHandle);
606
+ }
607
+
608
+ private positionPanel(triggerX: f32, triggerY: f32, triggerWidth: f32, triggerHeight: f32): void {
609
+ const popupWidth = this.resolvePopupWidth(triggerWidth);
610
+ const panelHeight = this.resolveViewportClampedPanelOuterHeight();
611
+ this.panelNode.width(popupWidth, Unit.Pixel);
612
+ this.panelNode.height(panelHeight, Unit.Pixel);
613
+ this.popupScrollBox.width(100.0, Unit.Percent);
614
+ this.popupScrollBox.height(<f32>Math.max(0.0, panelHeight - (PANEL_PADDING * 2.0)), Unit.Pixel);
615
+ this.popupPresenter.showAnchored(triggerX, triggerY, triggerWidth, triggerHeight, popupWidth, panelHeight);
616
+ }
617
+
618
+ private close(): void {
619
+ if (!this.openState && !this.popupPresenter.isOpen) {
620
+ return;
621
+ }
622
+ this.popupPresenter.hide();
623
+ this.openState = false;
624
+ if (Dropdown.activeInstance === this) {
625
+ Dropdown.activeInstance = null;
626
+ }
627
+ this.semanticExpanded(false);
628
+ this.requestSemanticAnnouncement();
629
+ if (this.keyFilterToken != 0) {
630
+ EventRouter.removeKeyFilter(this.keyFilterToken);
631
+ this.keyFilterToken = 0;
632
+ }
633
+ this.handleThemeChanged();
634
+ }
635
+
636
+ private rebuildPanel(): void {
637
+ for (let index = 0; index < this.optionNodes.length; ++index) {
638
+ this.optionsHost.removeChildNode(unchecked(this.optionNodes[index]));
639
+ }
640
+ for (let index = 0; index < this.itemsValue.length; ++index) {
641
+ const optionNode = unchecked(this.optionNodes[index]);
642
+ optionNode.label(unchecked(this.itemsValue[index]).label);
643
+ this.optionsHost.addChildNode(optionNode);
644
+ }
645
+ this.refreshPanelLayout();
646
+ }
647
+
648
+ private syncValueLabel(): void {
649
+ if (this.selectedIndexValue >= 0 && this.selectedIndexValue < this.itemsValue.length) {
650
+ const label = unchecked(this.itemsValue[this.selectedIndexValue]).label;
651
+ this.fieldPresenter.valueNode.text(label);
652
+ this.setDefaultSemanticLabel(label);
653
+ return;
654
+ }
655
+ this.fieldPresenter.valueNode.text("");
656
+ this.setDefaultSemanticLabel("Dropdown");
657
+ }
658
+
659
+ private syncOptionVisuals(): void {
660
+ const theme = activeTheme.value;
661
+ for (let index = 0; index < this.itemsValue.length; ++index) {
662
+ unchecked(this.optionNodes[index]).applyTheme(
663
+ theme,
664
+ index == this.highlightedIndexValue,
665
+ index == this.selectedIndexValue,
666
+ this.isEnabled,
667
+ );
668
+ }
669
+ }
670
+
671
+ private moveHighlight(delta: i32): void {
672
+ if (this.itemsValue.length == 0) {
673
+ return;
674
+ }
675
+ let nextIndex = this.highlightedIndexValue;
676
+ if (nextIndex < 0) {
677
+ nextIndex = this.selectedIndexValue >= 0 ? this.selectedIndexValue : 0;
678
+ }
679
+ nextIndex += delta;
680
+ if (nextIndex < 0) {
681
+ nextIndex = this.itemsValue.length - 1;
682
+ } else if (nextIndex >= this.itemsValue.length) {
683
+ nextIndex = 0;
684
+ }
685
+ this.highlightIndex(nextIndex);
686
+ }
687
+
688
+ private clearItems(): void {
689
+ this.close();
690
+ this.itemsValue.length = 0;
691
+ this.selectedIndexValue = -1;
692
+ this.highlightedIndexValue = -1;
693
+ }
694
+
695
+ private ensureOptionNodes(): void {
696
+ while (this.optionNodes.length < this.itemsValue.length) {
697
+ const optionNode = new DropdownOptionNode(this.optionRowTemplateValue).bindOwner(this, this.optionNodes.length);
698
+ this.optionNodes.push(optionNode);
699
+ }
700
+ }
701
+
702
+ private resolveOptionRowHeight(): f32 {
703
+ if (this.optionNodes.length == 0) {
704
+ return OPTION_HEIGHT;
705
+ }
706
+ return unchecked(this.optionNodes[0]).rowHeight;
707
+ }
708
+
709
+ private resolveVisibleItemCount(): i32 {
710
+ if (this.maxVisibleItemsValue <= 0 || this.itemsValue.length <= this.maxVisibleItemsValue) {
711
+ return this.itemsValue.length;
712
+ }
713
+ return this.maxVisibleItemsValue;
714
+ }
715
+
716
+ private resolvePanelOuterHeight(): f32 {
717
+ return <f32>this.resolveVisibleItemCount() * this.resolveOptionRowHeight() + (PANEL_PADDING * 2.0);
718
+ }
719
+
720
+ private resolveViewportClampedPanelOuterHeight(): f32 {
721
+ const maxHeight = <f32>Math.max(PANEL_EDGE_PADDING, ui.getViewportHeight() - (PANEL_EDGE_PADDING * 2.0));
722
+ return <f32>Math.min(this.resolvePanelOuterHeight(), maxHeight);
723
+ }
724
+
725
+ private resolvePopupWidth(triggerWidth: f32): f32 {
726
+ return this.popupWidthValue > 0.0 ? this.popupWidthValue : triggerWidth;
727
+ }
728
+
729
+ private refreshPanelLayout(): void {
730
+ this.optionsHost.width(100.0, Unit.Percent);
731
+ this.optionsHost.height(<f32>this.itemsValue.length * this.resolveOptionRowHeight(), Unit.Pixel);
732
+ this.popupScrollBox.width(100.0, Unit.Percent);
733
+ this.popupScrollBox.height(
734
+ <f32>Math.max(0.0, this.resolveViewportClampedPanelOuterHeight() - (PANEL_PADDING * 2.0)),
735
+ Unit.Pixel,
736
+ );
737
+ if (this.openState) {
738
+ const bounds = this.tryGetViewportBounds();
739
+ if (bounds !== null) {
740
+ this.positionPanel(unchecked(bounds[0]), unchecked(bounds[1]), unchecked(bounds[2]), unchecked(bounds[3]));
741
+ }
742
+ this.ensureHighlightedVisible();
743
+ }
744
+ }
745
+
746
+ private ensureHighlightedVisible(): void {
747
+ if (!this.openState || this.highlightedIndexValue < 0) {
748
+ return;
749
+ }
750
+ const visibleHeight = <f32>Math.max(0.0, this.resolveViewportClampedPanelOuterHeight() - (PANEL_PADDING * 2.0));
751
+ if (visibleHeight <= 0.0) {
752
+ return;
753
+ }
754
+ const rowHeight = this.resolveOptionRowHeight();
755
+ const itemTop = <f32>this.highlightedIndexValue * rowHeight;
756
+ const itemBottom = itemTop + rowHeight;
757
+ let nextOffset = this.popupScrollBox.scrollState.offsetY.value;
758
+ if (itemTop < nextOffset) {
759
+ nextOffset = itemTop;
760
+ } else if (itemBottom > nextOffset + visibleHeight) {
761
+ nextOffset = itemBottom - visibleHeight;
762
+ }
763
+ this.popupScrollBox.setRuntimeScrollOffset(0.0, nextOffset);
764
+ }
765
+
766
+ private handleThemeChanged(): void {
767
+ if (this.disposed) {
768
+ return;
769
+ }
770
+ const theme = activeTheme.value;
771
+ if (!this.popupPanelColorOverridden) {
772
+ this.popupPanelColorValue = theme.contextMenu.panelBackground;
773
+ }
774
+ if (!this.popupPanelBackgroundBlurOverridden) {
775
+ this.popupPanelBackgroundBlurSigmaValue = DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA;
776
+ }
777
+ this.cursor(this.isEnabled ? CursorStyle.Pointer : CursorStyle.Default);
778
+ this.cornerRadius(0.0);
779
+ this.border(0.0, 0x00000000, BorderStyle.Solid);
780
+ this.padding(0.0, 0.0, 0.0, 0.0);
781
+ this.bgColor(0x00000000);
782
+ this.opacity(this.isEnabled ? 1.0 : 0.6);
783
+ this.fieldPresenter.root.flexGrow(1.0);
784
+ this.fieldPresenter.apply(
785
+ theme,
786
+ new DropdownFieldVisualState(
787
+ this.openState,
788
+ this.focusedState,
789
+ this.isEnabled,
790
+ this.pointerPressedState,
791
+ this.selectedIndexValue >= 0 && this.selectedIndexValue < this.itemsValue.length
792
+ ? unchecked(this.itemsValue[this.selectedIndexValue]).label
793
+ : "",
794
+ ),
795
+ );
796
+ this.chevronPresenter.apply(
797
+ theme,
798
+ new DropdownChevronVisualState(
799
+ this.openState,
800
+ this.hoveredState,
801
+ this.isEnabled,
802
+ ),
803
+ );
804
+ this.panelNode
805
+ .padding(PANEL_PADDING, PANEL_PADDING, PANEL_PADDING, PANEL_PADDING)
806
+ .cornerRadius(theme.spacing.sm)
807
+ .bgColor(this.popupPanelColorValue)
808
+ .border(1.0, theme.contextMenu.panelBorderColor, BorderStyle.Solid)
809
+ .backgroundBlur(this.popupPanelBackgroundBlurSigmaValue)
810
+ .dropShadow(
811
+ theme.contextMenu.panelShadowColor,
812
+ 0.0,
813
+ theme.contextMenu.shadowOffsetY,
814
+ theme.contextMenu.shadowBlur,
815
+ theme.contextMenu.shadowSpread,
816
+ );
817
+ this.popupScrollBox.bgColor(0x00000000);
818
+ this.syncOptionVisuals();
819
+ this.syncFocusChrome(theme);
820
+ }
821
+
822
+ private track(disposable: Disposable): void {
823
+ this.disposables.push(disposable);
824
+ }
825
+
826
+ private disposeControl(): void {
827
+ if (this.disposed) {
828
+ return;
829
+ }
830
+ this.disposed = true;
831
+ this.popupPresenter.dispose();
832
+ disposeAll(this.disposables);
833
+ FocusAdornerManager.hideOwner(this);
834
+ }
835
+
836
+ private syncFocusChrome(theme: Theme): void {
837
+ if (this.focusedState && this.isEnabled && keyboardFocusVisible.value) {
838
+ FocusAdornerManager.showStandard(this, theme.spacing.sm);
839
+ return;
840
+ }
841
+ FocusAdornerManager.hideOwner(this);
842
+ }
843
+
844
+ private replaceFieldPresenter(nextFieldPresenter: DropdownFieldPresenter, nextChevronPresenter: DropdownChevronPresenter): void {
845
+ const previousFieldRoot = this.fieldPresenter.root;
846
+ this.fieldPresenter = nextFieldPresenter;
847
+ this.chevronPresenter = nextChevronPresenter;
848
+ nextFieldPresenter.root.flexGrow(1.0);
849
+ nextFieldPresenter.chevronHost.addChildNode(nextChevronPresenter.root);
850
+ const children = new Array<Node>();
851
+ children.push(nextFieldPresenter.root);
852
+ children.push(this.popupRoot);
853
+ this.replaceChildren(children);
854
+ previousFieldRoot.dispose();
855
+ }
856
+ }