diff --git a/examples/Tables/Program.cs b/examples/Tables/Program.cs index 495b21a..539d673 100644 --- a/examples/Tables/Program.cs +++ b/examples/Tables/Program.cs @@ -19,7 +19,7 @@ namespace TableExample private static void RenderSimpleTable() { - // Create the table. + // Create the table var table = new Table(); table.AddColumn(new TableColumn("[u]Foo[/]")); table.AddColumn(new TableColumn("[u]Bar[/]")); @@ -35,7 +35,7 @@ namespace TableExample private static void RenderBigTable() { - // Create the table. + // Create the table var table = new Table().SetBorder(TableBorder.Rounded); table.AddColumn("[red underline]Foo[/]"); table.AddColumn(new TableColumn("[blue]Bar[/]") { Alignment = Justify.Right, NoWrap = true }); @@ -57,16 +57,16 @@ namespace TableExample private static void RenderNestedTable() { - // Create simple table. + // Create simple table var simple = new Table().SetBorder(TableBorder.Rounded).SetBorderColor(Color.Red); - simple.AddColumn(new TableColumn("[u]Foo[/]").Centered()); - simple.AddColumn(new TableColumn("[u]Bar[/]")); - simple.AddColumn(new TableColumn("[u]Baz[/]")); + simple.AddColumn(new TableColumn("[u]CDE[/]").Centered()); + simple.AddColumn(new TableColumn("[u]FED[/]")); + simple.AddColumn(new TableColumn("[u]IHG[/]")); simple.AddRow("Hello", "[red]World![/]", ""); simple.AddRow("[blue]Bonjour[/]", "[white]le[/]", "[red]monde![/]"); simple.AddRow("[blue]Hej[/]", "[yellow]Världen![/]", ""); - // Create other table. + // Create other table var second = new Table().SetBorder(TableBorder.Square).SetBorderColor(Color.Green); second.AddColumn(new TableColumn("[u]Foo[/]")); second.AddColumn(new TableColumn("[u]Bar[/]")); @@ -75,12 +75,11 @@ namespace TableExample second.AddRow(simple, new Text("Whaaat"), new Text("Lolz")); second.AddRow("[blue]Hej[/]", "[yellow]Världen![/]", ""); + // Create the outer most table var table = new Table().SetBorder(TableBorder.Rounded); - table.AddColumn(new TableColumn(new Panel("[u]Foo[/]").SetBorderColor(Color.Red))); - table.AddColumn(new TableColumn(new Panel("[u]Bar[/]").SetBorderColor(Color.Green))); - table.AddColumn(new TableColumn(new Panel("[u]Baz[/]").SetBorderColor(Color.Blue))); - - // Add some rows + table.AddColumn(new TableColumn(new Panel("[u]ABC[/]").SetBorderColor(Color.Red))); + table.AddColumn(new TableColumn(new Panel("[u]DEF[/]").SetBorderColor(Color.Green))); + table.AddColumn(new TableColumn(new Panel("[u]GHI[/]").SetBorderColor(Color.Blue))); table.AddRow(new Text("Hello").Centered(), new Markup("[red]World![/]"), Text.Empty); table.AddRow(second, new Text("Whaaat"), new Text("Lol")); table.AddRow(new Markup("[blue]Hej[/]").Centered(), new Markup("[yellow]Världen![/]"), Text.Empty); diff --git a/src/Spectre.Console.Tests/Unit/TableTests.cs b/src/Spectre.Console.Tests/Unit/TableTests.cs index fa3cca2..662c316 100644 --- a/src/Spectre.Console.Tests/Unit/TableTests.cs +++ b/src/Spectre.Console.Tests/Unit/TableTests.cs @@ -383,5 +383,54 @@ namespace Spectre.Console.Tests.Unit console.Lines[1].ShouldBe("│ Foo │ Bar │ Baz │"); console.Lines[2].ShouldBe("└─────┴─────┴────────┘"); } + + [Fact] + public void Should_Not_Draw_Tables_That_Are_Impossible_To_Draw() + { + // Given + var console = new PlainConsole(width: 25); + + var first = new Table().SetBorder(TableBorder.Rounded).SetBorderColor(Color.Red); + first.AddColumn(new TableColumn("[u]PS1[/]").Centered()); + first.AddColumn(new TableColumn("[u]PS2[/]")); + first.AddColumn(new TableColumn("[u]PS3[/]")); + first.AddRow("Hello", "[red]World[/]", string.Empty); + first.AddRow("[blue]Bonjour[/]", "[white]le[/]", "[red]monde![/]"); + first.AddRow("[blue]Hej[/]", "[yellow]Världen[/]", string.Empty); + + var second = new Table().SetBorder(TableBorder.Square).SetBorderColor(Color.Green); + second.AddColumn(new TableColumn("[u]Foo[/]")); + second.AddColumn(new TableColumn("[u]Bar[/]")); + second.AddColumn(new TableColumn("[u]Baz[/]")); + second.AddRow("Hello", "[red]World[/]", string.Empty); + second.AddRow(first, new Text("Whaaat"), new Text("Lolz")); + second.AddRow("[blue]Hej[/]", "[yellow]Världen[/]", string.Empty); + + var table = new Table().SetBorder(TableBorder.Rounded); + table.AddColumn(new TableColumn(new Panel("[u]ABC[/]").SetBorderColor(Color.Red))); + table.AddColumn(new TableColumn(new Panel("[u]DEF[/]").SetBorderColor(Color.Green))); + table.AddColumn(new TableColumn(new Panel("[u]GHI[/]").SetBorderColor(Color.Blue))); + table.AddRow(new Text("Hello").Centered(), new Markup("[red]World[/]"), Text.Empty); + table.AddRow(second, new Text("Whaat"), new Text("Lol").RightAligned()); + table.AddRow(new Markup("[blue]Hej[/]"), new Markup("[yellow]Världen[/]"), Text.Empty); + + // When + console.Render(table); + + // Then + console.Lines.Count.ShouldBe(12); + console.Lines[00].ShouldBe("╭───────┬───────┬───────╮"); + console.Lines[01].ShouldBe("│ ┌───┐ │ ┌───┐ │ ┌───┐ │"); + console.Lines[02].ShouldBe("│ │ A │ │ │ D │ │ │ G │ │"); + console.Lines[03].ShouldBe("│ │ B │ │ │ E │ │ │ H │ │"); + console.Lines[04].ShouldBe("│ │ C │ │ │ F │ │ │ I │ │"); + console.Lines[05].ShouldBe("│ └───┘ │ └───┘ │ └───┘ │"); + console.Lines[06].ShouldBe("├───────┼───────┼───────┤"); + console.Lines[07].ShouldBe("│ Hello │ World │ │"); + console.Lines[08].ShouldBe("│ … │ Whaat │ Lol │"); + console.Lines[09].ShouldBe("│ Hej │ Värld │ │"); + console.Lines[10].ShouldBe("│ │ en │ │"); + console.Lines[11].ShouldBe("╰───────┴───────┴───────╯"); + } } } diff --git a/src/Spectre.Console/Rendering/Segment.cs b/src/Spectre.Console/Rendering/Segment.cs index aa74d5e..0bcd925 100644 --- a/src/Spectre.Console/Rendering/Segment.cs +++ b/src/Spectre.Console/Rendering/Segment.cs @@ -119,6 +119,54 @@ namespace Spectre.Console.Rendering new Segment(Text.Substring(offset, Text.Length - offset), Style)); } + /// + /// Gets the number of cells that the segments occupies in the console. + /// + /// The render context. + /// The segments to measure. + /// The number of cells that the segments occupies in the console. + public static int CellLength(RenderContext context, List segments) + { + return segments.Sum(segment => segment.CellLength(context)); + } + + /// + /// Truncates the segments to the specified width. + /// + /// The render context. + /// The segments to truncate. + /// The maximum width that the segments may occupy. + /// A list of segments that has been truncated. + public static List Truncate(RenderContext context, IEnumerable segments, int maxWidth) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (segments is null) + { + throw new ArgumentNullException(nameof(segments)); + } + + var result = new List(); + + var totalWidth = 0; + foreach (var segment in segments) + { + var segmentWidth = segment.CellLength(context); + if (totalWidth + segmentWidth > maxWidth) + { + break; + } + + result.Add(segment); + totalWidth += segmentWidth; + } + + return result; + } + /// /// Splits the provided segments into lines. /// @@ -315,11 +363,25 @@ namespace Spectre.Console.Rendering } else if (overflow == Overflow.Crop) { - result.Add(new Segment(segment.Text.Substring(0, width), segment.Style)); + if (Math.Max(0, width - 1) == 0) + { + result.Add(new Segment(string.Empty, segment.Style)); + } + else + { + result.Add(new Segment(segment.Text.Substring(0, width), segment.Style)); + } } else if (overflow == Overflow.Ellipsis) { - result.Add(new Segment(segment.Text.Substring(0, width - 1) + "…", segment.Style)); + if (Math.Max(0, width - 1) == 0) + { + result.Add(new Segment("…", segment.Style)); + } + else + { + result.Add(new Segment(segment.Text.Substring(0, width - 1) + "…", segment.Style)); + } } return result; diff --git a/src/Spectre.Console/Widgets/Paragraph.cs b/src/Spectre.Console/Widgets/Paragraph.cs index de4bbf9..f8efbbd 100644 --- a/src/Spectre.Console/Widgets/Paragraph.cs +++ b/src/Spectre.Console/Widgets/Paragraph.cs @@ -193,6 +193,12 @@ namespace Spectre.Console private List SplitLines(RenderContext context, int maxWidth) { + if (maxWidth <= 0) + { + // Nothing fits, so return an empty line. + return new List(); + } + if (_lines.Max(x => x.CellWidth(context)) <= maxWidth) { return Clone(); diff --git a/src/Spectre.Console/Widgets/Table.cs b/src/Spectre.Console/Widgets/Table.cs index 673e4fe..b7291c0 100644 --- a/src/Spectre.Console/Widgets/Table.cs +++ b/src/Spectre.Console/Widgets/Table.cs @@ -153,6 +153,7 @@ namespace Spectre.Console var borderStyle = BorderStyle ?? Style.Plain; var tableWidth = maxWidth; + var actualMaxWidth = maxWidth; var showBorder = Border.Visible; var hideBorder = !Border.Visible; @@ -170,6 +171,10 @@ namespace Spectre.Console // Update the table width. tableWidth = columnWidths.Sum() + GetExtraWidth(includePadding: true); + if (tableWidth <= 0 || tableWidth > actualMaxWidth || columnWidths.Any(c => c <= 0)) + { + return new List(new[] { new Segment("…", BorderStyle ?? Style.Plain) }); + } var rows = new List>(); if (ShowHeaders) @@ -213,13 +218,15 @@ namespace Spectre.Console // Iterate through each cell row foreach (var cellRowIndex in Enumerable.Range(0, cellHeight)) { + var rowResult = new List(); + foreach (var (cellIndex, firstCell, lastCell, cell) in cells.Enumerate()) { if (firstCell && showBorder) { // Show left column edge var part = firstRow && ShowHeaders ? TableBorderPart.HeaderLeft : TableBorderPart.CellLeft; - result.Add(new Segment(border.GetPart(part), borderStyle)); + rowResult.Add(new Segment(border.GetPart(part), borderStyle)); } // Pad column on left side. @@ -228,18 +235,18 @@ namespace Spectre.Console var leftPadding = _columns[cellIndex].Padding.Left; if (leftPadding > 0) { - result.Add(new Segment(new string(' ', leftPadding))); + rowResult.Add(new Segment(new string(' ', leftPadding))); } } // Add content - result.AddRange(cell[cellRowIndex]); + rowResult.AddRange(cell[cellRowIndex]); // Pad cell content right var length = cell[cellRowIndex].Sum(segment => segment.CellLength(context)); if (length < columnWidths[cellIndex]) { - result.Add(new Segment(new string(' ', columnWidths[cellIndex] - length))); + rowResult.Add(new Segment(new string(' ', columnWidths[cellIndex] - length))); } // Pad column on the right side @@ -248,7 +255,7 @@ namespace Spectre.Console var rightPadding = _columns[cellIndex].Padding.Right; if (rightPadding > 0) { - result.Add(new Segment(new string(' ', rightPadding))); + rowResult.Add(new Segment(new string(' ', rightPadding))); } } @@ -256,16 +263,26 @@ namespace Spectre.Console { // Add right column edge var part = firstRow && ShowHeaders ? TableBorderPart.HeaderRight : TableBorderPart.CellRight; - result.Add(new Segment(border.GetPart(part), borderStyle)); + rowResult.Add(new Segment(border.GetPart(part), borderStyle)); } else if (showBorder) { // Add column separator var part = firstRow && ShowHeaders ? TableBorderPart.HeaderSeparator : TableBorderPart.CellSeparator; - result.Add(new Segment(border.GetPart(part), borderStyle)); + rowResult.Add(new Segment(border.GetPart(part), borderStyle)); } } + // Is the row larger than the allowed max width? + if (Segment.CellLength(context, rowResult) > actualMaxWidth) + { + result.AddRange(Segment.Truncate(context, rowResult, actualMaxWidth)); + } + else + { + result.AddRange(rowResult); + } + result.Add(Segment.LineBreak); } diff --git a/src/Spectre.Console/Widgets/TableColumn.cs b/src/Spectre.Console/Widgets/TableColumn.cs index 30dbe0f..29b045f 100644 --- a/src/Spectre.Console/Widgets/TableColumn.cs +++ b/src/Spectre.Console/Widgets/TableColumn.cs @@ -41,7 +41,7 @@ namespace Spectre.Console /// /// The table column text. public TableColumn(string text) - : this(new Markup(text)) + : this(new Markup(text).SetOverflow(Overflow.Ellipsis)) { }