diff --git a/TUI.Test/StackPanelTests.cs b/TUI.Test/StackPanelTests.cs
index b08469a..4026b07 100644
--- a/TUI.Test/StackPanelTests.cs
+++ b/TUI.Test/StackPanelTests.cs
@@ -569,15 +569,93 @@ public void StackPanelHorizontalChildSpansThePanelHeight()
}
///
- /// Tests that a container that does not use the available width, such as BorderElement, measures
- /// the same with or without one.
+ /// Tests that a BorderElement passes the width it is measured against down to its child, less
+ /// the border, so word-wrapped text inside it reports a row per wrapped line (ktsu-dev/TUI#161).
///
[TestMethod]
- public void ContainerWithoutAWidthAwareLayoutMeasuresTheSameWithAWidth()
+ public void BorderElementMeasuresItsChildAgainstTheWidthInsideTheBorder()
{
- BorderElement border = [new TextElement("abc")];
+ BorderElement border = [new TextElement("hello world foo") { WordWrap = true }];
- Assert.AreEqual(border.CalculateRequiredDimensions(), border.CalculateRequiredDimensions(2));
+ Assert.AreEqual(new Dimensions(17, 3), border.CalculateRequiredDimensions());
+ Assert.AreEqual(new Dimensions(11, 4), border.CalculateRequiredDimensions(12));
+ }
+
+ ///
+ /// Tests that word-wrapped text inside a border inside a vertical panel is given, and draws,
+ /// every wrapped line rather than only the first (ktsu-dev/TUI#161).
+ ///
+ [TestMethod]
+ public void StackPanelVerticalDrawsEveryWrappedLineOfTextInsideABorder()
+ {
+ TextElement text = new("hello world foo") { WordWrap = true };
+ BorderElement border = [text];
+ StackPanel panel = [border];
+ panel.Dimensions = new Dimensions(12, 10);
+ panel.ArrangeChildren();
+ border.ArrangeChildren();
+
+ RecordingConsoleProvider provider = new();
+ panel.Render(provider);
+
+ Assert.AreEqual(new Dimensions(12, 4), border.Dimensions);
+ Assert.AreEqual(new Dimensions(10, 2), text.Dimensions);
+ Assert.AreEqual(new Position(1, 1), Assert.ContainsSingle(provider.WritesOf("hello")).Position);
+ Assert.AreEqual(new Position(1, 2), Assert.ContainsSingle(provider.WritesOf("world foo")).Position);
+ }
+
+ ///
+ /// 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).
+ ///
+ [TestMethod]
+ public void StackPanelHorizontalRequiredDimensionsWrapTextToTheAvailableWidth()
+ {
+ StackPanel panel = new() { Orientation = Orientation.Horizontal };
+ panel.Add(new TextElement("hello world foo") { WordWrap = true });
+
+ Assert.AreEqual(new Dimensions(15, 1), panel.CalculateRequiredDimensions());
+ Assert.AreEqual(new Dimensions(5, 3), panel.CalculateRequiredDimensions(8));
+ }
+
+ ///
+ /// Tests that a horizontal panel measures each child against the width its earlier siblings and
+ /// the spacing leave, as it does when arranging them (ktsu-dev/TUI#161).
+ ///
+ [TestMethod]
+ public void StackPanelHorizontalMeasuresEachChildAgainstTheRemainingWidth()
+ {
+ StackPanel panel = new() { Orientation = Orientation.Horizontal, Spacing = 1 };
+ panel.Add(new TextElement("abc"));
+ panel.Add(new TextElement("hello world") { WordWrap = true });
+
+ Assert.AreEqual(new Dimensions(9, 2), panel.CalculateRequiredDimensions(9));
+ }
+
+ ///
+ /// Tests that word-wrapped text inside a horizontal panel inside a vertical panel is given a row
+ /// per wrapped line and draws them all, with the next row placed below it (ktsu-dev/TUI#161).
+ ///
+ [TestMethod]
+ public void StackPanelVerticalDrawsEveryWrappedLineOfTextInsideAHorizontalPanel()
+ {
+ StackPanel row = new() { Orientation = Orientation.Horizontal };
+ TextElement text = new("hello world foo") { WordWrap = true };
+ row.Add(text);
+ StackPanel panel = [row, new TextElement("next")];
+ panel.Dimensions = new Dimensions(8, 10);
+ panel.ArrangeChildren();
+ row.ArrangeChildren();
+
+ RecordingConsoleProvider provider = new();
+ panel.Render(provider);
+
+ Assert.AreEqual(new Dimensions(8, 3), row.Dimensions);
+ Assert.AreEqual(new Dimensions(5, 3), text.Dimensions);
+ Assert.AreEqual(new Position(0, 0), Assert.ContainsSingle(provider.WritesOf("hello")).Position);
+ Assert.AreEqual(new Position(0, 1), Assert.ContainsSingle(provider.WritesOf("world")).Position);
+ Assert.AreEqual(new Position(0, 2), Assert.ContainsSingle(provider.WritesOf("foo")).Position);
+ Assert.AreEqual(new Position(0, 3), Assert.ContainsSingle(provider.WritesOf("next")).Position);
}
///
diff --git a/TUI/Elements/Layouts/StackPanel.cs b/TUI/Elements/Layouts/StackPanel.cs
index 321c565..4a4a666 100644
--- a/TUI/Elements/Layouts/StackPanel.cs
+++ b/TUI/Elements/Layouts/StackPanel.cs
@@ -91,11 +91,12 @@ protected override void OnArrangeChildren()
{
unplaced.Remove(child);
- // A vertical child's width is known before its height, so it is measured against that
- // width. Word-wrapped text needs this to report one row per wrapped line (ktsu-dev/TUI#131).
+ // A child is measured against the width it can have: the panel's width when stacking
+ // vertically, and the width not yet taken when stacking horizontally. Word-wrapped text
+ // needs this to report one row per wrapped line (ktsu-dev/TUI#131, ktsu-dev/TUI#161).
Dimensions childDimensions = Orientation == Orientation.Vertical
? child.CalculateRequiredDimensions(contentArea.Width)
- : child.CalculateRequiredDimensions();
+ : child.CalculateRequiredDimensions(contentArea.Width - currentOffset);
if (Orientation == Orientation.Vertical)
{
@@ -165,12 +166,18 @@ private Dimensions CalculateRequiredDimensionsForChildren(int? availableWidth)
IUIElement[] visibleChildren = [.. GetVisibleChildren()];
int totalSpacing = Math.Max(0, (visibleChildren.Length - 1) * Spacing);
+ int consumedWidth = 0;
foreach (IUIElement? child in visibleChildren)
{
- Dimensions childDimensions = Orientation == Orientation.Vertical && availableWidth is int width
- ? child.CalculateRequiredDimensions(width)
- : child.CalculateRequiredDimensions();
+ // 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
+ {
+ int width when Orientation == Orientation.Vertical => child.CalculateRequiredDimensions(width),
+ int width => child.CalculateRequiredDimensions(Math.Max(0, width - consumedWidth)),
+ _ => child.CalculateRequiredDimensions(),
+ };
if (Orientation == Orientation.Vertical)
{
@@ -180,6 +187,7 @@ private Dimensions CalculateRequiredDimensionsForChildren(int? availableWidth)
else
{
totalWidth += childDimensions.Width;
+ consumedWidth += childDimensions.Width + Spacing;
maxHeight = Math.Max(maxHeight, childDimensions.Height);
}
}
diff --git a/TUI/Elements/Primitives/BorderElement.cs b/TUI/Elements/Primitives/BorderElement.cs
index be27642..f962c25 100644
--- a/TUI/Elements/Primitives/BorderElement.cs
+++ b/TUI/Elements/Primitives/BorderElement.cs
@@ -187,9 +187,18 @@ protected override void OnArrangeChildren()
// class adds to this result, so only the child's own size belongs here (ktsu-dev/TUI#132).
// A title is drawn as " title " between the corners, so the content area must be at least
// two wider than the title or a layout sizes the border too narrow to show it (ktsu-dev/TUI#157).
- protected override Dimensions OnCalculateRequiredDimensionsForChildren()
+ protected override Dimensions OnCalculateRequiredDimensionsForChildren() =>
+ WithRoomForTitle(Children.Count == 0 ? Dimensions.Empty : Children.First().CalculateRequiredDimensions());
+
+ ///
+ // The base class has already taken the border off availableWidth, so the child is measured
+ // against the width it will be arranged into. Without this a word-wrapped child is measured
+ // unwrapped and given a single row (ktsu-dev/TUI#161).
+ protected override Dimensions OnCalculateRequiredDimensionsForChildren(int availableWidth) =>
+ WithRoomForTitle(Children.Count == 0 ? Dimensions.Empty : Children.First().CalculateRequiredDimensions(availableWidth));
+
+ private Dimensions WithRoomForTitle(Dimensions childDimensions)
{
- Dimensions childDimensions = Children.Count == 0 ? Dimensions.Empty : Children.First().CalculateRequiredDimensions();
if (BorderStyle == BorderStyle.None || string.IsNullOrEmpty(Title))
{
return childDimensions;