diff --git a/TUI.Test/ContainerNoRoomMeasureTests.cs b/TUI.Test/ContainerNoRoomMeasureTests.cs new file mode 100644 index 0000000..a1be514 --- /dev/null +++ b/TUI.Test/ContainerNoRoomMeasureTests.cs @@ -0,0 +1,69 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.TUI.Test; + +using ktsu.TUI.Core.Elements.Layouts; +using ktsu.TUI.Core.Elements.Primitives; +using ktsu.TUI.Core.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests for a container measured at a width its padding takes entirely (ktsu-dev/TUI#249). +/// +/// +/// The container measured its children at width 0, which reads as "no +/// constraint", so a word-wrapped child reported its full unwrapped height for text that could +/// never be drawn, and the rows it claimed were taken from the siblings below. +/// +[TestClass] +public sealed class ContainerNoRoomMeasureTests +{ + /// + /// A border measured at width 1 or 2 reports the bare border, whatever its child is. + /// + /// A width no wider than the border's padding. + [TestMethod] + [DataRow(1)] + [DataRow(2)] + public void BorderWithNoRoomForItsChildMeasuresAsTheBareBorder(int availableWidth) + { + BorderElement border = new() { Child = new TextElement("one\ntwo\nthree\nfour\nfive") { WordWrap = true } }; + + Assert.AreEqual(new Dimensions(2, 2), border.CalculateRequiredDimensions(availableWidth)); + } + + /// + /// A border given one more column than its padding still measures its child. + /// + [TestMethod] + public void BorderWithOneColumnOfRoomStillMeasuresItsChild() + { + BorderElement border = new() { Child = new TextElement("ab") { WordWrap = true } }; + + Assert.AreEqual(new Dimensions(3, 4), border.CalculateRequiredDimensions(3)); + } + + /// + /// The issue's layout: a border squeezed to its padding width beside a label leaves the row + /// below where the bare border ends, not where its unwrapped child would. + /// + [TestMethod] + public void SqueezedBorderDoesNotPushTheNextRowDown() + { + BorderElement border = new() { Child = new TextElement("one\ntwo\nthree\nfour\nfive") { WordWrap = true } }; + StackPanel row = new(Orientation.Horizontal); + row.AddChild(new TextElement("Label text here: ")); + row.AddChild(border); + StackPanel panel = new(Orientation.Vertical); + panel.AddChild(row); + panel.AddChild(new TextElement("below")); + panel.Position = Position.Origin; + panel.Dimensions = new Dimensions(19, 20); + RecordingConsoleProvider provider = new(); + + panel.ArrangeChildren(); + panel.Render(provider); + + Assert.AreEqual(2, provider.WritesOf("below").Single().Position.Y); + } +} diff --git a/TUI/Elements/UIContainerBase.cs b/TUI/Elements/UIContainerBase.cs index 15e5ba7..be4db3a 100644 --- a/TUI/Elements/UIContainerBase.cs +++ b/TUI/Elements/UIContainerBase.cs @@ -141,7 +141,16 @@ protected override Dimensions OnCalculateRequiredDimensions() /// protected override Dimensions OnCalculateRequiredDimensions(int availableWidth) { - Dimensions requiredDimensions = OnCalculateRequiredDimensionsForChildren(Math.Max(0, availableWidth - Padding.Horizontal)); + if (availableWidth > 0 && availableWidth <= Padding.Horizontal) + { + // The padding takes every column, so no child can be drawn. Measuring the children at + // width 0 would read as "no constraint", and a word-wrapped child would then claim its + // full unwrapped height in rows that never show, taking them from the siblings below + // (ktsu-dev/TUI#249, the container route to ktsu-dev/TUI#198). + return Dimensions.Empty.WithPadding(Padding); + } + + Dimensions requiredDimensions = OnCalculateRequiredDimensionsForChildren(availableWidth - Padding.Horizontal); return requiredDimensions.WithPadding(Padding); }