From c82d8c45233feafdd26b1e3ae7b785dc592f6cea Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Sat, 16 Sep 2023 18:49:12 +0200 Subject: [PATCH] Add option to show separator between table rows (#1304) * Add option to show separator between table rows * Panels should show header if borders are not shown Closes #835 --- examples/Console/Borders/Program.cs | 56 +++++++++---------- .../Extensions/TableExtensions.cs | 32 +++++++++++ .../Rendering/Borders/TableBorderPart.cs | 20 +++++++ .../Borders/Tables/Ascii2TableBorder.cs | 4 ++ .../Tables/AsciiDoubleHeadTableBorder.cs | 4 ++ .../Borders/Tables/AsciiTableBorder.cs | 4 ++ .../Borders/Tables/DoubleEdgeTableBorder.cs | 4 ++ .../Borders/Tables/DoubleTableBorder.cs | 4 ++ .../Borders/Tables/HeavyEdgeTableBorder.cs | 4 ++ .../Borders/Tables/HeavyHeadTableBorder.cs | 4 ++ .../Borders/Tables/HeavyTableBorder.cs | 4 ++ .../Borders/Tables/HorizontalTableBorder.cs | 4 ++ .../Borders/Tables/MarkdownTableBorder.cs | 7 +++ .../Tables/MinimalDoubleHeadTableBorder.cs | 4 ++ .../Tables/MinimalHeavyHeadTableBorder.cs | 4 ++ .../Borders/Tables/MinimalTableBorder.cs | 4 ++ .../Rendering/Borders/Tables/NoTableBorder.cs | 3 + .../Borders/Tables/RoundedTableBorder.cs | 4 ++ .../Borders/Tables/SimpleHeavyTableBorder.cs | 4 ++ .../Borders/Tables/SimpleTableBorder.cs | 4 ++ .../Borders/Tables/SquareTableBorder.cs | 4 ++ src/Spectre.Console/Rendering/TablePart.cs | 5 ++ src/Spectre.Console/Spectre.Console.csproj | 3 - src/Spectre.Console/TableBorder.cs | 12 +++- src/Spectre.Console/Widgets/Panel.cs | 9 +++ src/Spectre.Console/Widgets/Table/Table.cs | 5 ++ .../Widgets/Table/TableRenderer.cs | 51 ++++++++++++++--- .../Widgets/Table/TableRendererContext.cs | 2 + .../Borders/Box/NoBorder.Output.verified.txt | 2 +- .../NoBorder_With_Header.Output.verified.txt | 2 + .../Render_Row_Separators.Output.verified.txt | 7 +++ .../Unit/Rendering/Borders/BoxBorderTests.cs | 16 ++++++ .../Unit/Widgets/Table/TableTests.cs | 19 +++++++ 33 files changed, 272 insertions(+), 43 deletions(-) create mode 100644 test/Spectre.Console.Tests/Expectations/Rendering/Borders/Box/NoBorder_With_Header.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_Row_Separators.Output.verified.txt diff --git a/examples/Console/Borders/Program.cs b/examples/Console/Borders/Program.cs index 0dca101..2b755ae 100644 --- a/examples/Console/Borders/Program.cs +++ b/examples/Console/Borders/Program.cs @@ -20,21 +20,22 @@ public static class Program { static IRenderable CreatePanel(string name, BoxBorder border) { - return new Panel($"This is a panel with\nthe [yellow]{name}[/] border.") - .Header($" [blue]{name}[/] ", Justify.Center) - .Border(border) - .BorderStyle(Style.Parse("grey")); + return + new Panel($"This is a panel with\nthe [yellow]{name}[/] border.") + .Header($" [blue]{name}[/] ", Justify.Center) + .Border(border) + .BorderStyle(Style.Parse("grey")); } var items = new[] { - CreatePanel("Ascii", BoxBorder.Ascii), - CreatePanel("Square", BoxBorder.Square), - CreatePanel("Rounded", BoxBorder.Rounded), - CreatePanel("Heavy", BoxBorder.Heavy), - CreatePanel("Double", BoxBorder.Double), - CreatePanel("None", BoxBorder.None), - }; + CreatePanel("Ascii", BoxBorder.Ascii), + CreatePanel("Square", BoxBorder.Square), + CreatePanel("Rounded", BoxBorder.Rounded), + CreatePanel("Heavy", BoxBorder.Heavy), + CreatePanel("Double", BoxBorder.Double), + CreatePanel("None", BoxBorder.None), + }; AnsiConsole.Write( new Padder( @@ -47,6 +48,7 @@ public static class Program static IRenderable CreateTable(string name, TableBorder border) { var table = new Table().Border(border); + table.ShowRowSeparators(); table.AddColumn("[yellow]Header 1[/]", c => c.Footer("[grey]Footer 1[/]")); table.AddColumn("[yellow]Header 2[/]", col => col.Footer("[grey]Footer 2[/]").RightAligned()); table.AddRow("Cell", "Cell"); @@ -54,29 +56,23 @@ public static class Program return new Panel(table) .Header($" [blue]{name}[/] ", Justify.Center) + .PadBottom(1) .NoBorder(); } var items = new[] { - CreateTable("Ascii", TableBorder.Ascii), - CreateTable("Ascii2", TableBorder.Ascii2), - CreateTable("AsciiDoubleHead", TableBorder.AsciiDoubleHead), - CreateTable("Horizontal", TableBorder.Horizontal), - CreateTable("Simple", TableBorder.Simple), - CreateTable("SimpleHeavy", TableBorder.SimpleHeavy), - CreateTable("Minimal", TableBorder.Minimal), - CreateTable("MinimalHeavyHead", TableBorder.MinimalHeavyHead), - CreateTable("MinimalDoubleHead", TableBorder.MinimalDoubleHead), - CreateTable("Square", TableBorder.Square), - CreateTable("Rounded", TableBorder.Rounded), - CreateTable("Heavy", TableBorder.Heavy), - CreateTable("HeavyEdge", TableBorder.HeavyEdge), - CreateTable("HeavyHead", TableBorder.HeavyHead), - CreateTable("Double", TableBorder.Double), - CreateTable("DoubleEdge", TableBorder.DoubleEdge), - CreateTable("Markdown", TableBorder.Markdown), - }; + CreateTable("Ascii", TableBorder.Ascii), CreateTable("Ascii2", TableBorder.Ascii2), + CreateTable("AsciiDoubleHead", TableBorder.AsciiDoubleHead), + CreateTable("Horizontal", TableBorder.Horizontal), CreateTable("Simple", TableBorder.Simple), + CreateTable("SimpleHeavy", TableBorder.SimpleHeavy), CreateTable("Minimal", TableBorder.Minimal), + CreateTable("MinimalHeavyHead", TableBorder.MinimalHeavyHead), + CreateTable("MinimalDoubleHead", TableBorder.MinimalDoubleHead), + CreateTable("Square", TableBorder.Square), CreateTable("Rounded", TableBorder.Rounded), + CreateTable("Heavy", TableBorder.Heavy), CreateTable("HeavyEdge", TableBorder.HeavyEdge), + CreateTable("HeavyHead", TableBorder.HeavyHead), CreateTable("Double", TableBorder.Double), + CreateTable("DoubleEdge", TableBorder.DoubleEdge), CreateTable("Markdown", TableBorder.Markdown), + }; AnsiConsole.Write(new Columns(items).Collapse()); } @@ -87,4 +83,4 @@ public static class Program AnsiConsole.Write(new Rule($"[white bold]{title}[/]").RuleStyle("grey").LeftJustified()); AnsiConsole.WriteLine(); } -} +} \ No newline at end of file diff --git a/src/Spectre.Console/Extensions/TableExtensions.cs b/src/Spectre.Console/Extensions/TableExtensions.cs index e232e64..ddb2331 100644 --- a/src/Spectre.Console/Extensions/TableExtensions.cs +++ b/src/Spectre.Console/Extensions/TableExtensions.cs @@ -334,6 +334,38 @@ public static class TableExtensions return table; } + /// + /// Shows row separators. + /// + /// The table. + /// The same instance so that multiple calls can be chained. + public static Table ShowRowSeparators(this Table table) + { + if (table is null) + { + throw new ArgumentNullException(nameof(table)); + } + + table.ShowRowSeparators = true; + return table; + } + + /// + /// Hides row separators. + /// + /// The table. + /// The same instance so that multiple calls can be chained. + public static Table HideRowSeparators(this Table table) + { + if (table is null) + { + throw new ArgumentNullException(nameof(table)); + } + + table.ShowRowSeparators = false; + return table; + } + /// /// Shows table footers. /// diff --git a/src/Spectre.Console/Rendering/Borders/TableBorderPart.cs b/src/Spectre.Console/Rendering/Borders/TableBorderPart.cs index 2359266..dbf64af 100644 --- a/src/Spectre.Console/Rendering/Borders/TableBorderPart.cs +++ b/src/Spectre.Console/Rendering/Borders/TableBorderPart.cs @@ -114,4 +114,24 @@ public enum TableBorderPart /// The bottom right part of a footer. /// FooterBottomRight, + + /// + /// The left part of a row. + /// + RowLeft, + + /// + /// The center part of a row. + /// + RowCenter, + + /// + /// The separator part of a row. + /// + RowSeparator, + + /// + /// The right part of a row. + /// + RowRight, } \ No newline at end of file diff --git a/src/Spectre.Console/Rendering/Borders/Tables/Ascii2TableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/Ascii2TableBorder.cs index 7bd17d6..0cd6122 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/Ascii2TableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/Ascii2TableBorder.cs @@ -32,6 +32,10 @@ public sealed class Ascii2TableBorder : TableBorder TableBorderPart.FooterBottom => "-", TableBorderPart.FooterBottomSeparator => "+", TableBorderPart.FooterBottomRight => "+", + TableBorderPart.RowLeft => "|", + TableBorderPart.RowCenter => "-", + TableBorderPart.RowSeparator => "+", + TableBorderPart.RowRight => "|", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/Borders/Tables/AsciiDoubleHeadTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/AsciiDoubleHeadTableBorder.cs index f5f2bad..33c78ce 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/AsciiDoubleHeadTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/AsciiDoubleHeadTableBorder.cs @@ -32,6 +32,10 @@ public sealed class AsciiDoubleHeadTableBorder : TableBorder TableBorderPart.FooterBottom => "-", TableBorderPart.FooterBottomSeparator => "+", TableBorderPart.FooterBottomRight => "+", + TableBorderPart.RowLeft => "|", + TableBorderPart.RowCenter => "-", + TableBorderPart.RowSeparator => "+", + TableBorderPart.RowRight => "|", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/Borders/Tables/AsciiTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/AsciiTableBorder.cs index 7f4f743..903665c 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/AsciiTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/AsciiTableBorder.cs @@ -32,6 +32,10 @@ public sealed class AsciiTableBorder : TableBorder TableBorderPart.FooterBottom => "-", TableBorderPart.FooterBottomSeparator => "-", TableBorderPart.FooterBottomRight => "+", + TableBorderPart.RowLeft => "|", + TableBorderPart.RowCenter => "-", + TableBorderPart.RowSeparator => "+", + TableBorderPart.RowRight => "|", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/Borders/Tables/DoubleEdgeTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/DoubleEdgeTableBorder.cs index ab13621..6f756ff 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/DoubleEdgeTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/DoubleEdgeTableBorder.cs @@ -32,6 +32,10 @@ public sealed class DoubleEdgeTableBorder : TableBorder TableBorderPart.FooterBottom => "═", TableBorderPart.FooterBottomSeparator => "╧", TableBorderPart.FooterBottomRight => "╝", + TableBorderPart.RowLeft => "╟", + TableBorderPart.RowCenter => "─", + TableBorderPart.RowSeparator => "┼", + TableBorderPart.RowRight => "╢", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/Borders/Tables/DoubleTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/DoubleTableBorder.cs index b5eca04..40b2f6b 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/DoubleTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/DoubleTableBorder.cs @@ -32,6 +32,10 @@ public sealed class DoubleTableBorder : TableBorder TableBorderPart.FooterBottom => "═", TableBorderPart.FooterBottomSeparator => "╩", TableBorderPart.FooterBottomRight => "╝", + TableBorderPart.RowLeft => "╠", + TableBorderPart.RowCenter => "═", + TableBorderPart.RowSeparator => "╬", + TableBorderPart.RowRight => "╣", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/Borders/Tables/HeavyEdgeTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/HeavyEdgeTableBorder.cs index f5728be..31cadca 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/HeavyEdgeTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/HeavyEdgeTableBorder.cs @@ -35,6 +35,10 @@ public sealed class HeavyEdgeTableBorder : TableBorder TableBorderPart.FooterBottom => "━", TableBorderPart.FooterBottomSeparator => "┷", TableBorderPart.FooterBottomRight => "┛", + TableBorderPart.RowLeft => "┠", + TableBorderPart.RowCenter => "─", + TableBorderPart.RowSeparator => "┼", + TableBorderPart.RowRight => "┨", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/Borders/Tables/HeavyHeadTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/HeavyHeadTableBorder.cs index 87f3574..36c71c3 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/HeavyHeadTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/HeavyHeadTableBorder.cs @@ -35,6 +35,10 @@ public sealed class HeavyHeadTableBorder : TableBorder TableBorderPart.FooterBottom => "─", TableBorderPart.FooterBottomSeparator => "┴", TableBorderPart.FooterBottomRight => "┘", + TableBorderPart.RowLeft => "├", + TableBorderPart.RowCenter => "─", + TableBorderPart.RowSeparator => "┼", + TableBorderPart.RowRight => "┤", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/Borders/Tables/HeavyTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/HeavyTableBorder.cs index 783f742..68d3675 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/HeavyTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/HeavyTableBorder.cs @@ -35,6 +35,10 @@ public sealed class HeavyTableBorder : TableBorder TableBorderPart.FooterBottom => "━", TableBorderPart.FooterBottomSeparator => "┻", TableBorderPart.FooterBottomRight => "┛", + TableBorderPart.RowLeft => "┣", + TableBorderPart.RowCenter => "━", + TableBorderPart.RowSeparator => "╋", + TableBorderPart.RowRight => "┫", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/Borders/Tables/HorizontalTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/HorizontalTableBorder.cs index db3021d..740f8e5 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/HorizontalTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/HorizontalTableBorder.cs @@ -32,6 +32,10 @@ public sealed class HorizontalTableBorder : TableBorder TableBorderPart.FooterBottom => "─", TableBorderPart.FooterBottomSeparator => "─", TableBorderPart.FooterBottomRight => "─", + TableBorderPart.RowLeft => "─", + TableBorderPart.RowCenter => "─", + TableBorderPart.RowSeparator => "─", + TableBorderPart.RowRight => "─", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/Borders/Tables/MarkdownTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/MarkdownTableBorder.cs index f338c39..fa21f1c 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/MarkdownTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/MarkdownTableBorder.cs @@ -5,6 +5,9 @@ namespace Spectre.Console.Rendering; /// public sealed class MarkdownTableBorder : TableBorder { + /// + public override bool SupportsRowSeparator => false; + /// public override string GetPart(TableBorderPart part) { @@ -32,6 +35,10 @@ public sealed class MarkdownTableBorder : TableBorder TableBorderPart.FooterBottom => " ", TableBorderPart.FooterBottomSeparator => " ", TableBorderPart.FooterBottomRight => " ", + TableBorderPart.RowLeft => " ", + TableBorderPart.RowCenter => " ", + TableBorderPart.RowSeparator => " ", + TableBorderPart.RowRight => " ", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/Borders/Tables/MinimalDoubleHeadTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/MinimalDoubleHeadTableBorder.cs index 5fd281e..8d58d8f 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/MinimalDoubleHeadTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/MinimalDoubleHeadTableBorder.cs @@ -32,6 +32,10 @@ public sealed class MinimalDoubleHeadTableBorder : TableBorder TableBorderPart.FooterBottom => " ", TableBorderPart.FooterBottomSeparator => " ", TableBorderPart.FooterBottomRight => " ", + TableBorderPart.RowLeft => " ", + TableBorderPart.RowCenter => "─", + TableBorderPart.RowSeparator => "┼", + TableBorderPart.RowRight => " ", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/Borders/Tables/MinimalHeavyHeadTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/MinimalHeavyHeadTableBorder.cs index 7c8cffe..4d9fd6d 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/MinimalHeavyHeadTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/MinimalHeavyHeadTableBorder.cs @@ -35,6 +35,10 @@ public sealed class MinimalHeavyHeadTableBorder : TableBorder TableBorderPart.FooterBottom => " ", TableBorderPart.FooterBottomSeparator => " ", TableBorderPart.FooterBottomRight => " ", + TableBorderPart.RowLeft => " ", + TableBorderPart.RowCenter => "─", + TableBorderPart.RowSeparator => "┼", + TableBorderPart.RowRight => " ", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/Borders/Tables/MinimalTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/MinimalTableBorder.cs index 06d47aa..54d1d17 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/MinimalTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/MinimalTableBorder.cs @@ -32,6 +32,10 @@ public sealed class MinimalTableBorder : TableBorder TableBorderPart.FooterBottom => " ", TableBorderPart.FooterBottomSeparator => " ", TableBorderPart.FooterBottomRight => " ", + TableBorderPart.RowLeft => " ", + TableBorderPart.RowCenter => "─", + TableBorderPart.RowSeparator => "┼", + TableBorderPart.RowRight => " ", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/Borders/Tables/NoTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/NoTableBorder.cs index c251d9b..26ed655 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/NoTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/NoTableBorder.cs @@ -8,6 +8,9 @@ public sealed class NoTableBorder : TableBorder /// public override bool Visible => false; + /// + public override bool SupportsRowSeparator => false; + /// public override string GetPart(TableBorderPart part) { diff --git a/src/Spectre.Console/Rendering/Borders/Tables/RoundedTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/RoundedTableBorder.cs index 9d02c68..5db78ad 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/RoundedTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/RoundedTableBorder.cs @@ -35,6 +35,10 @@ public sealed class RoundedTableBorder : TableBorder TableBorderPart.FooterBottom => "─", TableBorderPart.FooterBottomSeparator => "┴", TableBorderPart.FooterBottomRight => "╯", + TableBorderPart.RowLeft => "├", + TableBorderPart.RowCenter => "─", + TableBorderPart.RowSeparator => "┼", + TableBorderPart.RowRight => "┤", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/Borders/Tables/SimpleHeavyTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/SimpleHeavyTableBorder.cs index d8ae957..1a703ab 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/SimpleHeavyTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/SimpleHeavyTableBorder.cs @@ -35,6 +35,10 @@ public sealed class SimpleHeavyTableBorder : TableBorder TableBorderPart.FooterBottom => " ", TableBorderPart.FooterBottomSeparator => " ", TableBorderPart.FooterBottomRight => " ", + TableBorderPart.RowLeft => "─", + TableBorderPart.RowCenter => "─", + TableBorderPart.RowSeparator => "─", + TableBorderPart.RowRight => "─", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/Borders/Tables/SimpleTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/SimpleTableBorder.cs index 30fa563..0bc8531 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/SimpleTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/SimpleTableBorder.cs @@ -32,6 +32,10 @@ public sealed class SimpleTableBorder : TableBorder TableBorderPart.FooterBottom => " ", TableBorderPart.FooterBottomSeparator => " ", TableBorderPart.FooterBottomRight => " ", + TableBorderPart.RowLeft => "─", + TableBorderPart.RowCenter => "─", + TableBorderPart.RowSeparator => "─", + TableBorderPart.RowRight => "─", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/Borders/Tables/SquareTableBorder.cs b/src/Spectre.Console/Rendering/Borders/Tables/SquareTableBorder.cs index 4e7e731..e30ce3d 100644 --- a/src/Spectre.Console/Rendering/Borders/Tables/SquareTableBorder.cs +++ b/src/Spectre.Console/Rendering/Borders/Tables/SquareTableBorder.cs @@ -32,6 +32,10 @@ public sealed class SquareTableBorder : TableBorder TableBorderPart.FooterBottom => "─", TableBorderPart.FooterBottomSeparator => "┴", TableBorderPart.FooterBottomRight => "┘", + TableBorderPart.RowLeft => "├", + TableBorderPart.RowCenter => "─", + TableBorderPart.RowSeparator => "┼", + TableBorderPart.RowRight => "┤", _ => throw new InvalidOperationException("Unknown border part."), }; } diff --git a/src/Spectre.Console/Rendering/TablePart.cs b/src/Spectre.Console/Rendering/TablePart.cs index 1992252..1634967 100644 --- a/src/Spectre.Console/Rendering/TablePart.cs +++ b/src/Spectre.Console/Rendering/TablePart.cs @@ -15,6 +15,11 @@ public enum TablePart /// HeaderSeparator, + /// + /// The separator between the rows. + /// + RowSeparator, + /// /// The separator between the footer and the cells. /// diff --git a/src/Spectre.Console/Spectre.Console.csproj b/src/Spectre.Console/Spectre.Console.csproj index bf1630c..fdecc82 100644 --- a/src/Spectre.Console/Spectre.Console.csproj +++ b/src/Spectre.Console/Spectre.Console.csproj @@ -36,9 +36,6 @@ - - - $(DefineConstants)TRACE;WCWIDTH_VISIBILITY_INTERNAL diff --git a/src/Spectre.Console/TableBorder.cs b/src/Spectre.Console/TableBorder.cs index a206544..a339713 100644 --- a/src/Spectre.Console/TableBorder.cs +++ b/src/Spectre.Console/TableBorder.cs @@ -13,7 +13,12 @@ public abstract partial class TableBorder /// /// Gets the safe border for this border or null if none exist. /// - public virtual TableBorder? SafeBorder { get; } + public virtual TableBorder? SafeBorder { get; } = null; + + /// + /// Gets a value indicating whether the border supports row separators or not. + /// + public virtual bool SupportsRowSeparator { get; } = true; /// /// Gets the string representation of a specified table border part. @@ -81,6 +86,11 @@ public abstract partial class TableBorder (GetPart(TableBorderPart.HeaderBottomLeft), GetPart(TableBorderPart.HeaderBottom), GetPart(TableBorderPart.HeaderBottomSeparator), GetPart(TableBorderPart.HeaderBottomRight)), + // Separator between header and cells + TablePart.RowSeparator => + (GetPart(TableBorderPart.RowLeft), GetPart(TableBorderPart.RowCenter), + GetPart(TableBorderPart.RowSeparator), GetPart(TableBorderPart.RowRight)), + // Separator between footer and cells TablePart.FooterSeparator => (GetPart(TableBorderPart.FooterTopLeft), GetPart(TableBorderPart.FooterTop), diff --git a/src/Spectre.Console/Widgets/Panel.cs b/src/Spectre.Console/Widgets/Panel.cs index 05d41d4..7a34d95 100644 --- a/src/Spectre.Console/Widgets/Panel.cs +++ b/src/Spectre.Console/Widgets/Panel.cs @@ -132,6 +132,15 @@ public sealed class Panel : Renderable, IHasBoxBorder, IHasBorder, IExpandable, { AddTopBorder(result, options, border, borderStyle, panelWidth); } + else + { + // Not showing border, but we have a header text? + // Use a invisible border to draw the top border + if (Header?.Text != null) + { + AddTopBorder(result, options, BoxBorder.None, borderStyle, panelWidth); + } + } // Split the child segments into lines. var childSegments = ((IRenderable)child).Render(options with { Height = height }, innerWidth); diff --git a/src/Spectre.Console/Widgets/Table/Table.cs b/src/Spectre.Console/Widgets/Table/Table.cs index c0d83e1..345e83d 100644 --- a/src/Spectre.Console/Widgets/Table/Table.cs +++ b/src/Spectre.Console/Widgets/Table/Table.cs @@ -31,6 +31,11 @@ public sealed class Table : Renderable, IHasTableBorder, IExpandable, IAlignable /// public bool ShowHeaders { get; set; } = true; + /// + /// Gets or sets a value indicating whether or not row separators should be shown. + /// + public bool ShowRowSeparators { get; set; } + /// /// Gets or sets a value indicating whether or not table footers should be shown. /// diff --git a/src/Spectre.Console/Widgets/Table/TableRenderer.cs b/src/Spectre.Console/Widgets/Table/TableRenderer.cs index d27326e..23bff61 100644 --- a/src/Spectre.Console/Widgets/Table/TableRenderer.cs +++ b/src/Spectre.Console/Widgets/Table/TableRenderer.cs @@ -36,7 +36,9 @@ internal static class TableRenderer // Show top of header? if (isFirstRow && context.ShowBorder) { - var separator = Aligner.Align(context.Border.GetColumnRow(TablePart.Top, columnWidths, context.Columns), context.Alignment, context.MaxWidth); + var separator = Aligner.Align( + context.Border.GetColumnRow(TablePart.Top, columnWidths, context.Columns), + context.Alignment, context.MaxWidth); result.Add(new Segment(separator, context.BorderStyle)); result.Add(Segment.LineBreak); } @@ -66,7 +68,9 @@ internal static class TableRenderer if (isFirstCell && context.ShowBorder) { // Show left column edge - var part = isFirstRow && context.ShowHeaders ? TableBorderPart.HeaderLeft : TableBorderPart.CellLeft; + var part = isFirstRow && context.ShowHeaders + ? TableBorderPart.HeaderLeft + : TableBorderPart.CellLeft; rowResult.Add(new Segment(context.Border.GetPart(part), context.BorderStyle)); } @@ -91,7 +95,8 @@ internal static class TableRenderer } // Pad column on the right side - if (context.ShowBorder || (context.HideBorder && !isLastCell) || (context.HideBorder && isLastCell && context.IsGrid && context.PadRightCell)) + if (context.ShowBorder || (context.HideBorder && !isLastCell) || + (context.HideBorder && isLastCell && context.IsGrid && context.PadRightCell)) { var rightPadding = context.Columns[cellIndex].Padding.GetRightSafe(); if (rightPadding > 0) @@ -103,13 +108,17 @@ internal static class TableRenderer if (isLastCell && context.ShowBorder) { // Add right column edge - var part = isFirstRow && context.ShowHeaders ? TableBorderPart.HeaderRight : TableBorderPart.CellRight; + var part = isFirstRow && context.ShowHeaders + ? TableBorderPart.HeaderRight + : TableBorderPart.CellRight; rowResult.Add(new Segment(context.Border.GetPart(part), context.BorderStyle)); } else if (context.ShowBorder) { // Add column separator - var part = isFirstRow && context.ShowHeaders ? TableBorderPart.HeaderSeparator : TableBorderPart.CellSeparator; + var part = isFirstRow && context.ShowHeaders + ? TableBorderPart.HeaderSeparator + : TableBorderPart.CellSeparator; rowResult.Add(new Segment(context.Border.GetPart(part), context.BorderStyle)); } } @@ -133,15 +142,40 @@ internal static class TableRenderer // Show header separator? if (isFirstRow && context.ShowBorder && context.ShowHeaders && context.HasRows) { - var separator = Aligner.Align(context.Border.GetColumnRow(TablePart.HeaderSeparator, columnWidths, context.Columns), context.Alignment, context.MaxWidth); + var separator = + Aligner.Align( + context.Border.GetColumnRow(TablePart.HeaderSeparator, columnWidths, context.Columns), + context.Alignment, context.MaxWidth); result.Add(new Segment(separator, context.BorderStyle)); result.Add(Segment.LineBreak); } + // Show row separator? + if (context.Border.SupportsRowSeparator && context.ShowRowSeparators + && !isFirstRow && !isLastRow) + { + var hasVisibleFootes = context is { ShowFooters: true, HasFooters: true }; + var isNextLastLine = index == context.Rows.Count - 2; + + var isRenderingFooter = hasVisibleFootes && isNextLastLine; + if (!isRenderingFooter) + { + var separator = + Aligner.Align( + context.Border.GetColumnRow(TablePart.RowSeparator, columnWidths, context.Columns), + context.Alignment, context.MaxWidth); + result.Add(new Segment(separator, context.BorderStyle)); + result.Add(Segment.LineBreak); + } + } + // Show bottom of footer? if (isLastRow && context.ShowBorder) { - var separator = Aligner.Align(context.Border.GetColumnRow(TablePart.Bottom, columnWidths, context.Columns), context.Alignment, context.MaxWidth); + var separator = + Aligner.Align( + context.Border.GetColumnRow(TablePart.Bottom, columnWidths, context.Columns), + context.Alignment, context.MaxWidth); result.Add(new Segment(separator, context.BorderStyle)); result.Add(Segment.LineBreak); } @@ -151,7 +185,8 @@ internal static class TableRenderer return result; } - private static IEnumerable RenderAnnotation(TableRendererContext context, TableTitle? header, Style defaultStyle) + private static IEnumerable RenderAnnotation(TableRendererContext context, TableTitle? header, + Style defaultStyle) { if (header == null) { diff --git a/src/Spectre.Console/Widgets/Table/TableRendererContext.cs b/src/Spectre.Console/Widgets/Table/TableRendererContext.cs index b549802..ad9b709 100644 --- a/src/Spectre.Console/Widgets/Table/TableRendererContext.cs +++ b/src/Spectre.Console/Widgets/Table/TableRendererContext.cs @@ -10,6 +10,7 @@ internal sealed class TableRendererContext : TableAccessor public TableBorder Border { get; } public Style BorderStyle { get; } public bool ShowBorder { get; } + public bool ShowRowSeparators { get; } public bool HasRows { get; } public bool HasFooters { get; } @@ -47,6 +48,7 @@ internal sealed class TableRendererContext : TableAccessor HasFooters = Rows.Any(column => column.IsFooter); Border = table.Border.GetSafeBorder(!options.Unicode && table.UseSafeBorder); BorderStyle = table.BorderStyle ?? Style.Plain; + ShowRowSeparators = table.ShowRowSeparators; TableWidth = tableWidth; MaxWidth = maxWidth; diff --git a/test/Spectre.Console.Tests/Expectations/Rendering/Borders/Box/NoBorder.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Rendering/Borders/Box/NoBorder.Output.verified.txt index 0d3e931..a254250 100644 --- a/test/Spectre.Console.Tests/Expectations/Rendering/Borders/Box/NoBorder.Output.verified.txt +++ b/test/Spectre.Console.Tests/Expectations/Rendering/Borders/Box/NoBorder.Output.verified.txt @@ -1 +1 @@ - Hello World + Hello World diff --git a/test/Spectre.Console.Tests/Expectations/Rendering/Borders/Box/NoBorder_With_Header.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Rendering/Borders/Box/NoBorder_With_Header.Output.verified.txt new file mode 100644 index 0000000..2ad34ae --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Rendering/Borders/Box/NoBorder_With_Header.Output.verified.txt @@ -0,0 +1,2 @@ + Greeting + Hello World diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_Row_Separators.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_Row_Separators.Output.verified.txt new file mode 100644 index 0000000..02d8f0d --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_Row_Separators.Output.verified.txt @@ -0,0 +1,7 @@ +┌────────┬────────┬───────┐ +│ Foo │ Bar │ Baz │ +├────────┼────────┼───────┤ +│ Qux │ Corgi │ Waldo │ +├────────┼────────┼───────┤ +│ Grault │ Garply │ Fred │ +└────────┴────────┴───────┘ diff --git a/test/Spectre.Console.Tests/Unit/Rendering/Borders/BoxBorderTests.cs b/test/Spectre.Console.Tests/Unit/Rendering/Borders/BoxBorderTests.cs index f16fe97..d9643cb 100644 --- a/test/Spectre.Console.Tests/Unit/Rendering/Borders/BoxBorderTests.cs +++ b/test/Spectre.Console.Tests/Unit/Rendering/Borders/BoxBorderTests.cs @@ -23,6 +23,22 @@ public sealed class BoxBorderTests [Fact] [Expectation("NoBorder")] public Task Should_Render_As_Expected() + { + // Given + var console = new TestConsole(); + var panel = Fixture.GetPanel().NoBorder(); + panel.Header = null; + + // When + console.Write(panel); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("NoBorder_With_Header")] + public Task Should_Render_NoBorder_With_Header_As_Expected() { // Given var console = new TestConsole(); diff --git a/test/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs index a8fb908..8a1ce1d 100644 --- a/test/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs +++ b/test/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs @@ -142,6 +142,25 @@ public sealed class TableTests return Verifier.Verify(console.Output); } + [Fact] + [Expectation("Render_Row_Separators")] + public Task Should_Render_Table_With_Row_Separators_Correctly() + { + // Given + var console = new TestConsole(); + var table = new Table(); + table.ShowRowSeparators(); + table.AddColumns("Foo", "Bar", "Baz"); + table.AddRow("Qux", "Corgi", "Waldo"); + table.AddRow("Grault", "Garply", "Fred"); + + // When + console.Write(table); + + // Then + return Verifier.Verify(console.Output); + } + [Fact] [Expectation("Render_EA_Character")] public Task Should_Render_Table_With_EA_Character_Correctly()