From ce9dabae659c2394234bfe57f871518b823febd0 Mon Sep 17 00:00:00 2001 From: Zyie <24736175+Zyie@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:41:45 +0100 Subject: [PATCH 1/3] fix: mixing layout and non layout children Improves the yoga index calculation by accounting for non-layout children, ensuring accurate positioning of elements. --- docs/docs/guides/styles/overflow-and-scrolling.md | 2 +- src/core/utils/sort-children.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/docs/guides/styles/overflow-and-scrolling.md b/docs/docs/guides/styles/overflow-and-scrolling.md index f27d609e..a149d29e 100644 --- a/docs/docs/guides/styles/overflow-and-scrolling.md +++ b/docs/docs/guides/styles/overflow-and-scrolling.md @@ -63,4 +63,4 @@ const container = new LayoutContainer({ }); ``` -See the [Trackpad Customization](../core/components.md##trackpad-customization) section for more details on available options. \ No newline at end of file +See the [Trackpad Customization](../core/components.md#trackpad-customization) section for more details on available options. \ No newline at end of file diff --git a/src/core/utils/sort-children.ts b/src/core/utils/sort-children.ts index 032514a2..8b2a6c1e 100644 --- a/src/core/utils/sort-children.ts +++ b/src/core/utils/sort-children.ts @@ -15,7 +15,16 @@ export function onChildAdded(layout: Layout, pixiParent: Container) { if (!parentLayout && (pixiParent as OverflowContainer).isOverflowContainer) { parentLayout = pixiParent.parent?.layout; // update yogaIndex + // this is inaccurate when a child earlier in the list is not in layout yogaIndex = pixiParent.children.indexOf(layout.target); + // substract non layout children number + for (let i = 0; i < yogaIndex; i++) { + const element = pixiParent.children[i]!; + + if (!element.layout || !element.visible) { + yogaIndex--; + } + } pixiParent = pixiParent.parent!; } From ce9827c2b5bb1fd4961de4dcbfbfc81afed67d3f Mon Sep 17 00:00:00 2001 From: Zyie <24736175+Zyie@users.noreply.github.com> Date: Tue, 2 Sep 2025 09:39:58 +0100 Subject: [PATCH 2/3] better sort-child implementation --- src/core/utils/sort-children.ts | 106 +++++++++++++++++++------------- 1 file changed, 62 insertions(+), 44 deletions(-) diff --git a/src/core/utils/sort-children.ts b/src/core/utils/sort-children.ts index 8b2a6c1e..e0faf5bb 100644 --- a/src/core/utils/sort-children.ts +++ b/src/core/utils/sort-children.ts @@ -4,73 +4,91 @@ import { type Layout } from '../Layout'; /** * Sorts the children of the layout based on their order in the parent container - * This is necessary because not all children are part of the layout and we need to - * make sure that the Yoga children are in the correct order - * @param layout - The layout to sort the children for + * syncing the Yoga tree with the Pixi display list. */ export function onChildAdded(layout: Layout, pixiParent: Container) { let parentLayout = pixiParent.layout; - let yogaIndex = -1; + let overflowContainer: Container | undefined; + // If inside an overflow container, actual yoga parent is the overflow's parent layout. if (!parentLayout && (pixiParent as OverflowContainer).isOverflowContainer) { - parentLayout = pixiParent.parent?.layout; - // update yogaIndex - // this is inaccurate when a child earlier in the list is not in layout - yogaIndex = pixiParent.children.indexOf(layout.target); - // substract non layout children number - for (let i = 0; i < yogaIndex; i++) { - const element = pixiParent.children[i]!; - - if (!element.layout || !element.visible) { - yogaIndex--; - } - } - pixiParent = pixiParent.parent!; + overflowContainer = pixiParent; + parentLayout = overflowContainer.parent?.layout; + pixiParent = overflowContainer.parent!; } - if (parentLayout) { - const yogaParent = layout.yoga.getParent(); + if (!parentLayout) { + return; + } - if (yogaParent) { - yogaParent!.removeChild(layout.yoga); - } + // Detach from previous yoga parent if any + const yogaParent = layout.yoga.getParent(); - // If the child is the last one, we can just append it - if (pixiParent.children.indexOf(layout.target) === pixiParent.children.length - 1 && yogaIndex === -1) { - parentLayout.yoga.insertChild(layout.yoga, parentLayout.yoga.getChildCount()); + if (yogaParent) { + yogaParent.removeChild(layout.yoga); + } - return; - } + const yogaIndex = computeYogaInsertionIndex(layout, pixiParent, overflowContainer); - // Find the corresponding Yoga index - for (let i = 0; i < pixiParent.children.length; i++) { - const child = pixiParent.children[i]!; + if (yogaIndex === -1) { + return; + } - if (child.layout && child.visible) { - yogaIndex++; + // Fast append path + if (yogaIndex === parentLayout.yoga.getChildCount()) { + parentLayout.yoga.insertChild(layout.yoga, yogaIndex); + + return; + } + + parentLayout.yoga.insertChild(layout.yoga, yogaIndex); +} + +/** + * Computes the Yoga insertion index with a single pass over the logical (flattened if needed) sibling list. + * @param layout - layout being inserted + * @param parent - the real parent container whose layout we insert into + * @param overflow - optional overflow container that actually contains the target + */ +function computeYogaInsertionIndex(layout: Layout, parent: Container, overflow?: Container): number { + const target = layout.target; + let index = 0; + + if (overflow) { + // Iterate real parent children; when reaching overflow, iterate its children inline. + for (const child of parent.children) { + if (child === overflow) { + for (const inner of overflow.children) { + if (!inner.layout || !inner.visible) continue; + if (inner === target) return index; + index++; + } + + return -1; // target not found inside overflow } - if (child === layout.target) { - break; + if (child.layout && child.visible) { + index++; } } - // If the yogaIndex is -1, it means the child was not found in the parent container - // This can happen if the child is not part of the layout or is not visible - // In this case, we do not insert the child into the Yoga layout - if (yogaIndex === -1) { - return; - } - - parentLayout.yoga.insertChild(layout.yoga, yogaIndex); + return -1; } + for (const child of parent.children) { + if (!child.layout || !child.visible) continue; + if (child === target) return index; + index++; + } + + return -1; } /** * Removes the child from the layout - * @param layout - The layout to remove the child from */ export function onChildRemoved(layout: Layout) { const yogaParent = layout.yoga.getParent(); - yogaParent && yogaParent!.removeChild(layout.yoga); + if (yogaParent) { + yogaParent.removeChild(layout.yoga); + } } From 0f02cef276516ed42cc19cf7a0e7da76232da602 Mon Sep 17 00:00:00 2001 From: Zyie <24736175+Zyie@users.noreply.github.com> Date: Fri, 12 Sep 2025 13:08:42 +0100 Subject: [PATCH 3/3] fix: borderSizing crashing with scroll enabled fixes cases where border width is NaN or negative. --- package.json | 2 +- src/components/LayoutContainer.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a9d690d9..1eb088a2 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "pretest:unit": "tsx tests/scripts/generate-story-imports.ts", "test:unit": "vitest run", "test:unit:watch": "vitest --browser.headless=false", - "watch": "xs watch", + "watch": "vite build --watch", "prepare": "husky" }, "lint-staged": { diff --git a/src/components/LayoutContainer.ts b/src/components/LayoutContainer.ts index 6b67b152..c299b7d5 100644 --- a/src/components/LayoutContainer.ts +++ b/src/components/LayoutContainer.ts @@ -254,7 +254,11 @@ export class LayoutContainer extends Container { * @protected */ protected _drawBackground(computedLayout: ComputedLayout) { - const borderWidth = this.layout!.yoga.getBorder(Edge.All); + let borderWidth = this.layout!.yoga.getBorder(Edge.All); + + if (isNaN(borderWidth) || borderWidth < 0) { + borderWidth = 0; + } const boxSizing = this.layout!.yoga.getBoxSizing(); const alignment = boxSizing === BoxSizing.BorderBox ? 1 : 0;