From 9afc1ea72190f145873d59fca6b9069411b9b5f0 Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Wed, 21 Oct 2020 17:59:55 +0200 Subject: [PATCH] Add support for aligning tables --- examples/Colors/Program.cs | 1 - examples/Columns/Program.cs | 1 - examples/Tables/Program.cs | 1 + .../Unit/CalendarTests.cs | 87 ++++++++++ src/Spectre.Console.Tests/Unit/PanelTests.cs | 14 +- src/Spectre.Console.Tests/Unit/TableTests.cs | 160 +++++++++++++++++- .../Extensions/PanelExtensions.cs | 4 +- .../Extensions/TableExtensions.cs | 14 +- src/Spectre.Console/Internal/Aligner.cs | 97 +++++++++++ src/Spectre.Console/Rendering/Segment.cs | 92 +++++----- src/Spectre.Console/Widgets/Calendar.cs | 13 +- src/Spectre.Console/Widgets/Grid.cs | 9 +- src/Spectre.Console/Widgets/Panel.cs | 2 +- src/Spectre.Console/Widgets/PanelHeader.cs | 76 +++++++++ src/Spectre.Console/Widgets/Paragraph.cs | 29 +--- src/Spectre.Console/Widgets/Table.cs | 44 +++-- .../Widgets/{Title.cs => TableTitle.cs} | 30 ++-- 17 files changed, 542 insertions(+), 132 deletions(-) create mode 100644 src/Spectre.Console/Internal/Aligner.cs create mode 100644 src/Spectre.Console/Widgets/PanelHeader.cs rename src/Spectre.Console/Widgets/{Title.cs => TableTitle.cs} (61%) diff --git a/examples/Colors/Program.cs b/examples/Colors/Program.cs index ba0c52c..c68f626 100644 --- a/examples/Colors/Program.cs +++ b/examples/Colors/Program.cs @@ -1,4 +1,3 @@ -using System; using Spectre.Console; namespace ColorExample diff --git a/examples/Columns/Program.cs b/examples/Columns/Program.cs index 70d28cf..ebc8098 100644 --- a/examples/Columns/Program.cs +++ b/examples/Columns/Program.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Diagnostics; using System.Net.Http; using System.Threading.Tasks; using Newtonsoft.Json.Linq; diff --git a/examples/Tables/Program.cs b/examples/Tables/Program.cs index 209bded..1cadf62 100644 --- a/examples/Tables/Program.cs +++ b/examples/Tables/Program.cs @@ -36,6 +36,7 @@ namespace TableExample .AddRow("[blue]Hej[/]", "[yellow]Världen![/]", ""); return new Table() + .Centered() .SetBorder(TableBorder.DoubleEdge) .SetHeading("TABLE [yellow]HEADING[/]") .SetFootnote("TABLE [yellow]FOOTNOTE[/]") diff --git a/src/Spectre.Console.Tests/Unit/CalendarTests.cs b/src/Spectre.Console.Tests/Unit/CalendarTests.cs index 14a5369..8daf78a 100644 --- a/src/Spectre.Console.Tests/Unit/CalendarTests.cs +++ b/src/Spectre.Console.Tests/Unit/CalendarTests.cs @@ -34,6 +34,93 @@ namespace Spectre.Console.Tests.Unit console.Lines[10].ShouldBe("└─────┴─────┴─────┴─────┴─────┴─────┴─────┘"); } + [Fact] + public void Should_Center_Calendar_Correctly() + { + // Given + var console = new PlainConsole(width: 80); + var calendar = new Calendar(2020, 10) + .Centered() + .AddCalendarEvent(new DateTime(2020, 9, 1)) + .AddCalendarEvent(new DateTime(2020, 10, 3)) + .AddCalendarEvent(new DateTime(2020, 10, 12)); + + // When + console.Render(calendar); + + // Then + console.Lines.Count.ShouldBe(11); + console.Lines[00].ShouldBe(" 2020 October "); + console.Lines[01].ShouldBe(" ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐ "); + console.Lines[02].ShouldBe(" │ Sun │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ "); + console.Lines[03].ShouldBe(" ├─────┼─────┼─────┼─────┼─────┼─────┼─────┤ "); + console.Lines[04].ShouldBe(" │ │ │ │ │ 1 │ 2 │ 3* │ "); + console.Lines[05].ShouldBe(" │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ "); + console.Lines[06].ShouldBe(" │ 11 │ 12* │ 13 │ 14 │ 15 │ 16 │ 17 │ "); + console.Lines[07].ShouldBe(" │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │ "); + console.Lines[08].ShouldBe(" │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │ 31 │ "); + console.Lines[09].ShouldBe(" │ │ │ │ │ │ │ │ "); + console.Lines[10].ShouldBe(" └─────┴─────┴─────┴─────┴─────┴─────┴─────┘ "); + } + + [Fact] + public void Should_Left_Align_Calendar_Correctly() + { + // Given + var console = new PlainConsole(width: 80); + var calendar = new Calendar(2020, 10) + .LeftAligned() + .AddCalendarEvent(new DateTime(2020, 9, 1)) + .AddCalendarEvent(new DateTime(2020, 10, 3)) + .AddCalendarEvent(new DateTime(2020, 10, 12)); + + // When + console.Render(calendar); + + // Then + console.Lines.Count.ShouldBe(11); + console.Lines[00].ShouldBe(" 2020 October "); + console.Lines[01].ShouldBe("┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐"); + console.Lines[02].ShouldBe("│ Sun │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │"); + console.Lines[03].ShouldBe("├─────┼─────┼─────┼─────┼─────┼─────┼─────┤"); + console.Lines[04].ShouldBe("│ │ │ │ │ 1 │ 2 │ 3* │"); + console.Lines[05].ShouldBe("│ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │"); + console.Lines[06].ShouldBe("│ 11 │ 12* │ 13 │ 14 │ 15 │ 16 │ 17 │"); + console.Lines[07].ShouldBe("│ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │"); + console.Lines[08].ShouldBe("│ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │ 31 │"); + console.Lines[09].ShouldBe("│ │ │ │ │ │ │ │"); + console.Lines[10].ShouldBe("└─────┴─────┴─────┴─────┴─────┴─────┴─────┘"); + } + + [Fact] + public void Should_Right_Align_Calendar_Correctly() + { + // Given + var console = new PlainConsole(width: 80); + var calendar = new Calendar(2020, 10) + .RightAligned() + .AddCalendarEvent(new DateTime(2020, 9, 1)) + .AddCalendarEvent(new DateTime(2020, 10, 3)) + .AddCalendarEvent(new DateTime(2020, 10, 12)); + + // When + console.Render(calendar); + + // Then + console.Lines.Count.ShouldBe(11); + console.Lines[00].ShouldBe(" 2020 October "); + console.Lines[01].ShouldBe(" ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐"); + console.Lines[02].ShouldBe(" │ Sun │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │"); + console.Lines[03].ShouldBe(" ├─────┼─────┼─────┼─────┼─────┼─────┼─────┤"); + console.Lines[04].ShouldBe(" │ │ │ │ │ 1 │ 2 │ 3* │"); + console.Lines[05].ShouldBe(" │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │"); + console.Lines[06].ShouldBe(" │ 11 │ 12* │ 13 │ 14 │ 15 │ 16 │ 17 │"); + console.Lines[07].ShouldBe(" │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │"); + console.Lines[08].ShouldBe(" │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │ 31 │"); + console.Lines[09].ShouldBe(" │ │ │ │ │ │ │ │"); + console.Lines[10].ShouldBe(" └─────┴─────┴─────┴─────┴─────┴─────┴─────┘"); + } + [Fact] public void Should_Render_Calendar_Correctly_For_Specific_Culture() { diff --git a/src/Spectre.Console.Tests/Unit/PanelTests.cs b/src/Spectre.Console.Tests/Unit/PanelTests.cs index 828f5a8..9c3ddf8 100644 --- a/src/Spectre.Console.Tests/Unit/PanelTests.cs +++ b/src/Spectre.Console.Tests/Unit/PanelTests.cs @@ -73,7 +73,7 @@ namespace Spectre.Console.Tests.Unit // When console.Render(new Panel("Hello World") { - Header = new Title("Greeting"), + Header = new PanelHeader("Greeting"), Expand = true, Padding = new Padding(2, 0, 2, 0), }); @@ -94,7 +94,7 @@ namespace Spectre.Console.Tests.Unit // When console.Render(new Panel("Hello World") { - Header = new Title("Greeting").LeftAligned(), + Header = new PanelHeader("Greeting").LeftAligned(), Expand = true, }); @@ -114,7 +114,7 @@ namespace Spectre.Console.Tests.Unit // When console.Render(new Panel("Hello World") { - Header = new Title("Greeting").Centered(), + Header = new PanelHeader("Greeting").Centered(), Expand = true, }); @@ -134,7 +134,7 @@ namespace Spectre.Console.Tests.Unit // When console.Render(new Panel("Hello World") { - Header = new Title("Greeting").RightAligned(), + Header = new PanelHeader("Greeting").RightAligned(), Expand = true, }); @@ -154,7 +154,7 @@ namespace Spectre.Console.Tests.Unit // When console.Render(new Panel("Hello World") { - Header = new Title("Greeting"), + Header = new PanelHeader("Greeting"), Expand = true, }); @@ -244,7 +244,7 @@ namespace Spectre.Console.Tests.Unit } [Fact] - public void Should_Justify_Child_To_Right() + public void Should_Justify_Child_To_Right_Correctly() { // Given var console = new PlainConsole(width: 25); @@ -264,7 +264,7 @@ namespace Spectre.Console.Tests.Unit } [Fact] - public void Should_Justify_Child_To_Center() + public void Should_Center_Child_Correctly() { // Given var console = new PlainConsole(width: 25); diff --git a/src/Spectre.Console.Tests/Unit/TableTests.cs b/src/Spectre.Console.Tests/Unit/TableTests.cs index 3842198..9b0c1ef 100644 --- a/src/Spectre.Console.Tests/Unit/TableTests.cs +++ b/src/Spectre.Console.Tests/Unit/TableTests.cs @@ -168,6 +168,78 @@ namespace Spectre.Console.Tests.Unit console.Lines[5].ShouldBe("└────────┴────────┴───────┘"); } + [Fact] + public void Should_Left_Align_Table_Correctly() + { + // Given + var console = new PlainConsole(width: 80); + var table = new Table(); + table.Alignment = Justify.Left; + table.AddColumns("Foo", "Bar", "Baz"); + table.AddRow("Qux", "Corgi", "Waldo"); + table.AddRow("Grault", "Garply", "Fred"); + + // When + console.Render(table); + + // Then + console.Lines.Count.ShouldBe(6); + console.Lines[0].ShouldBe("┌────────┬────────┬───────┐"); + console.Lines[1].ShouldBe("│ Foo │ Bar │ Baz │"); + console.Lines[2].ShouldBe("├────────┼────────┼───────┤"); + console.Lines[3].ShouldBe("│ Qux │ Corgi │ Waldo │"); + console.Lines[4].ShouldBe("│ Grault │ Garply │ Fred │"); + console.Lines[5].ShouldBe("└────────┴────────┴───────┘"); + } + + [Fact] + public void Should_Center_Table_Correctly() + { + // Given + var console = new PlainConsole(width: 80); + var table = new Table(); + table.Alignment = Justify.Center; + table.AddColumns("Foo", "Bar", "Baz"); + table.AddRow("Qux", "Corgi", "Waldo"); + table.AddRow("Grault", "Garply", "Fred"); + + // When + console.Render(table); + + // Then + console.Lines.Count.ShouldBe(6); + console.Lines[0].ShouldBe(" ┌────────┬────────┬───────┐ "); + console.Lines[1].ShouldBe(" │ Foo │ Bar │ Baz │ "); + console.Lines[2].ShouldBe(" ├────────┼────────┼───────┤ "); + console.Lines[3].ShouldBe(" │ Qux │ Corgi │ Waldo │ "); + console.Lines[4].ShouldBe(" │ Grault │ Garply │ Fred │ "); + console.Lines[5].ShouldBe(" └────────┴────────┴───────┘ "); + } + + [Fact] + public void Should_Right_Align_Table_Correctly() + { + // Given + var console = new PlainConsole(width: 80); + var table = new Table(); + table.Alignment = Justify.Right; + table.AddColumns("Foo", "Bar", "Baz"); + table.AddRow("Qux", "Corgi", "Waldo"); + table.AddRow("Grault", "Garply", "Fred"); + + // When + console.Render(table); + + // Then + console.Lines.Count.ShouldBe(6); + console.Lines[0].ShouldBe(" ┌────────┬────────┬───────┐"); + console.Lines[1].ShouldBe(" │ Foo │ Bar │ Baz │"); + console.Lines[2].ShouldBe(" ├────────┼────────┼───────┤"); + console.Lines[3].ShouldBe(" │ Qux │ Corgi │ Waldo │"); + console.Lines[4].ShouldBe(" │ Grault │ Garply │ Fred │"); + console.Lines[5].ShouldBe(" └────────┴────────┴───────┘"); + } + [Fact] public void Should_Render_Table_Nested_In_Panels_Correctly() { @@ -393,8 +465,8 @@ namespace Spectre.Console.Tests.Unit // Given var console = new PlainConsole(width: 80); var table = new Table { Border = TableBorder.Rounded }; - table.Heading = new Title("Hello World"); - table.Footnote = new Title("Goodbye World"); + table.Heading = new TableTitle("Hello World"); + table.Footnote = new TableTitle("Goodbye World"); table.AddColumns("Foo", "Bar", "Baz"); table.AddRow("Qux", "Corgi", "Waldo"); table.AddRow("Grault", "Garply", "Fred"); @@ -413,5 +485,89 @@ namespace Spectre.Console.Tests.Unit console.Lines[6].ShouldBe("╰────────┴────────┴───────╯"); console.Lines[7].ShouldBe(" Goodbye World "); } + + [Fact] + public void Should_Left_Align_Table_With_Title_And_Footnote_Correctly() + { + // Given + var console = new PlainConsole(width: 80); + var table = new Table { Border = TableBorder.Rounded }; + table.LeftAligned(); + table.Heading = new TableTitle("Hello World"); + table.Footnote = new TableTitle("Goodbye World"); + table.AddColumns("Foo", "Bar", "Baz"); + table.AddRow("Qux", "Corgi", "Waldo"); + table.AddRow("Grault", "Garply", "Fred"); + + // When + console.Render(table); + + // Then + console.Lines.Count.ShouldBe(8); + console.Lines[0].ShouldBe(" Hello World "); + console.Lines[1].ShouldBe("╭────────┬────────┬───────╮"); + console.Lines[2].ShouldBe("│ Foo │ Bar │ Baz │"); + console.Lines[3].ShouldBe("├────────┼────────┼───────┤"); + console.Lines[4].ShouldBe("│ Qux │ Corgi │ Waldo │"); + console.Lines[5].ShouldBe("│ Grault │ Garply │ Fred │"); + console.Lines[6].ShouldBe("╰────────┴────────┴───────╯"); + console.Lines[7].ShouldBe(" Goodbye World "); + } + + [Fact] + public void Should_Center_Table_With_Title_And_Footnote_Correctly() + { + // Given + var console = new PlainConsole(width: 80); + var table = new Table { Border = TableBorder.Rounded }; + table.Centered(); + table.Heading = new TableTitle("Hello World"); + table.Footnote = new TableTitle("Goodbye World"); + table.AddColumns("Foo", "Bar", "Baz"); + table.AddRow("Qux", "Corgi", "Waldo"); + table.AddRow("Grault", "Garply", "Fred"); + + // When + console.Render(table); + + // Then + console.Lines.Count.ShouldBe(8); + console.Lines[0].ShouldBe(" Hello World "); + console.Lines[1].ShouldBe(" ╭────────┬────────┬───────╮ "); + console.Lines[2].ShouldBe(" │ Foo │ Bar │ Baz │ "); + console.Lines[3].ShouldBe(" ├────────┼────────┼───────┤ "); + console.Lines[4].ShouldBe(" │ Qux │ Corgi │ Waldo │ "); + console.Lines[5].ShouldBe(" │ Grault │ Garply │ Fred │ "); + console.Lines[6].ShouldBe(" ╰────────┴────────┴───────╯ "); + console.Lines[7].ShouldBe(" Goodbye World "); + } + + [Fact] + public void Should_Right_Align_Table_With_Title_And_Footnote_Correctly() + { + // Given + var console = new PlainConsole(width: 80); + var table = new Table { Border = TableBorder.Rounded }; + table.RightAligned(); + table.Heading = new TableTitle("Hello World"); + table.Footnote = new TableTitle("Goodbye World"); + table.AddColumns("Foo", "Bar", "Baz"); + table.AddRow("Qux", "Corgi", "Waldo"); + table.AddRow("Grault", "Garply", "Fred"); + + // When + console.Render(table); + + // Then + console.Lines.Count.ShouldBe(8); + console.Lines[0].ShouldBe(" Hello World "); + console.Lines[1].ShouldBe(" ╭────────┬────────┬───────╮"); + console.Lines[2].ShouldBe(" │ Foo │ Bar │ Baz │"); + console.Lines[3].ShouldBe(" ├────────┼────────┼───────┤"); + console.Lines[4].ShouldBe(" │ Qux │ Corgi │ Waldo │"); + console.Lines[5].ShouldBe(" │ Grault │ Garply │ Fred │"); + console.Lines[6].ShouldBe(" ╰────────┴────────┴───────╯"); + console.Lines[7].ShouldBe(" Goodbye World "); + } } } diff --git a/src/Spectre.Console/Extensions/PanelExtensions.cs b/src/Spectre.Console/Extensions/PanelExtensions.cs index 94492fd..84a7c38 100644 --- a/src/Spectre.Console/Extensions/PanelExtensions.cs +++ b/src/Spectre.Console/Extensions/PanelExtensions.cs @@ -27,7 +27,7 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(text)); } - return SetHeader(panel, new Title(text, style, alignment)); + return SetHeader(panel, new PanelHeader(text, style, alignment)); } /// @@ -36,7 +36,7 @@ namespace Spectre.Console /// The panel. /// The header to use. /// The same instance so that multiple calls can be chained. - public static Panel SetHeader(this Panel panel, Title header) + public static Panel SetHeader(this Panel panel, PanelHeader header) { if (panel is null) { diff --git a/src/Spectre.Console/Extensions/TableExtensions.cs b/src/Spectre.Console/Extensions/TableExtensions.cs index b1d0c03..0487228 100644 --- a/src/Spectre.Console/Extensions/TableExtensions.cs +++ b/src/Spectre.Console/Extensions/TableExtensions.cs @@ -183,9 +183,8 @@ namespace Spectre.Console /// The table. /// The heading. /// The style. - /// The alignment. /// The same instance so that multiple calls can be chained. - public static Table SetHeading(this Table table, string text, Style? style = null, Justify? alignment = null) + public static Table SetHeading(this Table table, string text, Style? style = null) { if (table is null) { @@ -197,7 +196,7 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(text)); } - return SetHeading(table, new Title(text, style, alignment)); + return SetHeading(table, new TableTitle(text, style)); } /// @@ -206,7 +205,7 @@ namespace Spectre.Console /// The table. /// The heading. /// The same instance so that multiple calls can be chained. - public static Table SetHeading(this Table table, Title heading) + public static Table SetHeading(this Table table, TableTitle heading) { if (table is null) { @@ -223,9 +222,8 @@ namespace Spectre.Console /// The table. /// The footnote. /// The style. - /// The alignment. /// The same instance so that multiple calls can be chained. - public static Table SetFootnote(this Table table, string text, Style? style = null, Justify? alignment = null) + public static Table SetFootnote(this Table table, string text, Style? style = null) { if (table is null) { @@ -237,7 +235,7 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(text)); } - return SetFootnote(table, new Title(text, style, alignment)); + return SetFootnote(table, new TableTitle(text, style)); } /// @@ -246,7 +244,7 @@ namespace Spectre.Console /// The table. /// The footnote. /// The same instance so that multiple calls can be chained. - public static Table SetFootnote(this Table table, Title footnote) + public static Table SetFootnote(this Table table, TableTitle footnote) { if (table is null) { diff --git a/src/Spectre.Console/Internal/Aligner.cs b/src/Spectre.Console/Internal/Aligner.cs new file mode 100644 index 0000000..f51e208 --- /dev/null +++ b/src/Spectre.Console/Internal/Aligner.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using Spectre.Console.Rendering; + +namespace Spectre.Console.Internal +{ + internal static class Aligner + { + public static string Align(RenderContext context, string text, Justify? alignment, int maxWidth) + { + if (alignment == null || alignment == Justify.Left) + { + return text; + } + + var width = Cell.GetCellLength(context, text); + if (width >= maxWidth) + { + return text; + } + + switch (alignment) + { + case Justify.Right: + { + var diff = maxWidth - width; + return new string(' ', diff) + text; + } + + case Justify.Center: + { + var diff = (maxWidth - width) / 2; + + var left = new string(' ', diff); + var right = new string(' ', diff); + + // Right side + var remainder = (maxWidth - width) % 2; + if (remainder != 0) + { + right += new string(' ', remainder); + } + + return left + text + right; + } + + default: + throw new NotSupportedException("Unknown alignment"); + } + } + + public static void Align(RenderContext context, T segments, Justify? alignment, int maxWidth) + where T : List + { + if (alignment == null || alignment == Justify.Left) + { + return; + } + + var width = Segment.CellLength(context, segments); + if (width >= maxWidth) + { + return; + } + + switch (alignment) + { + case Justify.Right: + { + var diff = maxWidth - width; + segments.Insert(0, new Segment(new string(' ', diff))); + break; + } + + case Justify.Center: + { + // Left side. + var diff = (maxWidth - width) / 2; + segments.Insert(0, new Segment(new string(' ', diff))); + + // Right side + segments.Add(new Segment(new string(' ', diff))); + var remainder = (maxWidth - width) % 2; + if (remainder != 0) + { + segments.Add(new Segment(new string(' ', remainder))); + } + + break; + } + + default: + throw new NotSupportedException("Unknown alignment"); + } + } + } +} diff --git a/src/Spectre.Console/Rendering/Segment.cs b/src/Spectre.Console/Rendering/Segment.cs index 3256b7f..7b42b20 100644 --- a/src/Spectre.Console/Rendering/Segment.cs +++ b/src/Spectre.Console/Rendering/Segment.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Text; using Spectre.Console.Internal; @@ -68,12 +67,7 @@ namespace Spectre.Console.Rendering private Segment(string text, Style style, bool lineBreak) { - if (text is null) - { - throw new ArgumentNullException(nameof(text)); - } - - Text = text.NormalizeLineEndings(); + Text = text?.NormalizeLineEndings() ?? throw new ArgumentNullException(nameof(text)); Style = style ?? throw new ArgumentNullException(nameof(style)); IsLineBreak = lineBreak; IsWhiteSpace = string.IsNullOrWhiteSpace(text); @@ -121,6 +115,15 @@ namespace Spectre.Console.Rendering new Segment(Text.Substring(offset, Text.Length - offset), Style)); } + /// + /// Clones the segment. + /// + /// A new segment that's identical to this one. + public Segment Clone() + { + return new Segment(Text, Style); + } + /// /// Gets the number of cells that the segments occupies in the console. /// @@ -238,48 +241,6 @@ namespace Spectre.Console.Rendering return lines; } - internal static IEnumerable Merge(IEnumerable segments) - { - var result = new List(); - - var previous = (Segment?)null; - foreach (var segment in segments) - { - if (previous == null) - { - previous = segment; - continue; - } - - // Same style? - if (previous.Style.Equals(segment.Style) && !previous.IsLineBreak) - { - previous = new Segment(previous.Text + segment.Text, previous.Style); - } - else - { - result.Add(previous); - previous = segment; - } - } - - if (previous != null) - { - result.Add(previous); - } - - return result; - } - - /// - /// Clones the segment. - /// - /// A new segment that's identical to this one. - public Segment Clone() - { - return new Segment(Text, Style); - } - /// /// Splits an overflowing segment into several new segments. /// @@ -436,6 +397,39 @@ namespace Spectre.Console.Rendering return new Segment(builder.ToString(), segment.Style); } + internal static IEnumerable Merge(IEnumerable segments) + { + var result = new List(); + + var previous = (Segment?)null; + foreach (var segment in segments) + { + if (previous == null) + { + previous = segment; + continue; + } + + // Same style? + if (previous.Style.Equals(segment.Style) && !previous.IsLineBreak) + { + previous = new Segment(previous.Text + segment.Text, previous.Style); + } + else + { + result.Add(previous); + previous = segment; + } + } + + if (previous != null) + { + result.Add(previous); + } + + return result; + } + internal static Segment TruncateWithEllipsis(string text, Style style, RenderContext context, int maxWidth) { return SplitOverflow( diff --git a/src/Spectre.Console/Widgets/Calendar.cs b/src/Spectre.Console/Widgets/Calendar.cs index 13bcd76..a1e665a 100644 --- a/src/Spectre.Console/Widgets/Calendar.cs +++ b/src/Spectre.Console/Widgets/Calendar.cs @@ -11,7 +11,7 @@ namespace Spectre.Console /// /// A renderable calendar. /// - public sealed class Calendar : Renderable, IHasCulture, IHasTableBorder + public sealed class Calendar : Renderable, IHasCulture, IHasTableBorder, IAlignable { private const int NumberOfWeekDays = 7; private const int ExpectedRowCount = 6; @@ -30,6 +30,7 @@ namespace Spectre.Console private Style _highlightStyle; private bool _showHeader; private Style? _headerStyle; + private Justify? _alignment; /// /// Gets or sets the calendar year. @@ -115,6 +116,13 @@ namespace Spectre.Console set => MarkAsDirty(() => _headerStyle = value); } + /// + public Justify? Alignment + { + get => _alignment; + set => MarkAsDirty(() => _alignment = value); + } + /// /// Gets a list containing all calendar events. /// @@ -195,12 +203,13 @@ namespace Spectre.Console Border = _border, UseSafeBorder = _useSafeBorder, BorderStyle = _borderStyle, + Alignment = _alignment, }; if (ShowHeader) { var heading = new DateTime(Year, Month, Day).ToString("Y", culture).EscapeMarkup(); - table.Heading = new Title(heading, HeaderStyle); + table.Heading = new TableTitle(heading, HeaderStyle); } // Add columns diff --git a/src/Spectre.Console/Widgets/Grid.cs b/src/Spectre.Console/Widgets/Grid.cs index 4fe9181..b11b3de 100644 --- a/src/Spectre.Console/Widgets/Grid.cs +++ b/src/Spectre.Console/Widgets/Grid.cs @@ -7,7 +7,7 @@ namespace Spectre.Console /// /// A renderable grid. /// - public sealed class Grid : Renderable, IExpandable + public sealed class Grid : Renderable, IExpandable, IAlignable { private readonly Table _table; @@ -28,6 +28,13 @@ namespace Spectre.Console set => _table.Expand = value; } + /// + public Justify? Alignment + { + get => _table.Alignment; + set => _table.Alignment = value; + } + /// /// Initializes a new instance of the class. /// diff --git a/src/Spectre.Console/Widgets/Panel.cs b/src/Spectre.Console/Widgets/Panel.cs index 5c2f708..e1bc285 100644 --- a/src/Spectre.Console/Widgets/Panel.cs +++ b/src/Spectre.Console/Widgets/Panel.cs @@ -39,7 +39,7 @@ namespace Spectre.Console /// /// Gets or sets the header. /// - public Title? Header { get; set; } + public PanelHeader? Header { get; set; } /// /// Initializes a new instance of the class. diff --git a/src/Spectre.Console/Widgets/PanelHeader.cs b/src/Spectre.Console/Widgets/PanelHeader.cs new file mode 100644 index 0000000..0c327f8 --- /dev/null +++ b/src/Spectre.Console/Widgets/PanelHeader.cs @@ -0,0 +1,76 @@ +using System; + +namespace Spectre.Console +{ + /// + /// Represents a panel header. + /// + public sealed class PanelHeader : IAlignable + { + /// + /// Gets the panel header text. + /// + public string Text { get; } + + /// + /// Gets or sets the panel header style. + /// + public Style? Style { get; set; } + + /// + /// Gets or sets the panel header alignment. + /// + public Justify? Alignment { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The panel header text. + /// The panel header style. + /// The panel header alignment. + public PanelHeader(string text, Style? style = null, Justify? alignment = null) + { + Text = text ?? throw new ArgumentNullException(nameof(text)); + Style = style; + Alignment = alignment; + } + + /// + /// Sets the panel header style. + /// + /// The panel header style. + /// The same instance so that multiple calls can be chained. + public PanelHeader SetStyle(Style? style) + { + Style = style ?? Style.Plain; + return this; + } + + /// + /// Sets the panel header style. + /// + /// The panel header style. + /// The same instance so that multiple calls can be chained. + public PanelHeader SetStyle(string style) + { + if (style is null) + { + throw new ArgumentNullException(nameof(style)); + } + + Style = Style.Parse(style); + return this; + } + + /// + /// Sets the panel header alignment. + /// + /// The panel header alignment. + /// The same instance so that multiple calls can be chained. + public PanelHeader SetAlignment(Justify alignment) + { + Alignment = alignment; + return this; + } + } +} diff --git a/src/Spectre.Console/Widgets/Paragraph.cs b/src/Spectre.Console/Widgets/Paragraph.cs index b9bd8e7..a78ed3a 100644 --- a/src/Spectre.Console/Widgets/Paragraph.cs +++ b/src/Spectre.Console/Widgets/Paragraph.cs @@ -144,37 +144,18 @@ namespace Spectre.Console // Justify lines var justification = context.Justification ?? Alignment ?? Justify.Left; - foreach (var (_, _, last, line) in lines.Enumerate()) + if (justification != Justify.Left) { - var length = line.Sum(l => l.StripLineEndings().CellLength(context)); - if (length < maxWidth) + foreach (var line in lines) { - // Justify right side - if (justification == Justify.Right) - { - var diff = maxWidth - length; - line.Prepend(new Segment(new string(' ', diff))); - } - else if (justification == Justify.Center) - { - // Left side. - var diff = (maxWidth - length) / 2; - line.Prepend(new Segment(new string(' ', diff))); - - // Right side - line.Add(new Segment(new string(' ', diff))); - var remainder = (maxWidth - length) % 2; - if (remainder != 0) - { - line.Add(new Segment(new string(' ', remainder))); - } - } + Aligner.Align(context, line, justification, maxWidth); } } if (context.SingleLine) { - return lines.First().Where(segment => !segment.IsLineBreak); + // Return the first line + return lines[0].Where(segment => !segment.IsLineBreak); } return new SegmentLineEnumerator(lines); diff --git a/src/Spectre.Console/Widgets/Table.cs b/src/Spectre.Console/Widgets/Table.cs index 52bc054..b937e72 100644 --- a/src/Spectre.Console/Widgets/Table.cs +++ b/src/Spectre.Console/Widgets/Table.cs @@ -9,7 +9,7 @@ namespace Spectre.Console /// /// A renderable table. /// - public sealed class Table : Renderable, IHasTableBorder, IExpandable + public sealed class Table : Renderable, IHasTableBorder, IExpandable, IAlignable { private const int EdgeCount = 2; @@ -58,12 +58,15 @@ namespace Spectre.Console /// /// Gets or sets the table title. /// - public Title? Heading { get; set; } + public TableTitle? Heading { get; set; } /// /// Gets or sets the table footnote. /// - public Title? Footnote { get; set; } + public TableTitle? Footnote { get; set; } + + /// + public Justify? Alignment { get; set; } // Whether this is a grid or not. internal bool IsGrid { get; set; } @@ -200,7 +203,7 @@ namespace Spectre.Console rows.AddRange(_rows); var result = new List(); - result.AddRange(RenderAnnotation(context, Heading, tableWidth, _defaultHeadingStyle)); + result.AddRange(RenderAnnotation(context, Heading, actualMaxWidth, tableWidth, _defaultHeadingStyle)); // Iterate all rows foreach (var (index, firstRow, lastRow, row) in rows.Enumerate()) @@ -222,7 +225,7 @@ namespace Spectre.Console // Show top of header? if (firstRow && showBorder) { - var separator = border.GetColumnRow(TablePart.Top, columnWidths, _columns); + var separator = Aligner.Align(context, border.GetColumnRow(TablePart.Top, columnWidths, _columns), Alignment, actualMaxWidth); result.Add(new Segment(separator, borderStyle)); result.Add(Segment.LineBreak); } @@ -288,6 +291,9 @@ namespace Spectre.Console } } + // Align the row result. + Aligner.Align(context, rowResult, Alignment, actualMaxWidth); + // Is the row larger than the allowed max width? if (Segment.CellLength(context, rowResult) > actualMaxWidth) { @@ -304,7 +310,7 @@ namespace Spectre.Console // Show header separator? if (firstRow && showBorder && ShowHeaders && hasRows) { - var separator = border.GetColumnRow(TablePart.Separator, columnWidths, _columns); + var separator = Aligner.Align(context, border.GetColumnRow(TablePart.Separator, columnWidths, _columns), Alignment, actualMaxWidth); result.Add(new Segment(separator, borderStyle)); result.Add(Segment.LineBreak); } @@ -312,13 +318,13 @@ namespace Spectre.Console // Show bottom of footer? if (lastRow && showBorder) { - var separator = border.GetColumnRow(TablePart.Bottom, columnWidths, _columns); + var separator = Aligner.Align(context, border.GetColumnRow(TablePart.Bottom, columnWidths, _columns), Alignment, actualMaxWidth); result.Add(new Segment(separator, borderStyle)); result.Add(Segment.LineBreak); } } - result.AddRange(RenderAnnotation(context, Footnote, tableWidth, _defaultFootnoteStyle)); + result.AddRange(RenderAnnotation(context, Footnote, actualMaxWidth, tableWidth, _defaultFootnoteStyle)); return result; } @@ -392,25 +398,27 @@ namespace Spectre.Console return widths; } - private static IEnumerable RenderAnnotation( - RenderContext context, Title? header, - int maxWidth, Style defaultStyle) + private IEnumerable RenderAnnotation( + RenderContext context, TableTitle? header, + int maxWidth, int tableWidth, Style defaultStyle) { if (header == null) { - yield break; + return Array.Empty(); } var paragraph = new Markup(header.Text.Capitalize(), header.Style ?? defaultStyle) - .SetAlignment(header.Alignment ?? Justify.Center) + .SetAlignment(Justify.Center) .SetOverflow(Overflow.Ellipsis); - foreach (var segment in ((IRenderable)paragraph).Render(context, maxWidth)) - { - yield return segment; - } + var items = new List(); + items.AddRange(((IRenderable)paragraph).Render(context, tableWidth)); - yield return Segment.LineBreak; + // Align over the whole buffer area + Aligner.Align(context, items, Alignment, maxWidth); + + items.Add(Segment.LineBreak); + return items; } private (int Min, int Max) MeasureColumn(TableColumn column, RenderContext options, int maxWidth) diff --git a/src/Spectre.Console/Widgets/Title.cs b/src/Spectre.Console/Widgets/TableTitle.cs similarity index 61% rename from src/Spectre.Console/Widgets/Title.cs rename to src/Spectre.Console/Widgets/TableTitle.cs index 83cbab4..9967015 100644 --- a/src/Spectre.Console/Widgets/Title.cs +++ b/src/Spectre.Console/Widgets/TableTitle.cs @@ -3,9 +3,9 @@ using System; namespace Spectre.Console { /// - /// Represents a title. + /// Represents a table title such as a heading or footnote. /// - public sealed class Title : IAlignable + public sealed class TableTitle { /// /// Gets the title text. @@ -18,21 +18,14 @@ namespace Spectre.Console public Style? Style { get; set; } /// - /// Gets or sets the title alignment. - /// - public Justify? Alignment { get; set; } - - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The title text. /// The title style. - /// The title alignment. - public Title(string text, Style? style = null, Justify? alignment = null) + public TableTitle(string text, Style? style = null) { Text = text ?? throw new ArgumentNullException(nameof(text)); Style = style; - Alignment = alignment; } /// @@ -40,20 +33,25 @@ namespace Spectre.Console /// /// The title style. /// The same instance so that multiple calls can be chained. - public Title SetStyle(Style? style) + public TableTitle SetStyle(Style? style) { Style = style ?? Style.Plain; return this; } /// - /// Sets the title alignment. + /// Sets the title style. /// - /// The title alignment. + /// The title style. /// The same instance so that multiple calls can be chained. - public Title SetAlignment(Justify alignment) + public TableTitle SetStyle(string style) { - Alignment = alignment; + if (style is null) + { + throw new ArgumentNullException(nameof(style)); + } + + Style = Style.Parse(style); return this; } }