Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 83 additions & 5 deletions TUI.Test/StackPanelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -569,15 +569,93 @@ public void StackPanelHorizontalChildSpansThePanelHeight()
}

/// <summary>
/// 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).
/// </summary>
[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));
}

/// <summary>
/// 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).
/// </summary>
[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);
}

/// <summary>
/// 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).
/// </summary>
[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));
}

/// <summary>
/// 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).
/// </summary>
[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));
}

/// <summary>
/// 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).
/// </summary>
[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);
}

/// <summary>
Expand Down
20 changes: 14 additions & 6 deletions TUI/Elements/Layouts/StackPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand All @@ -180,6 +187,7 @@ private Dimensions CalculateRequiredDimensionsForChildren(int? availableWidth)
else
{
totalWidth += childDimensions.Width;
consumedWidth += childDimensions.Width + Spacing;
maxHeight = Math.Max(maxHeight, childDimensions.Height);
}
}
Expand Down
13 changes: 11 additions & 2 deletions TUI/Elements/Primitives/BorderElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());

/// <inheritdoc />
// 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;
Expand Down
Loading