@effindomv2/fui-as 0.1.8 → 0.1.10

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effindomv2/fui-as",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "private": false,
5
5
  "license": "AGPL-3.0-only OR LicenseRef-EffinDom-Commercial",
6
6
  "description": "EffinDom v2 AssemblyScript frontend framework SDK and browser harness",
@@ -78,7 +78,7 @@
78
78
  },
79
79
  "dependencies": {
80
80
  "@assemblyscript/loader": "^0.28.17",
81
- "@effindomv2/runtime": "0.1.2"
81
+ "@effindomv2/runtime": "0.1.3"
82
82
  },
83
83
  "devDependencies": {
84
84
  "@as-pect/assembly": "8.1.0",
@@ -19,7 +19,8 @@ import { Theme, activeTheme } from "../core/Theme";
19
19
  import { warn } from "../core/Logger";
20
20
  import { Node } from "../core/Node";
21
21
  import { PersistedInt32Codec, PersistedValueState } from "../core/PersistedState";
22
- import { FlexBox, Portal, ScrollBarVisibility, ScrollBox } from "../nodes";
22
+ import { registerScrollHook } from "../core/ScrollHooks";
23
+ import { FlexBox, Portal, ScrollBarVisibility, ScrollBox, ScrollView } from "../nodes";
23
24
  import { bind2 } from "../core/bind";
24
25
  import { getControlTemplates } from "./ControlTemplateSet";
25
26
  import {
@@ -173,14 +174,13 @@ class DropdownOptionNode extends FlexBox {
173
174
 
174
175
  private syncPresenterLayout(): void {
175
176
  this.height(this.presenter.metrics.height, Unit.Pixel);
176
- this.presenter.root
177
- .width(100.0, Unit.Percent)
178
- .height(100.0, Unit.Percent);
177
+ this.presenter.root.fillSize();
179
178
  }
180
179
  }
181
180
 
182
181
  export class Dropdown extends FlexBox implements GlobalKeyHandler {
183
182
  private static activeInstance: Dropdown | null = null;
183
+ private static scrollHookRegistered: bool = false;
184
184
 
185
185
  private fieldTemplateValue: DropdownFieldTemplate | null = null;
186
186
  private chevronTemplateValue: DropdownChevronTemplate | null = null;
@@ -214,6 +214,7 @@ export class Dropdown extends FlexBox implements GlobalKeyHandler {
214
214
 
215
215
  constructor() {
216
216
  super();
217
+ Dropdown.ensureScrollHook();
217
218
  const fieldPresenter = createFieldPresenter(null);
218
219
  const chevronPresenter = createChevronPresenter(null);
219
220
  const popupRoot = new Portal()
@@ -396,6 +397,24 @@ export class Dropdown extends FlexBox implements GlobalKeyHandler {
396
397
  }
397
398
  }
398
399
 
400
+ static dismissActiveDropdownIfTriggerOutOfViewport(): void {
401
+ const dropdown = Dropdown.activeInstance;
402
+ if (dropdown === null) {
403
+ return;
404
+ }
405
+ if (!dropdown.isTriggerVisibleInViewport()) {
406
+ dropdown.close();
407
+ }
408
+ }
409
+
410
+ private static ensureScrollHook(): void {
411
+ if (Dropdown.scrollHookRegistered) {
412
+ return;
413
+ }
414
+ registerScrollHook((): void => Dropdown.dismissActiveDropdownIfTriggerOutOfViewport());
415
+ Dropdown.scrollHookRegistered = true;
416
+ }
417
+
399
418
  highlightIndex(index: i32): void {
400
419
  if (index < 0 || index >= this.itemsValue.length || this.highlightedIndexValue == index) {
401
420
  if (index < 0 || index >= this.itemsValue.length) {
@@ -605,6 +624,56 @@ export class Dropdown extends FlexBox implements GlobalKeyHandler {
605
624
  return ui.tryGetBounds(this.builtHandle);
606
625
  }
607
626
 
627
+ private findContainingScrollView(): ScrollView | null {
628
+ let current: Node | null = this.parentNode;
629
+ while (current !== null) {
630
+ if (current instanceof ScrollView) {
631
+ return current as ScrollView;
632
+ }
633
+ current = current.parentNode;
634
+ }
635
+ return null;
636
+ }
637
+
638
+ private isTriggerVisibleInViewport(): bool {
639
+ const bounds = this.tryGetViewportBounds();
640
+ if (bounds === null) {
641
+ return true;
642
+ }
643
+ const x = unchecked(bounds[0]);
644
+ const y = unchecked(bounds[1]);
645
+ const width = unchecked(bounds[2]);
646
+ const height = unchecked(bounds[3]);
647
+ if (width <= 0.0 || height <= 0.0) {
648
+ return true;
649
+ }
650
+ const right = x + width;
651
+ const bottom = y + height;
652
+
653
+ const scrollView = this.findContainingScrollView();
654
+ if (scrollView === null) {
655
+ const viewportWidth = ui.getViewportWidth();
656
+ const viewportHeight = ui.getViewportHeight();
657
+ return right > 0.0 && bottom > 0.0 && x < viewportWidth && y < viewportHeight;
658
+ }
659
+
660
+ const scrollViewBounds = ui.tryGetBounds(scrollView.builtHandle);
661
+ if (scrollViewBounds === null) {
662
+ const viewportWidth = ui.getViewportWidth();
663
+ const viewportHeight = ui.getViewportHeight();
664
+ return right > 0.0 && bottom > 0.0 && x < viewportWidth && y < viewportHeight;
665
+ }
666
+
667
+ const svX = unchecked(scrollViewBounds[0]);
668
+ const svY = unchecked(scrollViewBounds[1]);
669
+ const svWidth = unchecked(scrollViewBounds[2]);
670
+ const svHeight = unchecked(scrollViewBounds[3]);
671
+ const svRight = svX + svWidth;
672
+ const svBottom = svY + svHeight;
673
+
674
+ return right > svX && bottom > svY && x < svRight && y < svBottom;
675
+ }
676
+
608
677
  private positionPanel(triggerX: f32, triggerY: f32, triggerWidth: f32, triggerHeight: f32): void {
609
678
  const popupWidth = this.resolvePopupWidth(triggerWidth);
610
679
  const panelHeight = this.resolveViewportClampedPanelOuterHeight();
@@ -44,8 +44,7 @@ class DefaultCheckboxIndicatorPresenter extends CheckboxIndicatorPresenter {
44
44
  .justifyContent(1);
45
45
  super(root, new PressableIndicatorMetrics(20.0, 20.0));
46
46
  const markHost = new FlexBox()
47
- .width(100.0, Unit.Percent)
48
- .height(100.0, Unit.Percent)
47
+ .fillSize()
49
48
  .alignItems(1)
50
49
  .justifyContent(1);
51
50
  const markNode = new Svg();
@@ -34,8 +34,7 @@ class DefaultDropdownChevronPresenter extends DropdownChevronPresenter {
34
34
 
35
35
  constructor() {
36
36
  const root = new FlexBox()
37
- .width(100.0, Unit.Percent)
38
- .height(100.0, Unit.Percent)
37
+ .fillSize()
39
38
  .alignItems(AlignItems.Center)
40
39
  .justifyContent(JustifyContent.Center);
41
40
  const iconNode = new Svg()
@@ -48,8 +47,7 @@ class DefaultDropdownChevronPresenter extends DropdownChevronPresenter {
48
47
 
49
48
  apply(theme: Theme, state: DropdownChevronVisualState): void {
50
49
  this.root
51
- .width(100.0, Unit.Percent)
52
- .height(100.0, Unit.Percent)
50
+ .fillSize()
53
51
  .alignItems(AlignItems.Center)
54
52
  .justifyContent(JustifyContent.Center);
55
53
  this.iconNode
@@ -51,8 +51,7 @@ class DefaultDropdownOptionRowPresenter extends DropdownOptionRowPresenter {
51
51
  .wrapping(false) as Text;
52
52
  labelNode.overflowFade(true, false);
53
53
  const root = new FlexBox()
54
- .width(100.0, Unit.Percent)
55
- .height(100.0, Unit.Percent)
54
+ .fillSize()
56
55
  .alignItems(AlignItems.Center)
57
56
  .child(labelNode);
58
57
  super(root, labelNode, new DropdownOptionRowMetrics(34.0));
@@ -15,6 +15,7 @@ import { DragDropEffects, DragSession, ExternalDropItemInfo, Node } from "./Node
15
15
  import { DragDropManager } from "./DragDropManager";
16
16
  import { ExternalDragEventType, ExternalDropManager } from "./ExternalDropManager";
17
17
  import { isCoarsePointer } from "./Platform";
18
+ import { runScrollHooks } from "./ScrollHooks";
18
19
 
19
20
  export interface GlobalKeyHandler {
20
21
  handleGlobalKeyEvent(eventType: KeyEventType, key: string, modifiers: u32): bool;
@@ -161,6 +162,7 @@ export class EventRouter {
161
162
  viewportHeight: f32,
162
163
  ): void {
163
164
  ToolTipManager.handleScroll();
165
+ runScrollHooks();
164
166
  const node = this.resolveNode(handle);
165
167
  if (node === null) {
166
168
  return;
@@ -2,7 +2,7 @@ import * as ui from "../bindings/ui";
2
2
  import { BorderStyle, Unit } from "./ffi";
3
3
  import { Node } from "./Node";
4
4
  import { activeTheme } from "./Theme";
5
- import { Portal, ScrollView, FlexBox } from "../nodes";
5
+ import { Portal, FlexBox } from "../nodes";
6
6
 
7
7
  const STANDARD_FOCUS_RING_WIDTH: f32 = 2.0;
8
8
  const STANDARD_FOCUS_RING_OUTSET: f32 = 2.0;
@@ -59,7 +59,7 @@ export class FocusAdornerManager {
59
59
  .position(0.0, 0.0)
60
60
  .width(0.0, Unit.Pixel)
61
61
  .height(0.0, Unit.Pixel)
62
- .clipToBounds(true) as Portal;
62
+ .clipToBounds(false) as Portal;
63
63
  this.hostRoot = hostRoot;
64
64
  this.ringNode = ringNode;
65
65
  return hostRoot;
@@ -239,20 +239,6 @@ export class FocusAdornerManager {
239
239
  maxX = <f32>Math.min(maxX, ui.getViewportWidth());
240
240
  maxY = <f32>Math.min(maxY, ui.getViewportHeight());
241
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
242
  const width = maxX - minX;
257
243
  const height = maxY - minY;
258
244
  if (width <= 0.0 || height <= 0.0) {
package/src/core/Node.ts CHANGED
@@ -1343,6 +1343,38 @@ export abstract class Node implements DragGestureHost, Disposable {
1343
1343
  return false;
1344
1344
  }
1345
1345
 
1346
+ _debugNodeId(): string | null {
1347
+ return this.nodeIdValue;
1348
+ }
1349
+
1350
+ _debugTreePath(): string {
1351
+ const segments = new Array<i32>();
1352
+ let current: Node | null = this;
1353
+ while (current !== null) {
1354
+ const parent = current.retainedParent;
1355
+ if (parent === null) {
1356
+ break;
1357
+ }
1358
+ let childIndex = -1;
1359
+ for (let index = 0; index < parent.childNodes.length; ++index) {
1360
+ if (unchecked(parent.childNodes[index]) === current) {
1361
+ childIndex = index;
1362
+ break;
1363
+ }
1364
+ }
1365
+ if (childIndex < 0) {
1366
+ break;
1367
+ }
1368
+ segments.push(childIndex);
1369
+ current = parent;
1370
+ }
1371
+ let path = "root";
1372
+ for (let index = segments.length - 1; index >= 0; --index) {
1373
+ path += "/" + unchecked(segments[index]).toString();
1374
+ }
1375
+ return path;
1376
+ }
1377
+
1346
1378
  protected capturePointer(): void {
1347
1379
  if (!this.hasBuiltHandle()) {
1348
1380
  return;
@@ -0,0 +1,13 @@
1
+ type ScrollHook = () => void;
2
+
3
+ const hooks: Array<ScrollHook> = new Array<ScrollHook>();
4
+
5
+ export function registerScrollHook(hook: ScrollHook): void {
6
+ hooks.push(hook);
7
+ }
8
+
9
+ export function runScrollHooks(): void {
10
+ for (let index = 0; index < hooks.length; ++index) {
11
+ unchecked(hooks[index])();
12
+ }
13
+ }
package/src/core/Theme.ts CHANGED
@@ -38,6 +38,7 @@ export class Colors {
38
38
  readonly scrollbarThumb: u32,
39
39
  readonly dialogBackdrop: u32,
40
40
  readonly dialogShadow: u32,
41
+ readonly panelShadow: u32,
41
42
  readonly focusRing: u32,
42
43
  ) {}
43
44
  }
@@ -231,16 +232,17 @@ export function generateTheme(isDark: bool, accentColor: u32 = DEFAULT_ACCENT_CO
231
232
  : mixColor(accent, surface, 0.40);
232
233
  const dialogBackdrop = isDark ? rgba(0x00, 0x00, 0x00, 0x24) : rgba(0x00, 0x00, 0x00, 0x18);
233
234
  const dialogShadow = isDark ? rgba(0x00, 0x00, 0x00, 0xd8) : rgba(0x00, 0x00, 0x00, 0x88);
235
+ const panelShadow = withAlpha(dialogShadow, <u32>Math.round(<f32>colorAlpha(dialogShadow) * 0.30));
234
236
  const focusRing = accent;
235
237
  const contextMenuPanelBackground = isDark ? rgba(0x18, 0x1d, 0x26, 0xd8) : rgba(0xff, 0xff, 0xff, 0xdc);
236
238
  const contextMenuPanelBorderColor = isDark ? rgba(0xff, 0xff, 0xff, 0x10) : rgba(0x0f, 0x17, 0x2a, 0x14);
237
- const contextMenuPanelShadowColor = isDark ? rgba(0x00, 0x00, 0x00, 0xb0) : rgba(0x0f, 0x17, 0x2a, 0x24);
239
+ const contextMenuPanelShadowColor = panelShadow;
238
240
  const contextMenuItemBackground = rgba(0x00, 0x00, 0x00, 0x00);
239
241
  const contextMenuItemHover = isDark ? rgba(0xff, 0xff, 0xff, 0x0c) : rgba(0x0f, 0x17, 0x2a, 0x08);
240
242
  const contextMenuSeparatorColor = isDark ? rgba(0xff, 0xff, 0xff, 0x10) : rgba(0x0f, 0x17, 0x2a, 0x12);
241
243
  const toolTipPanelBackground = isDark ? rgba(0x11, 0x17, 0x20, 0xf0) : rgba(0xff, 0xff, 0xff, 0xf8);
242
244
  const toolTipPanelBorderColor = isDark ? rgba(0xff, 0xff, 0xff, 0x12) : rgba(0x0f, 0x17, 0x2a, 0x12);
243
- const toolTipPanelShadowColor = isDark ? rgba(0x00, 0x00, 0x00, 0xb8) : rgba(0x0f, 0x17, 0x2a, 0x22);
245
+ const toolTipPanelShadowColor = panelShadow;
244
246
 
245
247
  return new Theme(
246
248
  new Colors(
@@ -257,6 +259,7 @@ export function generateTheme(isDark: bool, accentColor: u32 = DEFAULT_ACCENT_CO
257
259
  scrollbarThumb,
258
260
  dialogBackdrop,
259
261
  dialogShadow,
262
+ panelShadow,
260
263
  focusRing,
261
264
  ),
262
265
  DEFAULT_SPACING,
@@ -623,6 +623,8 @@ export class FlexBox extends Node {
623
623
  let inFlowChildCount = 0;
624
624
  let fullMainAxisPercentChildCount = 0;
625
625
  let mainAxisPercentTotal: f32 = 0.0;
626
+ let firstFullMainAxisPercentChild: Node | null = null;
627
+ let firstFullMainAxisPercentChildIndex: i32 = -1;
626
628
 
627
629
  for (let i = 0; i < this.childNodes.length; ++i) {
628
630
  const child = unchecked(this.childNodes[i]);
@@ -639,6 +641,10 @@ export class FlexBox extends Node {
639
641
  mainAxisPercentTotal += percent;
640
642
  if (percent >= MAIN_AXIS_PERCENT_FULL_THRESHOLD) {
641
643
  fullMainAxisPercentChildCount += 1;
644
+ if (firstFullMainAxisPercentChild === null) {
645
+ firstFullMainAxisPercentChild = child;
646
+ firstFullMainAxisPercentChildIndex = i;
647
+ }
642
648
  }
643
649
  }
644
650
 
@@ -649,7 +655,11 @@ export class FlexBox extends Node {
649
655
  if (fullMainAxisPercentChildCount > 0) {
650
656
  if ((this.layoutWarningMask & LAYOUT_WARNING_FULL_MAIN_AXIS_PERCENT) == 0) {
651
657
  this.layoutWarningMask |= LAYOUT_WARNING_FULL_MAIN_AXIS_PERCENT;
652
- warn("Layout", this.buildFullMainAxisPercentWarning(isRow));
658
+ warn("Layout", this.buildFullMainAxisPercentWarning(
659
+ isRow,
660
+ firstFullMainAxisPercentChild,
661
+ firstFullMainAxisPercentChildIndex,
662
+ ));
653
663
  }
654
664
  return;
655
665
  }
@@ -721,18 +731,56 @@ export class FlexBox extends Node {
721
731
  this.cancelBackgroundColorTransition();
722
732
  }
723
733
 
724
- private buildFullMainAxisPercentWarning(isRow: bool): string {
734
+ private buildFullMainAxisPercentWarning(isRow: bool, child: Node | null, childIndex: i32): string {
735
+ let message = "";
725
736
  if (isRow) {
726
- return "A row container has an in-flow child using width(100.0, Unit.Percent) alongside siblings. Unit.Percent is literal parent-relative sizing, not flex sharing. Use fillWidth() when the child should take remaining row space.";
737
+ message = "A row container has an in-flow child using width(100.0, Unit.Percent) alongside siblings. Unit.Percent is literal parent-relative sizing, not flex sharing. Use fillWidth() when the child should take remaining row space.";
738
+ } else {
739
+ message = "A column container has an in-flow child using height(100.0, Unit.Percent) alongside siblings. Unit.Percent is literal parent-relative sizing, not flex sharing. Use fillHeight() when the child should take remaining column space.";
727
740
  }
728
- return "A column container has an in-flow child using height(100.0, Unit.Percent) alongside siblings. Unit.Percent is literal parent-relative sizing, not flex sharing. Use fillHeight() when the child should take remaining column space.";
741
+ return message + this.buildWarningContextSuffix(child, childIndex);
729
742
  }
730
743
 
731
744
  private buildMainAxisPercentOverflowWarning(isRow: bool): string {
745
+ let message = "";
732
746
  if (isRow) {
733
- return "A row container has in-flow children whose explicit width percentages exceed 100% in total. Unit.Percent is literal parent-relative sizing, not flex sharing. Use fillWidth() for the child that should expand, or reduce the percentages so they fit.";
747
+ message = "A row container has in-flow children whose explicit width percentages exceed 100% in total. Unit.Percent is literal parent-relative sizing, not flex sharing. Use fillWidth() for the child that should expand, or reduce the percentages so they fit.";
748
+ } else {
749
+ message = "A column container has in-flow children whose explicit height percentages exceed 100% in total. Unit.Percent is literal parent-relative sizing, not flex sharing. Use fillHeight() for the child that should expand, or reduce the percentages so they fit.";
750
+ }
751
+ return message + this.buildWarningContextSuffix(null, -1);
752
+ }
753
+
754
+ private buildWarningContextSuffix(child: Node | null, childIndex: i32): string {
755
+ const containerNodeId = this._debugNodeId();
756
+ const childNodeId = child !== null ? child._debugNodeId() : null;
757
+ const containerPath = this._debugTreePath();
758
+ const childPath = child !== null ? child._debugTreePath() : null;
759
+ const hasContainerNodeId = containerNodeId !== null && containerNodeId.length > 0;
760
+ const hasChildNodeId = childNodeId !== null && childNodeId.length > 0;
761
+ const hasChildPath = childPath !== null;
762
+ const hasChildIndex = childIndex >= 0;
763
+ let suffix = " [containerPath=" + containerPath;
764
+ let needsSeparator = true;
765
+ if (hasChildPath) {
766
+ suffix += ", childPath=" + changetype<string>(childPath);
767
+ }
768
+ if (hasContainerNodeId) {
769
+ suffix += ", containerNodeId=" + changetype<string>(containerNodeId);
770
+ needsSeparator = true;
771
+ }
772
+ if (hasChildNodeId) {
773
+ suffix += ", childNodeId=" + changetype<string>(childNodeId);
774
+ needsSeparator = true;
775
+ }
776
+ if (hasChildIndex) {
777
+ if (needsSeparator) {
778
+ suffix += ", ";
779
+ }
780
+ suffix += "childIndex=" + childIndex.toString();
734
781
  }
735
- return "A column container has in-flow children whose explicit height percentages exceed 100% in total. Unit.Percent is literal parent-relative sizing, not flex sharing. Use fillHeight() for the child that should expand, or reduce the percentages so they fit.";
782
+ suffix += "]";
783
+ return suffix;
736
784
  }
737
785
 
738
786
  protected applyVisualStyle(): void {
@@ -99,9 +99,7 @@ export class VirtualList extends FlexBox {
99
99
  .child(topSpacerValue);
100
100
 
101
101
  for (let index = 0; index < poolSizeValue; ++index) {
102
- const container = new FlexBox()
103
- .width(FULL_SIZE, Unit.Percent)
104
- .height(FULL_SIZE, Unit.Percent);
102
+ const container = new FlexBox().fillSize();
105
103
  const rowArea = new SelectionArea()
106
104
  .width(FULL_SIZE, Unit.Percent)
107
105
  .height(0.0, Unit.Pixel)
@@ -117,8 +115,7 @@ export class VirtualList extends FlexBox {
117
115
  .scrollEnabledY(true)
118
116
  .scrollOffset(scrollStateValue.offsetX.value, scrollStateValue.offsetY.value)
119
117
  .scrollContentSize(-1.0, <f32>totalItemsValue * itemHeightValue)
120
- .width(FULL_SIZE, Unit.Percent)
121
- .height(FULL_SIZE, Unit.Percent)
118
+ .fillSize()
122
119
  .child(contentValue) as ScrollBox;
123
120
  scrollStateValue.contentHeight.value = <f32>totalItemsValue * itemHeightValue;
124
121
 
@@ -132,8 +129,7 @@ export class VirtualList extends FlexBox {
132
129
 
133
130
  this.flexDirection(FlexDirection.Column)
134
131
  .child(this.scrollBoxValue)
135
- .width(FULL_SIZE, Unit.Percent)
136
- .height(FULL_SIZE, Unit.Percent);
132
+ .fillSize();
137
133
  this.attachListeners();
138
134
  }
139
135