diff --git a/TUI.Test/BorderElementRenderTests.cs b/TUI.Test/BorderElementRenderTests.cs
index 325f94e..a922ea3 100644
--- a/TUI.Test/BorderElementRenderTests.cs
+++ b/TUI.Test/BorderElementRenderTests.cs
@@ -2,6 +2,7 @@
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;
@@ -230,4 +231,44 @@ public void TitleWithALineBreakIsDrawnOnOneRow(string title)
Assert.IsFalse(provider.Writes.Any(w => w.Text.Contains('\r') || w.Text.Contains('\n')), "A write carried a line break");
Assert.AreEqual(0, provider.WritesOf(" ab cd ").Single().Position.Y);
}
+
+ ///
+ /// A border sized by a layout is measured wide enough for its title, so a title longer than
+ /// the content is drawn in full rather than cut short or dropped (ktsu-dev/TUI#157).
+ ///
+ [TestMethod]
+ public void TitleLongerThanTheContentIsDrawnInFullInsideAStackPanel()
+ {
+ // Arrange
+ StackPanel panel = new(Orientation.Horizontal) { Spacing = 1 };
+ panel.AddChild(new BorderElement { Title = "Single", Child = new TextElement { Text = "Content" } });
+ panel.AddChild(new BorderElement { Title = "Status", Child = new TextElement { Text = "OK" } });
+ panel.Position = Position.Origin;
+ panel.Dimensions = new Dimensions(80, 24);
+ RecordingConsoleProvider provider = new();
+
+ // Act
+ panel.ArrangeChildren();
+ panel.Render(provider);
+
+ // Assert
+ Assert.ContainsSingle(provider.WritesOf(" Single "));
+ Assert.ContainsSingle(provider.WritesOf(" Status "));
+ }
+
+ ///
+ /// The measure reserves room for the title only when a title is drawn: an untitled border, and a
+ /// border, are still exactly as wide as their content.
+ ///
+ [TestMethod]
+ public void MeasuredWidthMakesRoomForTheTitleOnlyWhenOneIsDrawn()
+ {
+ BorderElement titled = new() { Title = "Status", Child = new TextElement { Text = "OK" } };
+ BorderElement untitled = new() { Child = new TextElement { Text = "OK" } };
+ BorderElement borderless = new(BorderStyle.None) { Title = "Status", Child = new TextElement { Text = "OK" } };
+
+ Assert.AreEqual(" Status ".Length + 2, titled.CalculateRequiredDimensions().Width);
+ Assert.AreEqual("OK".Length + 2, untitled.CalculateRequiredDimensions().Width);
+ Assert.AreEqual("OK".Length + 2, borderless.CalculateRequiredDimensions().Width);
+ }
}
diff --git a/TUI/Elements/Primitives/BorderElement.cs b/TUI/Elements/Primitives/BorderElement.cs
index c318779..be27642 100644
--- a/TUI/Elements/Primitives/BorderElement.cs
+++ b/TUI/Elements/Primitives/BorderElement.cs
@@ -151,9 +151,7 @@ protected override void OnRender(IConsoleProvider provider)
{
int maxTitleWidth = dimensions.Width - 4; // Leave space for border and padding
- // A line break written to the terminal would move the rest of the title off the top
- // border row and over the left edge, so each break becomes a space (ktsu-dev/TUI#151).
- string singleLineTitle = Title.Replace("\r\n", " ", StringComparison.Ordinal).Replace('\r', ' ').Replace('\n', ' ');
+ string singleLineTitle = GetSingleLineTitle();
string displayTitle = singleLineTitle.Length > maxTitleWidth ? singleLineTitle[..maxTitleWidth] : singleLineTitle;
string titleWithPadding = $" {displayTitle} ";
@@ -186,9 +184,24 @@ protected override void OnArrangeChildren()
///
// The border itself is reserved by the Padding(1) set in the constructor, which the base
- // class adds to this result, so only the child's own size belongs here (ktsu-dev/TUI#132)
- protected override Dimensions OnCalculateRequiredDimensionsForChildren() =>
- Children.Count == 0 ? Dimensions.Empty : Children.First().CalculateRequiredDimensions();
+ // 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()
+ {
+ Dimensions childDimensions = Children.Count == 0 ? Dimensions.Empty : Children.First().CalculateRequiredDimensions();
+ if (BorderStyle == BorderStyle.None || string.IsNullOrEmpty(Title))
+ {
+ return childDimensions;
+ }
+
+ return new Dimensions(Math.Max(childDimensions.Width, GetSingleLineTitle().Length + 2), childDimensions.Height);
+ }
+
+ // A line break written to the terminal would move the rest of the title off the top
+ // border row and over the left edge, so each break becomes a space (ktsu-dev/TUI#151).
+ private string GetSingleLineTitle() =>
+ Title.Replace("\r\n", " ", StringComparison.Ordinal).Replace('\r', ' ').Replace('\n', ' ');
private static BorderCharacters GetBorderCharacters(BorderStyle style)
{