From 6af5c5372b886bad143744cf501c90018248c4cb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 11:30:09 +0000 Subject: [PATCH] Leave a collapsed child out of a horizontal StackPanel's measured height [patch] When a horizontal StackPanel runs out of width, arranging it collapses the remaining children, but measuring it still counted every child's height. Those children were measured against a width of 0, which TextElement treats as unconstrained, so a tall child that was never drawn set the row's height and left blank rows that pushed later siblings down. Stop measuring once the width is used up, as the arrange pass does, and count spacing only between the children that were measured. The unconstrained measure is unchanged. Fixes ktsu-dev/TUI#224 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW3EQrDsG6bGZe8kZZK --- TUI.Test/StackPanelTests.cs | 37 ++++++++++++++++++++++++++++++ TUI/Elements/Layouts/StackPanel.cs | 13 ++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/TUI.Test/StackPanelTests.cs b/TUI.Test/StackPanelTests.cs index 9d8eea8..f8128ec 100644 --- a/TUI.Test/StackPanelTests.cs +++ b/TUI.Test/StackPanelTests.cs @@ -628,6 +628,43 @@ public void StackPanelVerticalDrawsEveryWrappedLineOfTextInsideABorder() Assert.AreEqual(new Position(1, 2), Assert.ContainsSingle(provider.WritesOf("world foo")).Position); } + /// + /// Tests that a horizontal panel does not ask for the height of a child it collapses for lack of + /// width, so a tall child that no longer fits leaves no blank rows and later siblings move up + /// (ktsu-dev/TUI#224). + /// + [TestMethod] + public void StackPanelHorizontalRequiredHeightIgnoresChildrenThatDoNotFit() + { + TextElement wide = new(new string('A', 20)); + TextElement tall = new("l1\nl2\nl3\nl4\nl5"); + StackPanel row = new(Orientation.Horizontal) { wide, tall }; + TextElement footer = new("footer"); + StackPanel root = [row, footer]; + root.Dimensions = new Dimensions(20, 10); + + root.ArrangeChildren(); + row.ArrangeChildren(); + + Assert.AreEqual(new Dimensions(20, 1), row.CalculateRequiredDimensions(20)); + Assert.AreEqual(Dimensions.Empty, tall.Dimensions); + Assert.AreEqual(new Dimensions(20, 1), row.Dimensions); + Assert.AreEqual(1, footer.Position.Y); + } + + /// + /// Tests that a horizontal panel still measures every child when no width constrains it. + /// + [TestMethod] + public void StackPanelHorizontalUnconstrainedMeasureCountsEveryChild() + { + StackPanel row = new(Orientation.Horizontal) { Spacing = 1 }; + row.Add(new TextElement(new string('A', 20))); + row.Add(new TextElement("l1\nl2\nl3\nl4\nl5")); + + Assert.AreEqual(new Dimensions(23, 5), row.CalculateRequiredDimensions()); + } + /// /// Tests that a horizontal panel measures word-wrapped text against the width it has, so it /// reports the wrapped height to a parent rather than one row (ktsu-dev/TUI#161). diff --git a/TUI/Elements/Layouts/StackPanel.cs b/TUI/Elements/Layouts/StackPanel.cs index 4a4a666..b7eee60 100644 --- a/TUI/Elements/Layouts/StackPanel.cs +++ b/TUI/Elements/Layouts/StackPanel.cs @@ -165,11 +165,20 @@ private Dimensions CalculateRequiredDimensionsForChildren(int? availableWidth) int maxHeight = 0; IUIElement[] visibleChildren = [.. GetVisibleChildren()]; - int totalSpacing = Math.Max(0, (visibleChildren.Length - 1) * Spacing); + int measuredChildren = 0; int consumedWidth = 0; foreach (IUIElement? child in visibleChildren) { + // A horizontal child that starts where the width has run out is collapsed when arranged, + // so it adds neither width nor height, matching the arrange pass's break (ktsu-dev/TUI#224). + if (Orientation == Orientation.Horizontal && availableWidth is int limit && consumedWidth >= limit) + { + break; + } + + measuredChildren++; + // A horizontal child gets the width its earlier siblings and the spacing between them // leave, as it does when arranged (ktsu-dev/TUI#161). Dimensions childDimensions = availableWidth switch @@ -192,6 +201,8 @@ private Dimensions CalculateRequiredDimensionsForChildren(int? availableWidth) } } + int totalSpacing = Math.Max(0, (measuredChildren - 1) * Spacing); + return Orientation == Orientation.Vertical ? new Dimensions(maxWidth, totalHeight + totalSpacing) : new Dimensions(totalWidth + totalSpacing, maxHeight);