@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,263 @@
1
+ import * as ui from "../bindings/ui";
2
+ import { BorderStyle, Unit } from "./ffi";
3
+ import { Node } from "./Node";
4
+ import { activeTheme } from "./Theme";
5
+ import { Portal, ScrollView, FlexBox } from "../nodes";
6
+
7
+ const STANDARD_FOCUS_RING_WIDTH: f32 = 2.0;
8
+ const STANDARD_FOCUS_RING_OUTSET: f32 = 2.0;
9
+
10
+ class FocusAdornerStyle {
11
+ constructor(
12
+ readonly topLeftRadius: f32,
13
+ readonly topRightRadius: f32,
14
+ readonly bottomRightRadius: f32,
15
+ readonly bottomLeftRadius: f32,
16
+ ) {}
17
+ }
18
+
19
+ class FocusAdornerRect {
20
+ constructor(
21
+ readonly x: f32,
22
+ readonly y: f32,
23
+ readonly width: f32,
24
+ readonly height: f32,
25
+ ) {}
26
+ }
27
+
28
+ export class FocusAdornerManager {
29
+ private static hostRoot: Portal | null = null;
30
+ private static ringNode: FlexBox | null = null;
31
+ private static activeOwner: Node | null = null;
32
+ private static activeStyle: FocusAdornerStyle | null = null;
33
+ private static attached: bool = false;
34
+ private static lastHostX: f32 = NaN;
35
+ private static lastHostY: f32 = NaN;
36
+ private static lastHostWidth: f32 = NaN;
37
+ private static lastHostHeight: f32 = NaN;
38
+ private static lastRingX: f32 = NaN;
39
+ private static lastRingY: f32 = NaN;
40
+ private static lastRingWidth: f32 = NaN;
41
+ private static lastRingHeight: f32 = NaN;
42
+ private static lastColor: u32 = 0;
43
+ private static lastTopLeftRadius: f32 = NaN;
44
+ private static lastTopRightRadius: f32 = NaN;
45
+ private static lastBottomRightRadius: f32 = NaN;
46
+ private static lastBottomLeftRadius: f32 = NaN;
47
+
48
+ static createDefaultHost(): Portal {
49
+ const existingHost = this.hostRoot;
50
+ if (existingHost !== null) {
51
+ return existingHost;
52
+ }
53
+ const ringNode = new FlexBox()
54
+ .positionAbsolute()
55
+ .bgColor(0x00000000)
56
+ .border(STANDARD_FOCUS_RING_WIDTH, 0x00000000, BorderStyle.Solid);
57
+ const hostRoot = new Portal()
58
+ .positionAbsolute()
59
+ .position(0.0, 0.0)
60
+ .width(0.0, Unit.Pixel)
61
+ .height(0.0, Unit.Pixel)
62
+ .clipToBounds(true) as Portal;
63
+ this.hostRoot = hostRoot;
64
+ this.ringNode = ringNode;
65
+ return hostRoot;
66
+ }
67
+
68
+ static clear(): void {
69
+ this.hide();
70
+ this.activeOwner = null;
71
+ this.activeStyle = null;
72
+ }
73
+
74
+ static showStandard(owner: Node, cornerRadius: f32): void {
75
+ this.showStandardCorners(owner, cornerRadius, cornerRadius, cornerRadius, cornerRadius);
76
+ }
77
+
78
+ static showStandardCorners(owner: Node, tl: f32, tr: f32, br: f32, bl: f32): void {
79
+ this.activeOwner = owner;
80
+ this.activeStyle = new FocusAdornerStyle(
81
+ tl + STANDARD_FOCUS_RING_OUTSET,
82
+ tr + STANDARD_FOCUS_RING_OUTSET,
83
+ br + STANDARD_FOCUS_RING_OUTSET,
84
+ bl + STANDARD_FOCUS_RING_OUTSET,
85
+ );
86
+ this.sync();
87
+ }
88
+
89
+ static hideOwner(owner: Node): void {
90
+ if (this.activeOwner !== owner) {
91
+ return;
92
+ }
93
+ this.activeOwner = null;
94
+ this.activeStyle = null;
95
+ this.hide();
96
+ }
97
+
98
+ static handleOwnerDestroyed(owner: Node): void {
99
+ this.hideOwner(owner);
100
+ }
101
+
102
+ static refreshAfterCommit(): bool {
103
+ return this.sync();
104
+ }
105
+
106
+ private static sync(): bool {
107
+ const owner = this.activeOwner;
108
+ const style = this.activeStyle;
109
+ if (owner === null || style === null) {
110
+ return this.hide();
111
+ }
112
+ const hostRoot = this.hostRoot;
113
+ const ringNode = this.ringNode;
114
+ if (hostRoot === null || ringNode === null) {
115
+ return false;
116
+ }
117
+ if (owner.builtHandle == 0 || hostRoot.builtHandle == 0) {
118
+ return this.hide();
119
+ }
120
+ const ringRect = this.resolveRingRect(owner);
121
+ if (ringRect === null) {
122
+ return this.hide();
123
+ }
124
+ const visibleRect = this.resolveVisibleRect(owner, ringRect);
125
+ if (visibleRect === null) {
126
+ return this.hide();
127
+ }
128
+ const color = activeTheme.value.colors.focusRing;
129
+ let changed = false;
130
+ if (!this.attached) {
131
+ hostRoot.addChildNode(ringNode);
132
+ this.attached = true;
133
+ changed = true;
134
+ }
135
+ if (
136
+ visibleRect.x != this.lastHostX ||
137
+ visibleRect.y != this.lastHostY ||
138
+ visibleRect.width != this.lastHostWidth ||
139
+ visibleRect.height != this.lastHostHeight ||
140
+ (ringRect.x - visibleRect.x) != this.lastRingX ||
141
+ (ringRect.y - visibleRect.y) != this.lastRingY ||
142
+ ringRect.width != this.lastRingWidth ||
143
+ ringRect.height != this.lastRingHeight
144
+ ) {
145
+ const relativeRingX = ringRect.x - visibleRect.x;
146
+ const relativeRingY = ringRect.y - visibleRect.y;
147
+ hostRoot.position(visibleRect.x, visibleRect.y);
148
+ hostRoot.width(visibleRect.width, Unit.Pixel);
149
+ hostRoot.height(visibleRect.height, Unit.Pixel);
150
+ ringNode.position(relativeRingX, relativeRingY);
151
+ ringNode.width(ringRect.width, Unit.Pixel);
152
+ ringNode.height(ringRect.height, Unit.Pixel);
153
+ this.lastHostX = visibleRect.x;
154
+ this.lastHostY = visibleRect.y;
155
+ this.lastHostWidth = visibleRect.width;
156
+ this.lastHostHeight = visibleRect.height;
157
+ this.lastRingX = relativeRingX;
158
+ this.lastRingY = relativeRingY;
159
+ this.lastRingWidth = ringRect.width;
160
+ this.lastRingHeight = ringRect.height;
161
+ changed = true;
162
+ }
163
+ if (
164
+ color != this.lastColor ||
165
+ style.topLeftRadius != this.lastTopLeftRadius ||
166
+ style.topRightRadius != this.lastTopRightRadius ||
167
+ style.bottomRightRadius != this.lastBottomRightRadius ||
168
+ style.bottomLeftRadius != this.lastBottomLeftRadius
169
+ ) {
170
+ ringNode.corners(style.topLeftRadius, style.topRightRadius, style.bottomRightRadius, style.bottomLeftRadius);
171
+ ringNode.border(STANDARD_FOCUS_RING_WIDTH, color, BorderStyle.Solid);
172
+ this.lastColor = color;
173
+ this.lastTopLeftRadius = style.topLeftRadius;
174
+ this.lastTopRightRadius = style.topRightRadius;
175
+ this.lastBottomRightRadius = style.bottomRightRadius;
176
+ this.lastBottomLeftRadius = style.bottomLeftRadius;
177
+ changed = true;
178
+ }
179
+ return changed;
180
+ }
181
+
182
+ private static hide(): bool {
183
+ const hostRoot = this.hostRoot;
184
+ const ringNode = this.ringNode;
185
+ if (!this.attached || hostRoot === null || ringNode === null) {
186
+ this.attached = false;
187
+ this.resetCachedGeometry();
188
+ return false;
189
+ }
190
+ hostRoot.removeChildNode(ringNode);
191
+ hostRoot.position(0.0, 0.0);
192
+ hostRoot.width(0.0, Unit.Pixel);
193
+ hostRoot.height(0.0, Unit.Pixel);
194
+ this.attached = false;
195
+ this.resetCachedGeometry();
196
+ return true;
197
+ }
198
+
199
+ private static resetCachedGeometry(): void {
200
+ this.lastHostX = NaN;
201
+ this.lastHostY = NaN;
202
+ this.lastHostWidth = NaN;
203
+ this.lastHostHeight = NaN;
204
+ this.lastRingX = NaN;
205
+ this.lastRingY = NaN;
206
+ this.lastRingWidth = NaN;
207
+ this.lastRingHeight = NaN;
208
+ this.lastColor = 0;
209
+ this.lastTopLeftRadius = NaN;
210
+ this.lastTopRightRadius = NaN;
211
+ this.lastBottomRightRadius = NaN;
212
+ this.lastBottomLeftRadius = NaN;
213
+ }
214
+
215
+ private static resolveRingRect(owner: Node): FocusAdornerRect | null {
216
+ if (owner.builtHandle == 0) {
217
+ return null;
218
+ }
219
+ const bounds = ui.tryGetBounds(owner.builtHandle);
220
+ if (bounds === null) {
221
+ return null;
222
+ }
223
+ return new FocusAdornerRect(
224
+ unchecked(bounds[0]) - STANDARD_FOCUS_RING_OUTSET,
225
+ unchecked(bounds[1]) - STANDARD_FOCUS_RING_OUTSET,
226
+ unchecked(bounds[2]) + (STANDARD_FOCUS_RING_OUTSET * 2.0),
227
+ unchecked(bounds[3]) + (STANDARD_FOCUS_RING_OUTSET * 2.0),
228
+ );
229
+ }
230
+
231
+ private static resolveVisibleRect(owner: Node, ringRect: FocusAdornerRect): FocusAdornerRect | null {
232
+ let minX = ringRect.x;
233
+ let minY = ringRect.y;
234
+ let maxX = ringRect.x + ringRect.width;
235
+ let maxY = ringRect.y + ringRect.height;
236
+
237
+ minX = <f32>Math.max(minX, 0.0);
238
+ minY = <f32>Math.max(minY, 0.0);
239
+ maxX = <f32>Math.min(maxX, ui.getViewportWidth());
240
+ maxY = <f32>Math.min(maxY, ui.getViewportHeight());
241
+
242
+ let ancestor = owner.parentNode;
243
+ while (ancestor !== null) {
244
+ if (ancestor instanceof ScrollView && ancestor.builtHandle != 0) {
245
+ const viewportBounds = ui.tryGetBounds(ancestor.builtHandle);
246
+ if (viewportBounds !== null) {
247
+ minX = <f32>Math.max(minX, unchecked(viewportBounds[0]));
248
+ minY = <f32>Math.max(minY, unchecked(viewportBounds[1]));
249
+ maxX = <f32>Math.min(maxX, unchecked(viewportBounds[0]) + unchecked(viewportBounds[2]));
250
+ maxY = <f32>Math.min(maxY, unchecked(viewportBounds[1]) + unchecked(viewportBounds[3]));
251
+ }
252
+ }
253
+ ancestor = ancestor.parentNode;
254
+ }
255
+
256
+ const width = maxX - minX;
257
+ const height = maxY - minY;
258
+ if (width <= 0.0 || height <= 0.0) {
259
+ return null;
260
+ }
261
+ return new FocusAdornerRect(minX, minY, width, height);
262
+ }
263
+ }
@@ -0,0 +1,36 @@
1
+ import { Signal } from "./Signal";
2
+ import { KeyEventType, KeyModifier, PointerEventType } from "./ffi";
3
+
4
+ export const keyboardFocusVisible = new Signal<bool>(true);
5
+
6
+ function isModifierKey(key: string): bool {
7
+ return key == "Shift" || key == "Control" || key == "Alt" || key == "Meta";
8
+ }
9
+
10
+ export function showKeyboardFocusForPointerEvent(eventType: PointerEventType): void {
11
+ if (eventType == PointerEventType.Down) {
12
+ keyboardFocusVisible.value = false;
13
+ }
14
+ }
15
+
16
+ function hasNonShiftModifier(modifiers: u32): bool {
17
+ return (modifiers & (KeyModifier.Ctrl | KeyModifier.Alt | KeyModifier.Meta)) != 0;
18
+ }
19
+
20
+ export function showKeyboardFocusForKeyEvent(eventType: KeyEventType, key: string, modifiers: u32 = 0): void {
21
+ if (eventType != KeyEventType.Down || isModifierKey(key)) {
22
+ return;
23
+ }
24
+ if (hasNonShiftModifier(modifiers)) {
25
+ return;
26
+ }
27
+ if (key == "Escape") {
28
+ keyboardFocusVisible.value = false;
29
+ return;
30
+ }
31
+ keyboardFocusVisible.value = true;
32
+ }
33
+
34
+ export function resetKeyboardFocusVisibility(): void {
35
+ keyboardFocusVisible.value = true;
36
+ }
@@ -0,0 +1,28 @@
1
+ import * as ui from "../bindings/ui";
2
+
3
+ let needsCommit: bool = false;
4
+ let flushScheduled: bool = false;
5
+
6
+ export function markNeedsCommit(): void {
7
+ needsCommit = true;
8
+ if (flushScheduled) {
9
+ return;
10
+ }
11
+ flushScheduled = true;
12
+ ui.requestRender();
13
+ }
14
+
15
+ export function flushCommit(): bool {
16
+ flushScheduled = false;
17
+ if (!needsCommit) {
18
+ return false;
19
+ }
20
+ needsCommit = false;
21
+ ui.commitFrame();
22
+ return true;
23
+ }
24
+
25
+ export function resetCommitState(): void {
26
+ needsCommit = false;
27
+ flushScheduled = false;
28
+ }
@@ -0,0 +1,161 @@
1
+ import { EventRouter } from "./EventRouter";
2
+ import {
3
+ getKeyboardScrollFallbackCandidates,
4
+ getKeyboardScrollSelectedCandidate,
5
+ } from "./KeyboardScrollTracker";
6
+ import { Node } from "./Node";
7
+ import { ScrollView, Text } from "../nodes";
8
+
9
+ const KEYBOARD_SCROLL_LINE_STEP: f32 = 40.0;
10
+ const KEYBOARD_SCROLL_PAGE_OVERLAP: f32 = 40.0;
11
+ const KEYBOARD_SCROLL_TOLERANCE: f32 = 0.5;
12
+
13
+ function isKeyboardScrollKey(key: string): bool {
14
+ return key == "ArrowLeft" ||
15
+ key == "ArrowRight" ||
16
+ key == "ArrowUp" ||
17
+ key == "ArrowDown" ||
18
+ key == "PageUp" ||
19
+ key == "PageDown" ||
20
+ key == "Home" ||
21
+ key == "End";
22
+ }
23
+
24
+ function isHorizontalKeyboardScrollKey(key: string): bool {
25
+ return key == "ArrowLeft" || key == "ArrowRight";
26
+ }
27
+
28
+ function isVerticalKeyboardScrollKey(key: string): bool {
29
+ return key == "ArrowUp" ||
30
+ key == "ArrowDown" ||
31
+ key == "PageUp" ||
32
+ key == "PageDown" ||
33
+ key == "Home" ||
34
+ key == "End";
35
+ }
36
+
37
+ function shouldSkipTextOwnedFallback(node: Text, key: string): bool {
38
+ if (key == "Home" || key == "End") {
39
+ return true;
40
+ }
41
+ return node.isEditableText &&
42
+ (key == "ArrowLeft" || key == "ArrowRight" || key == "ArrowUp" || key == "ArrowDown");
43
+ }
44
+
45
+ function pageStep(viewportHeight: f32): f32 {
46
+ if (viewportHeight <= 0.0) {
47
+ return 0.0;
48
+ }
49
+ if (viewportHeight > KEYBOARD_SCROLL_PAGE_OVERLAP) {
50
+ return viewportHeight - KEYBOARD_SCROLL_PAGE_OVERLAP;
51
+ }
52
+ return viewportHeight * 0.875;
53
+ }
54
+
55
+ function clamp(value: f32, minValue: f32, maxValue: f32): f32 {
56
+ if (value < minValue) {
57
+ return minValue;
58
+ }
59
+ if (value > maxValue) {
60
+ return maxValue;
61
+ }
62
+ return value;
63
+ }
64
+
65
+ function tryScrollViewport(view: ScrollView, key: string): bool {
66
+ const state = view.scrollState;
67
+ if (isHorizontalKeyboardScrollKey(key)) {
68
+ if (!view.isHorizontalScrollEnabled) {
69
+ return false;
70
+ }
71
+ const viewportWidth = state.viewportWidth.value;
72
+ const maxOffsetX = <f32>Math.max(0.0, state.contentWidth.value - viewportWidth);
73
+ if (maxOffsetX <= KEYBOARD_SCROLL_TOLERANCE) {
74
+ return false;
75
+ }
76
+ const currentOffsetX = clamp(state.offsetX.value, 0.0, maxOffsetX);
77
+ let nextOffsetX = currentOffsetX;
78
+ if (key == "ArrowLeft") {
79
+ nextOffsetX = currentOffsetX - KEYBOARD_SCROLL_LINE_STEP;
80
+ } else if (key == "ArrowRight") {
81
+ nextOffsetX = currentOffsetX + KEYBOARD_SCROLL_LINE_STEP;
82
+ } else {
83
+ return false;
84
+ }
85
+ nextOffsetX = clamp(nextOffsetX, 0.0, maxOffsetX);
86
+ if (Math.abs(nextOffsetX - currentOffsetX) <= KEYBOARD_SCROLL_TOLERANCE) {
87
+ return false;
88
+ }
89
+ view.scrollOffset(nextOffsetX, state.offsetY.value);
90
+ return true;
91
+ }
92
+
93
+ if (!isVerticalKeyboardScrollKey(key) || !view.isVerticalScrollEnabled) {
94
+ return false;
95
+ }
96
+ const viewportHeight = state.viewportHeight.value;
97
+ const maxOffsetY = <f32>Math.max(0.0, state.contentHeight.value - viewportHeight);
98
+ if (maxOffsetY <= KEYBOARD_SCROLL_TOLERANCE) {
99
+ return false;
100
+ }
101
+
102
+ const currentOffsetY = clamp(state.offsetY.value, 0.0, maxOffsetY);
103
+ let nextOffsetY = currentOffsetY;
104
+ if (key == "ArrowUp") {
105
+ nextOffsetY = currentOffsetY - KEYBOARD_SCROLL_LINE_STEP;
106
+ } else if (key == "ArrowDown") {
107
+ nextOffsetY = currentOffsetY + KEYBOARD_SCROLL_LINE_STEP;
108
+ } else if (key == "PageUp") {
109
+ nextOffsetY = currentOffsetY - pageStep(viewportHeight);
110
+ } else if (key == "PageDown") {
111
+ nextOffsetY = currentOffsetY + pageStep(viewportHeight);
112
+ } else if (key == "Home") {
113
+ nextOffsetY = 0.0;
114
+ } else if (key == "End") {
115
+ nextOffsetY = maxOffsetY;
116
+ } else {
117
+ return false;
118
+ }
119
+
120
+ nextOffsetY = clamp(nextOffsetY, 0.0, maxOffsetY);
121
+ if (Math.abs(nextOffsetY - currentOffsetY) <= KEYBOARD_SCROLL_TOLERANCE) {
122
+ return false;
123
+ }
124
+
125
+ view.scrollOffset(state.offsetX.value, nextOffsetY);
126
+ return true;
127
+ }
128
+
129
+ export function handleKeyboardScrollFallback(key: string, modifiers: u32): bool {
130
+ if (modifiers != 0 || !isKeyboardScrollKey(key)) {
131
+ return false;
132
+ }
133
+
134
+ const focusedNode = EventRouter.getFocusedNode();
135
+ if (focusedNode !== null) {
136
+ if (focusedNode instanceof Text && shouldSkipTextOwnedFallback(changetype<Text>(focusedNode), key)) {
137
+ return false;
138
+ }
139
+
140
+ let current: Node | null = focusedNode;
141
+ while (current !== null) {
142
+ if (current instanceof ScrollView && tryScrollViewport(changetype<ScrollView>(current), key)) {
143
+ return true;
144
+ }
145
+ current = current.parentNode;
146
+ }
147
+ }
148
+
149
+ const selectedCandidate = getKeyboardScrollSelectedCandidate();
150
+ if (selectedCandidate !== null && tryScrollViewport(selectedCandidate, key)) {
151
+ return true;
152
+ }
153
+
154
+ const fallbackCandidates = getKeyboardScrollFallbackCandidates();
155
+ for (let index = 0; index < fallbackCandidates.length; ++index) {
156
+ if (tryScrollViewport(unchecked(fallbackCandidates[index]), key)) {
157
+ return true;
158
+ }
159
+ }
160
+ return false;
161
+ }