diff --git a/examples/Colors/Program.cs b/examples/Colors/Program.cs index 0c031e1..0effd49 100644 --- a/examples/Colors/Program.cs +++ b/examples/Colors/Program.cs @@ -4,7 +4,7 @@ namespace ColorExample { public static class Program { - public static void Main(string[] args) + public static void Main() { if (AnsiConsole.Capabilities.ColorSystem == ColorSystem.NoColors) { diff --git a/examples/Diagnostic/Program.cs b/examples/Diagnostic/Program.cs index a577ff4..c194bde 100644 --- a/examples/Diagnostic/Program.cs +++ b/examples/Diagnostic/Program.cs @@ -3,9 +3,9 @@ using Spectre.Console; namespace Diagnostic { - public class Program + public static class Program { - public static void Main(string[] args) + public static void Main() { AnsiConsole.MarkupLine("Color system: [bold]{0}[/]", AnsiConsole.Capabilities.ColorSystem); AnsiConsole.MarkupLine("Supports ansi? [bold]{0}[/]", AnsiConsole.Capabilities.SupportsAnsi); diff --git a/examples/Grid/Program.cs b/examples/Grid/Program.cs index 554d952..7e082d5 100644 --- a/examples/Grid/Program.cs +++ b/examples/Grid/Program.cs @@ -2,9 +2,9 @@ using Spectre.Console; namespace GridExample { - public sealed class Program + public static class Program { - static void Main(string[] args) + public static void Main() { AnsiConsole.WriteLine(); AnsiConsole.MarkupLine("Usage: [grey]dotnet [blue]run[/] [[options]] [[[[--]] ...]]]][/]"); diff --git a/examples/Panel/Program.cs b/examples/Panel/Program.cs index 7b92f33..df3b033 100644 --- a/examples/Panel/Program.cs +++ b/examples/Panel/Program.cs @@ -2,9 +2,9 @@ using Spectre.Console; namespace PanelExample { - class Program + public static class Program { - static void Main(string[] args) + public static void Main() { var content = new Markup( "[underline]I[/] heard [underline on blue]you[/] like panels\n\n\n\n" + @@ -14,7 +14,7 @@ namespace PanelExample new Panel( new Panel(content) { - Border = BorderKind.Rounded + Border = BorderKind.Rounded, })); // Left adjusted panel with text @@ -22,6 +22,7 @@ namespace PanelExample new Text("Left adjusted\nLeft").LeftAligned()) { Expand = true, + Header = new Header("Left", new Style(foreground: Color.Red)).LeftAligned(), }); // Centered ASCII panel with text @@ -30,6 +31,7 @@ namespace PanelExample { Expand = true, Border = BorderKind.Ascii, + Header = new Header("Center", new Style(foreground: Color.Green)).Centered(), }); // Right adjusted, rounded panel with text @@ -38,6 +40,7 @@ namespace PanelExample { Expand = true, Border = BorderKind.Rounded, + Header = new Header("Right", new Style(foreground: Color.Blue)).RightAligned(), }); } } diff --git a/examples/Table/Program.cs b/examples/Table/Program.cs index 3912867..1b00046 100644 --- a/examples/Table/Program.cs +++ b/examples/Table/Program.cs @@ -5,7 +5,7 @@ namespace TableExample { public static class Program { - public static void Main(string[] args) + public static void Main() { // A simple table RenderSimpleTable(); diff --git a/src/Spectre.Console.Tests/Unit/PanelTests.cs b/src/Spectre.Console.Tests/Unit/PanelTests.cs index 31b792d..8c64eee 100644 --- a/src/Spectre.Console.Tests/Unit/PanelTests.cs +++ b/src/Spectre.Console.Tests/Unit/PanelTests.cs @@ -40,6 +40,108 @@ namespace Spectre.Console.Tests.Unit console.Lines[2].ShouldBe("└───────────────────┘"); } + [Fact] + public void Should_Render_Panel_With_Header() + { + // Given + var console = new PlainConsole(width: 80); + + // When + console.Render(new Panel("Hello World") + { + Header = new Header("Greeting"), + Expand = true, + Padding = new Padding(2, 2), + }); + + // Then + console.Lines.Count.ShouldBe(3); + console.Lines[0].ShouldBe("┌─Greeting─────────────────────────────────────────────────────────────────────┐"); + console.Lines[1].ShouldBe("│ Hello World │"); + console.Lines[2].ShouldBe("└──────────────────────────────────────────────────────────────────────────────┘"); + } + + [Fact] + public void Should_Render_Panel_With_Left_Aligned_Header() + { + // Given + var console = new PlainConsole(width: 80); + + // When + console.Render(new Panel("Hello World") + { + Header = new Header("Greeting").LeftAligned(), + Expand = true, + }); + + // Then + console.Lines.Count.ShouldBe(3); + console.Lines[0].ShouldBe("┌─Greeting─────────────────────────────────────────────────────────────────────┐"); + console.Lines[1].ShouldBe("│ Hello World │"); + console.Lines[2].ShouldBe("└──────────────────────────────────────────────────────────────────────────────┘"); + } + + [Fact] + public void Should_Render_Panel_With_Centered_Header() + { + // Given + var console = new PlainConsole(width: 80); + + // When + console.Render(new Panel("Hello World") + { + Header = new Header("Greeting").Centered(), + Expand = true, + }); + + // Then + console.Lines.Count.ShouldBe(3); + console.Lines[0].ShouldBe("┌───────────────────────────────────Greeting───────────────────────────────────┐"); + console.Lines[1].ShouldBe("│ Hello World │"); + console.Lines[2].ShouldBe("└──────────────────────────────────────────────────────────────────────────────┘"); + } + + [Fact] + public void Should_Render_Panel_With_Right_Aligned_Header() + { + // Given + var console = new PlainConsole(width: 80); + + // When + console.Render(new Panel("Hello World") + { + Header = new Header("Greeting").RightAligned(), + Expand = true, + }); + + // Then + console.Lines.Count.ShouldBe(3); + console.Lines[0].ShouldBe("┌─────────────────────────────────────────────────────────────────────Greeting─┐"); + console.Lines[1].ShouldBe("│ Hello World │"); + console.Lines[2].ShouldBe("└──────────────────────────────────────────────────────────────────────────────┘"); + } + + [Fact] + public void Should_Collapse_Header_If_It_Will_Not_Fit() + { + // Given + var console = new PlainConsole(width: 10); + + // When + console.Render(new Panel("Hello World") + { + Header = new Header("Greeting"), + Expand = true, + }); + + // Then + console.Lines.Count.ShouldBe(4); + console.Lines[0].ShouldBe("┌─Greet…─┐"); + console.Lines[1].ShouldBe("│ Hello │"); + console.Lines[2].ShouldBe("│ World │"); + console.Lines[3].ShouldBe("└────────┘"); + } + [Fact] public void Should_Render_Panel_With_Unicode_Correctly() { diff --git a/src/Spectre.Console/Rendering/Header.cs b/src/Spectre.Console/Rendering/Header.cs new file mode 100644 index 0000000..e57974d --- /dev/null +++ b/src/Spectre.Console/Rendering/Header.cs @@ -0,0 +1,60 @@ +using System; + +namespace Spectre.Console +{ + /// + /// Represents a header. + /// + public sealed class Header : IAlignable + { + /// + /// Gets the header text. + /// + public string Text { get; } + + /// + /// Gets or sets the header style. + /// + public Style? Style { get; set; } + + /// + /// Gets or sets the header alignment. + /// + public Justify? Alignment { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The header text. + /// The header style. + /// The header alignment. + public Header(string text, Style? style = null, Justify? alignment = null) + { + Text = text ?? throw new ArgumentNullException(nameof(text)); + Style = style; + Alignment = alignment; + } + + /// + /// Sets the header style. + /// + /// The header style. + /// The same instance so that multiple calls can be chained. + public Header SetStyle(Style? style) + { + Style = style ?? Style.Plain; + return this; + } + + /// + /// Sets the header alignment. + /// + /// The header alignment. + /// The same instance so that multiple calls can be chained. + public Header SetAlignment(Justify alignment) + { + Alignment = alignment; + return this; + } + } +} diff --git a/src/Spectre.Console/Rendering/Panel.cs b/src/Spectre.Console/Rendering/Panel.cs index 3e1ec72..200af4b 100644 --- a/src/Spectre.Console/Rendering/Panel.cs +++ b/src/Spectre.Console/Rendering/Panel.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Spectre.Console.Rendering; @@ -35,6 +36,11 @@ namespace Spectre.Console /// public Padding Padding { get; set; } = new Padding(1, 1); + /// + /// Gets or sets the header. + /// + public Header? Header { get; set; } + /// /// Initializes a new instance of the class. /// @@ -77,21 +83,16 @@ namespace Spectre.Console childWidth = measurement.Max; } - var panelWidth = childWidth + paddingWidth; + var panelWidth = childWidth + EdgeWidth + paddingWidth; + panelWidth = Math.Min(panelWidth, maxWidth); + + var result = new List(); // Panel top - var result = new List - { - new Segment(border.GetPart(BorderPart.HeaderTopLeft), borderStyle), - new Segment(border.GetPart(BorderPart.HeaderTop, panelWidth), borderStyle), - new Segment(border.GetPart(BorderPart.HeaderTopRight), borderStyle), - Segment.LineBreak, - }; - - // Render the child. - var childSegments = _child.Render(context, childWidth); + AddTopBorder(result, context, border, borderStyle, panelWidth); // Split the child segments into lines. + var childSegments = _child.Render(context, childWidth); foreach (var line in Segment.SplitLines(childSegments, panelWidth)) { result.Add(new Segment(border.GetPart(BorderPart.CellLeft), borderStyle)); @@ -126,12 +127,62 @@ namespace Spectre.Console } // Panel bottom - result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft), borderStyle)); - result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, panelWidth), borderStyle)); - result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight), borderStyle)); - result.Add(Segment.LineBreak); + AddBottomBorder(result, border, borderStyle, panelWidth); return result; } + + private static void AddBottomBorder(List result, SpectreBorder border, Style borderStyle, int panelWidth) + { + result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft), borderStyle)); + result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, panelWidth - EdgeWidth), borderStyle)); + result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight), borderStyle)); + result.Add(Segment.LineBreak); + } + + private void AddTopBorder(List segments, RenderContext context, SpectreBorder border, Style borderStyle, int panelWidth) + { + segments.Add(new Segment(border.GetPart(BorderPart.HeaderTopLeft), borderStyle)); + + if (Header != null) + { + var leftSpacing = 0; + var rightSpacing = 0; + + var headerWidth = panelWidth - (EdgeWidth * 2); + var header = Segment.TruncateWithEllipsis(Header.Text, Header.Style ?? borderStyle, context.Encoding, headerWidth); + + var excessWidth = headerWidth - header.CellLength(context.Encoding); + if (excessWidth > 0) + { + switch (Header.Alignment ?? Justify.Left) + { + case Justify.Left: + leftSpacing = 0; + rightSpacing = excessWidth; + break; + case Justify.Right: + leftSpacing = excessWidth; + rightSpacing = 0; + break; + case Justify.Center: + leftSpacing = excessWidth / 2; + rightSpacing = (excessWidth / 2) + (excessWidth % 2); + break; + } + } + + segments.Add(new Segment(border.GetPart(BorderPart.HeaderTop, leftSpacing + 1), borderStyle)); + segments.Add(header); + segments.Add(new Segment(border.GetPart(BorderPart.HeaderTop, rightSpacing + 1), borderStyle)); + } + else + { + segments.Add(new Segment(border.GetPart(BorderPart.HeaderTop, panelWidth - EdgeWidth), borderStyle)); + } + + segments.Add(new Segment(border.GetPart(BorderPart.HeaderTopRight), borderStyle)); + segments.Add(Segment.LineBreak); + } } } diff --git a/src/Spectre.Console/Rendering/PanelExtensions.cs b/src/Spectre.Console/Rendering/PanelExtensions.cs new file mode 100644 index 0000000..1f4488e --- /dev/null +++ b/src/Spectre.Console/Rendering/PanelExtensions.cs @@ -0,0 +1,50 @@ +using System; + +namespace Spectre.Console.Rendering +{ + /// + /// Contains extension methods for . + /// + public static class PanelExtensions + { + /// + /// Sets the panel header. + /// + /// The panel. + /// The header text. + /// The header style. + /// The header alignment. + /// The same instance so that multiple calls can be chained. + public static Panel SetHeader(this Panel panel, string text, Style? style = null, Justify? alignment = null) + { + if (panel is null) + { + throw new ArgumentNullException(nameof(panel)); + } + + if (text is null) + { + throw new ArgumentNullException(nameof(text)); + } + + return SetHeader(panel, new Header(text, style, alignment)); + } + + /// + /// Sets the panel header. + /// + /// The panel. + /// The header to use. + /// The same instance so that multiple calls can be chained. + public static Panel SetHeader(this Panel panel, Header header) + { + if (panel is null) + { + throw new ArgumentNullException(nameof(panel)); + } + + panel.Header = header; + return panel; + } + } +} diff --git a/src/Spectre.Console/Rendering/Paragraph.cs b/src/Spectre.Console/Rendering/Paragraph.cs index 8167a6f..f7d83e2 100644 --- a/src/Spectre.Console/Rendering/Paragraph.cs +++ b/src/Spectre.Console/Rendering/Paragraph.cs @@ -122,7 +122,7 @@ namespace Spectre.Console var min = _lines.Max(line => line.Max(segment => segment.CellLength(context.Encoding))); var max = _lines.Max(x => x.CellWidth(context.Encoding)); - return new Measurement(min, max); + return new Measurement(min, Math.Min(max, maxWidth)); } /// diff --git a/src/Spectre.Console/Rendering/Segment.cs b/src/Spectre.Console/Rendering/Segment.cs index b398595..6afa7e5 100644 --- a/src/Spectre.Console/Rendering/Segment.cs +++ b/src/Spectre.Console/Rendering/Segment.cs @@ -305,13 +305,21 @@ namespace Spectre.Console.Rendering } else if (overflow == Overflow.Ellipsis) { - result.Add(new Segment(segment.Text.Substring(0, width - 1), segment.Style)); - result.Add(new Segment("…", segment.Style)); + result.Add(new Segment(segment.Text.Substring(0, width - 1) + "…", segment.Style)); } return result; } + internal static Segment TruncateWithEllipsis(string text, Style style, Encoding encoding, int maxWidth) + { + return SplitOverflow( + new Segment(text, style), + Overflow.Ellipsis, + encoding, + maxWidth).First(); + } + internal static List> MakeSameHeight(int cellHeight, List> cells) { foreach (var cell in cells)