diff --git a/src/Sample/Program.cs b/src/Sample/Program.cs index d875cf0..09f99df 100644 --- a/src/Sample/Program.cs +++ b/src/Sample/Program.cs @@ -17,23 +17,18 @@ namespace Sample AnsiConsole.MarkupLine("[white on red]Good[/] [red]bye[/]!"); AnsiConsole.WriteLine(); - // We can also use System.ConsoleColor with AnsiConsole. - // To set the Foreground color + // We can also use System.ConsoleColor with AnsiConsole + // to set the foreground and background color. foreach (ConsoleColor value in Enum.GetValues(typeof(ConsoleColor))) { - AnsiConsole.Foreground = value; - AnsiConsole.WriteLine("Foreground: ConsoleColor.{0}", value); - } + var foreground = value; + var background = (ConsoleColor)(15 - (int)value); - AnsiConsole.WriteLine(); - AnsiConsole.Foreground = Color.Chartreuse2; - // As well as the background color - foreach (ConsoleColor value in Enum.GetValues(typeof(ConsoleColor))) - { - AnsiConsole.Background = value; - AnsiConsole.WriteLine("Background: ConsoleColor.{0}", value); + AnsiConsole.Foreground = foreground; + AnsiConsole.Background = background; + AnsiConsole.WriteLine("{0} on {1}", foreground, background); + AnsiConsole.ResetColors(); } - AnsiConsole.Reset(); // We can get the default console via the static API. var console = AnsiConsole.Console; @@ -96,6 +91,7 @@ namespace Sample foreground: Color.White), fit: true, content: Justify.Right)); + // A normal, square table var table = new Table(); table.AddColumns("[red underline]Foo[/]", "Bar"); table.AddRow("[blue][underline]Hell[/]o[/]", "World ๐ŸŒ"); @@ -104,7 +100,8 @@ namespace Sample table.AddRow("Hej ๐Ÿ‘‹", "[green]Vรคrlden[/]"); AnsiConsole.Render(table); - table = new Table(BorderKind.Rounded); + // A rounded table + table = new Table { Border = BorderKind.Rounded }; table.AddColumns("[red underline]Foo[/]", "Bar"); table.AddRow("[blue][underline]Hell[/]o[/]", "World ๐ŸŒ"); table.AddRow("[yellow]Patrik [green]\"Lol[/]\" Svensson[/]", "Was [underline]here[/]!"); @@ -112,16 +109,52 @@ namespace Sample table.AddRow("Hej ๐Ÿ‘‹", "[green]Vรคrlden[/]"); AnsiConsole.Render(table); + // A rounded table without headers + table = new Table { Border = BorderKind.Rounded, ShowHeaders = false }; + table.AddColumns("[red underline]Foo[/]", "Bar"); + table.AddRow("[blue][underline]Hell[/]o[/]", "World ๐ŸŒ"); + table.AddRow("[yellow]Patrik [green]\"Lol[/]\" Svensson[/]", "Was [underline]here[/]!"); + table.AddRow("Lorem ipsum dolor sit amet, consectetur [blue]adipiscing[/] elit,sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum", "โ—€ Strange language"); + table.AddRow("Hej ๐Ÿ‘‹", "[green]Vรคrlden[/]"); + AnsiConsole.Render(table); + + // Emulate the usage information for "dotnet run" AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine(" Usage: [grey]dotnet [blue]run[/] [[options] [[[[--] ...]][/]"); + AnsiConsole.MarkupLine("Usage: [grey]dotnet [blue]run[/] [[options] [[[[--] ...]][/]"); AnsiConsole.WriteLine(); var grid = new Grid(); - grid.AddColumns(3); - grid.AddRow(" Options", "", ""); - grid.AddRow(" [blue]-h[/], [blue]--help[/]", " ", "Show command line help."); - grid.AddRow(" [blue]-c[/], [blue]--configuration[/] ", " ", "The configuration to run for.\nThe default for most projects is [green]Debug[/]."); - grid.AddRow(" [blue]-v[/], [blue]--verbosity[/] ", " ", "Set the MSBuild verbosity level.\nAllowed values are q[grey][[uiet][/], m[grey][[inimal][/], n[grey][[ormal][/], d[grey][[etailed][/], and diag[grey][[nostic][/]."); + grid.AddColumn(new GridColumn { NoWrap = true }); + grid.AddColumn(new GridColumn { NoWrap = true, Width = 2 }); + grid.AddColumn(); + grid.AddRow("Options:", "", ""); + grid.AddRow(" [blue]-h[/], [blue]--help[/]", "", "Show command line help."); + grid.AddRow(" [blue]-c[/], [blue]--configuration[/] ", "", "The configuration to run for.\nThe default for most projects is [green]Debug[/]."); + grid.AddRow(" [blue]-v[/], [blue]--verbosity[/] ", "", "Set the MSBuild verbosity level. Allowed values are \nq[grey][[uiet][/], m[grey][[inimal][/], n[grey][[ormal][/], d[grey][[etailed][/], and diag[grey][[nostic][/]."); AnsiConsole.Render(grid); + + // A simple table + AnsiConsole.WriteLine(); + table = new Table { Border = BorderKind.Rounded }; + table.AddColumn("Foo"); + table.AddColumn("Bar"); + table.AddColumn("Baz"); + table.AddRow("Qux\nQuuuuuux", "[blue]Corgi[/]", "Waldo"); + table.AddRow("Grault", "Garply", "Fred"); + AnsiConsole.Render(table); + + // Render a table in some panels. + AnsiConsole.Render(new Panel(new Panel(table, border: BorderKind.Ascii))); + + // Draw another table + table = new Table { Expand = false }; + table.AddColumn(new TableColumn("Date")); + table.AddColumn(new TableColumn("Title")); + table.AddColumn(new TableColumn("Production\nBudget")); + table.AddColumn(new TableColumn("Box Office")); + table.AddRow("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$275,000,000", "[red]$375,126,118[/]"); + table.AddRow("May 25, 2018", "[yellow]Solo[/]: A Star Wars Story", "$275,000,000", "$393,151,347"); + table.AddRow("Dec 15, 2017", "Star Wars Ep. VIII: The Last Jedi", "$262,000,000", "[bold green]$1,332,539,889[/]"); + AnsiConsole.Render(table); } } } \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs index b1505a5..7416698 100644 --- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs +++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs @@ -246,6 +246,23 @@ namespace Spectre.Console.Tests.Unit public sealed class WriteLine { + [Fact] + public void Should_Reset_Colors_Correctly() + { + // Given + var fixture = new AnsiConsoleFixture(ColorSystem.Standard, AnsiSupport.Yes); + + // When + fixture.Console.Background = ConsoleColor.Red; + fixture.Console.WriteLine("Hello"); + fixture.Console.Background = ConsoleColor.Green; + fixture.Console.WriteLine("World"); + + // Then + fixture.Output.NormalizeLineEndings() + .ShouldBe("Hello\nWorld\n"); + } + [Theory] [InlineData(AnsiSupport.Yes)] [InlineData(AnsiSupport.No)] diff --git a/src/Spectre.Console.Tests/Unit/Composition/GridTests.cs b/src/Spectre.Console.Tests/Unit/Composition/GridTests.cs index 036aa14..de80d31 100644 --- a/src/Spectre.Console.Tests/Unit/Composition/GridTests.cs +++ b/src/Spectre.Console.Tests/Unit/Composition/GridTests.cs @@ -27,7 +27,8 @@ namespace Spectre.Console.Tests.Unit.Composition { // Given var grid = new Grid(); - grid.AddColumns(2); + grid.AddColumn(); + grid.AddColumn(); // When var result = Record.Exception(() => grid.AddRow("Foo")); @@ -59,7 +60,9 @@ namespace Spectre.Console.Tests.Unit.Composition // Given var console = new PlainConsole(width: 80); var grid = new Grid(); - grid.AddColumns(3); + grid.AddColumn(); + grid.AddColumn(); + grid.AddColumn(); grid.AddRow("Qux", "Corgi", "Waldo"); grid.AddRow("Grault", "Garply", "Fred"); @@ -75,22 +78,23 @@ namespace Spectre.Console.Tests.Unit.Composition [Fact] public void Should_Render_Grid() { - var console = new PlainConsole(width: 120); + var console = new PlainConsole(width: 80); var grid = new Grid(); - grid.AddColumns(3); - grid.AddRow("[bold]Options[/]", string.Empty, string.Empty); - grid.AddRow(" [blue]-h[/], [blue]--help[/]", " ", "Show command line help."); - grid.AddRow(" [blue]-c[/], [blue]--configuration[/]", " ", "The configuration to run for.\nThe default for most projects is [green]Debug[/]."); + grid.AddColumn(new GridColumn { NoWrap = true }); + grid.AddColumn(); + grid.AddRow("[bold]Options[/]", string.Empty); + grid.AddRow(" [blue]-h[/], [blue]--help[/]", "Show command line help."); + grid.AddRow(" [blue]-c[/], [blue]--configuration[/]", "The configuration to run for.\nThe default for most projects is [green]Debug[/]."); // When console.Render(grid); // Then console.Lines.Count.ShouldBe(4); - console.Lines[0].ShouldBe("Options "); - console.Lines[1].ShouldBe(" -h, --help Show command line help. "); - console.Lines[2].ShouldBe(" -c, --configuration The configuration to run for. "); - console.Lines[3].ShouldBe(" The default for most projects is Debug."); + console.Lines[0].ShouldBe("Options "); + console.Lines[1].ShouldBe(" -h, --help Show command line help. "); + console.Lines[2].ShouldBe(" -c, --configuration The configuration to run for. "); + console.Lines[3].ShouldBe(" The default for most projects is Debug. "); } } } diff --git a/src/Spectre.Console.Tests/Unit/Composition/TableTests.cs b/src/Spectre.Console.Tests/Unit/Composition/TableTests.cs index 58aed3f..4755e41 100644 --- a/src/Spectre.Console.Tests/Unit/Composition/TableTests.cs +++ b/src/Spectre.Console.Tests/Unit/Composition/TableTests.cs @@ -15,7 +15,7 @@ namespace Spectre.Console.Tests.Unit.Composition var table = new Table(); // When - var result = Record.Exception(() => table.AddColumn(null)); + var result = Record.Exception(() => table.AddColumn((string)null)); // Then result.ShouldBeOfType() @@ -88,6 +88,31 @@ namespace Spectre.Console.Tests.Unit.Composition } } + [Fact] + public void Should_Measure_Table_Correctly() + { + // Given + var console = new PlainConsole(width: 80); + var table = new Table(); + table.AddColumns("Foo", "Bar", "Baz"); + table.AddRow("Qux", "Corgi", "Waldo"); + table.AddRow("Grault", "Garply", "Fred"); + + // When + console.Render(new Panel(table)); + + // Then + console.Lines.Count.ShouldBe(8); + console.Lines[0].ShouldBe("โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”"); + 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("โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"); + } + [Fact] public void Should_Render_Table_Correctly() { @@ -102,6 +127,7 @@ namespace Spectre.Console.Tests.Unit.Composition console.Render(table); // Then + console.Lines.Count.ShouldBe(6); console.Lines[0].ShouldBe("โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”"); console.Lines[1].ShouldBe("โ”‚ Foo โ”‚ Bar โ”‚ Baz โ”‚"); console.Lines[2].ShouldBe("โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค"); @@ -111,11 +137,11 @@ namespace Spectre.Console.Tests.Unit.Composition } [Fact] - public void Should_Render_Table_With_Ascii_Border_Correctly() + public void Should_Expand_Table_To_Available_Space_If_Specified() { // Given var console = new PlainConsole(width: 80); - var table = new Table(BorderKind.Ascii); + var table = new Table() { Expand = true }; table.AddColumns("Foo", "Bar", "Baz"); table.AddRow("Qux", "Corgi", "Waldo"); table.AddRow("Grault", "Garply", "Fred"); @@ -124,6 +150,31 @@ namespace Spectre.Console.Tests.Unit.Composition console.Render(table); // Then + console.Lines.Count.ShouldBe(6); + console.Lines[0].Length.ShouldBe(80); + 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_With_Ascii_Border_Correctly() + { + // Given + var console = new PlainConsole(width: 80); + var table = new Table { Border = BorderKind.Ascii }; + 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("|--------+--------+-------|"); @@ -137,7 +188,7 @@ namespace Spectre.Console.Tests.Unit.Composition { // Given var console = new PlainConsole(width: 80); - var table = new Table(BorderKind.Rounded); + var table = new Table { Border = BorderKind.Rounded }; table.AddColumns("Foo", "Bar", "Baz"); table.AddRow("Qux", "Corgi", "Waldo"); table.AddRow("Grault", "Garply", "Fred"); @@ -146,6 +197,7 @@ namespace Spectre.Console.Tests.Unit.Composition console.Render(table); // Then + console.Lines.Count.ShouldBe(6); console.Lines[0].ShouldBe("โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ"); console.Lines[1].ShouldBe("โ”‚ Foo โ”‚ Bar โ”‚ Baz โ”‚"); console.Lines[2].ShouldBe("โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค"); @@ -159,7 +211,7 @@ namespace Spectre.Console.Tests.Unit.Composition { // Given var console = new PlainConsole(width: 80); - var table = new Table(BorderKind.None); + var table = new Table { Border = BorderKind.None }; table.AddColumns("Foo", "Bar", "Baz"); table.AddRow("Qux", "Corgi", "Waldo"); table.AddRow("Grault", "Garply", "Fred"); @@ -188,6 +240,7 @@ namespace Spectre.Console.Tests.Unit.Composition console.Render(table); // Then + console.Lines.Count.ShouldBe(7); console.Lines[0].ShouldBe("โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”"); console.Lines[1].ShouldBe("โ”‚ Foo โ”‚ Bar โ”‚ Baz โ”‚"); console.Lines[2].ShouldBe("โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค"); diff --git a/src/Spectre.Console/Composition/Grid.cs b/src/Spectre.Console/Composition/Grid.cs index a3f1a93..590755b 100644 --- a/src/Spectre.Console/Composition/Grid.cs +++ b/src/Spectre.Console/Composition/Grid.cs @@ -17,23 +17,27 @@ namespace Spectre.Console /// public Grid() { - _table = new Table(BorderKind.None, showHeaders: false); + _table = new Table + { + Border = BorderKind.None, + ShowHeaders = false, + }; } /// - public int Measure(Encoding encoding, int maxWidth) + public Measurement Measure(Encoding encoding, int maxWidth) { - return _table.Measure(encoding, maxWidth); + return ((IRenderable)_table).Measure(encoding, maxWidth); } /// public IEnumerable Render(Encoding encoding, int width) { - return _table.Render(encoding, width); + return ((IRenderable)_table).Render(encoding, width); } /// - /// Adds a single column to the grid. + /// Adds a column to the grid. /// public void AddColumn() { @@ -41,15 +45,23 @@ namespace Spectre.Console } /// - /// Adds the specified number of columns to the grid. + /// Adds a column to the grid. /// - /// The number of columns. - public void AddColumns(int count) + /// The column to add. + public void AddColumn(GridColumn column) { - for (var i = 0; i < count; i++) + if (column is null) { - _table.AddColumn(string.Empty); + throw new ArgumentNullException(nameof(column)); } + + _table.AddColumn(new TableColumn(string.Empty) + { + Width = column.Width, + NoWrap = column.NoWrap, + LeftPadding = 0, + RightPadding = 1, + }); } /// diff --git a/src/Spectre.Console/Composition/GridColumn.cs b/src/Spectre.Console/Composition/GridColumn.cs new file mode 100644 index 0000000..0783c24 --- /dev/null +++ b/src/Spectre.Console/Composition/GridColumn.cs @@ -0,0 +1,20 @@ +๏ปฟnamespace Spectre.Console +{ + /// + /// Represents a grid column. + /// + public sealed class GridColumn + { + /// + /// Gets or sets the width of the column. + /// If null, the column will adapt to it's contents. + /// + public int? Width { get; set; } + + /// + /// Gets or sets a value indicating whether wrapping of + /// text within the column should be prevented. + /// + public bool NoWrap { get; set; } + } +} diff --git a/src/Spectre.Console/Composition/IRenderable.cs b/src/Spectre.Console/Composition/IRenderable.cs index 464f7e8..545c1cb 100644 --- a/src/Spectre.Console/Composition/IRenderable.cs +++ b/src/Spectre.Console/Composition/IRenderable.cs @@ -13,8 +13,8 @@ namespace Spectre.Console.Composition /// /// The encoding to use. /// The maximum allowed width. - /// The width of the object. - int Measure(Encoding encoding, int maxWidth); + /// The minimum and maximum width of the object. + Measurement Measure(Encoding encoding, int maxWidth); /// /// Renders the object. diff --git a/src/Spectre.Console/Composition/Measurement.cs b/src/Spectre.Console/Composition/Measurement.cs new file mode 100644 index 0000000..0d67bb2 --- /dev/null +++ b/src/Spectre.Console/Composition/Measurement.cs @@ -0,0 +1,77 @@ +using System; + +namespace Spectre.Console.Composition +{ + /// + /// Represents a measurement. + /// + public struct Measurement : IEquatable + { + /// + /// Gets the minimum width. + /// + public int Min { get; } + + /// + /// Gets the maximum width. + /// + public int Max { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// The minimum width. + /// The maximum width. + public Measurement(int min, int max) + { + Min = min; + Max = max; + } + + /// + public override bool Equals(object obj) + { + return obj is Measurement measurement && Equals(measurement); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hash = (int)2166136261; + hash = (hash * 16777619) ^ Min.GetHashCode(); + hash = (hash * 16777619) ^ Max.GetHashCode(); + return hash; + } + } + + /// + public bool Equals(Measurement other) + { + return Min == other.Min && Max == other.Max; + } + + /// + /// Checks if two instances are equal. + /// + /// The first measurement instance to compare. + /// The second measurement instance to compare. + /// true if the two measurements are equal, otherwise false. + public static bool operator ==(Measurement left, Measurement right) + { + return left.Equals(right); + } + + /// + /// Checks if two instances are not equal. + /// + /// The first measurement instance to compare. + /// The second measurement instance to compare. + /// true if the two measurements are not equal, otherwise false. + public static bool operator !=(Measurement left, Measurement right) + { + return !(left == right); + } + } +} diff --git a/src/Spectre.Console/Composition/Panel.cs b/src/Spectre.Console/Composition/Panel.cs index 9dab975..0041593 100644 --- a/src/Spectre.Console/Composition/Panel.cs +++ b/src/Spectre.Console/Composition/Panel.cs @@ -35,19 +35,20 @@ namespace Spectre.Console } /// - public int Measure(Encoding encoding, int maxWidth) + Measurement IRenderable.Measure(Encoding encoding, int maxWidth) { var childWidth = _child.Measure(encoding, maxWidth); - return childWidth + 4; + return new Measurement(childWidth.Min + 4, childWidth.Max + 4); } /// - public IEnumerable Render(Encoding encoding, int width) + IEnumerable IRenderable.Render(Encoding encoding, int width) { var childWidth = width - 4; if (!_fit) { - childWidth = _child.Measure(encoding, width - 2); + var measurement = _child.Measure(encoding, width - 2); + childWidth = measurement.Max; } var result = new List(); diff --git a/src/Spectre.Console/Composition/Segment.cs b/src/Spectre.Console/Composition/Segment.cs index bdc9f65..803f510 100644 --- a/src/Spectre.Console/Composition/Segment.cs +++ b/src/Spectre.Console/Composition/Segment.cs @@ -211,5 +211,21 @@ namespace Spectre.Console.Composition return lines; } + + internal static List> MakeSameHeight(int cellHeight, List> cells) + { + foreach (var cell in cells) + { + if (cell.Count < cellHeight) + { + while (cell.Count != cellHeight) + { + cell.Add(new SegmentLine()); + } + } + } + + return cells; + } } } diff --git a/src/Spectre.Console/Composition/Table.Calculations.cs b/src/Spectre.Console/Composition/Table.Calculations.cs new file mode 100644 index 0000000..f140cf3 --- /dev/null +++ b/src/Spectre.Console/Composition/Table.Calculations.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Spectre.Console.Composition; +using Spectre.Console.Internal; + +namespace Spectre.Console +{ + /// + /// Represents a table. + /// + public sealed partial class Table + { + // Calculate the widths of each column, including padding, not including borders. + // Ported from Rich by Will McGugan, licensed under MIT. + // https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/table.py#L394 + private List CalculateColumnWidths(Encoding encoding, int maxWidth) + { + var width_ranges = _columns.Select(column => MeasureColumn(column, encoding, maxWidth)); + var widths = width_ranges.Select(range => range.Max).ToList(); + + var tableWidth = widths.Sum(); + + if (ShouldExpand()) + { + var ratios = _columns.Select(c => c.Ratio ?? 0).ToList(); + if (ratios.Any(r => r != 0)) + { + var fixedWidths = new List(); + foreach (var (range, column) in width_ranges.Zip(_columns, (a, b) => (a, b))) + { + fixedWidths.Add(column.IsFlexible() ? 0 : range.Max); + } + + var flexMinimum = new List(); + foreach (var column in _columns) + { + if (column.IsFlexible()) + { + flexMinimum.Add(column.Width ?? 1 + column.GetPadding()); + } + else + { + flexMinimum.Add(0); + } + } + + var flexibleWidth = maxWidth - fixedWidths.Sum(); + var flexWidths = Ratio.Distribute(flexibleWidth, ratios, flexMinimum); + + var flexWidthsIterator = flexWidths.GetEnumerator(); + foreach (var (index, _, _, column) in _columns.Enumerate()) + { + if (column.IsFlexible()) + { + flexWidthsIterator.MoveNext(); + widths[index] = fixedWidths[index] + flexWidthsIterator.Current; + } + } + } + } + + tableWidth = widths.Sum(); + + if (tableWidth > maxWidth) + { + var wrappable = _columns.Select(c => !c.NoWrap).ToList(); + widths = CollapseWidths(widths, wrappable, maxWidth); + tableWidth = widths.Sum(); + + // last resort, reduce columns evenly + if (tableWidth > maxWidth) + { + var excessWidth = tableWidth - maxWidth; + widths = Ratio.Reduce(excessWidth, widths.Select(w => 1).ToList(), widths, widths); + tableWidth = widths.Sum(); + } + } + + if (tableWidth < maxWidth && ShouldExpand()) + { + var padWidths = Ratio.Distribute(maxWidth - tableWidth, widths); + widths = widths.Zip(padWidths, (a, b) => (a, b)).Select(f => f.a + f.b).ToList(); + } + + return widths; + } + + // Reduce widths so that the total is less or equal to the max width. + // Ported from Rich by Will McGugan, licensed under MIT. + // https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/table.py#L442 + private static List CollapseWidths(List widths, List wrappable, int maxWidth) + { + var totalWidth = widths.Sum(); + var excessWidth = totalWidth - maxWidth; + + if (wrappable.AnyTrue()) + { + while (totalWidth > 0 && excessWidth > 0) + { + var maxColumn = widths.Zip(wrappable, (first, second) => (width: first, isWrappable: second)) + .Where(x => x.isWrappable) + .Max(x => x.width); + + var secondMaxColumn = widths.Zip(wrappable, (width, isWrappable) => isWrappable && width != maxColumn ? maxColumn : 0).Max(); + var columnDifference = maxColumn - secondMaxColumn; + + var ratios = widths.Zip(wrappable, (width, allowWrap) => + { + if (width == maxColumn && allowWrap) + { + return 1; + } + + return 0; + }).ToList(); + + if (!ratios.Any(x => x > 0) || columnDifference == 0) + { + break; + } + + var maxReduce = widths.Select(_ => Math.Min(excessWidth, columnDifference)).ToList(); + widths = Ratio.Reduce(excessWidth, ratios, maxReduce, widths); + + totalWidth = widths.Sum(); + excessWidth = totalWidth - maxWidth; + } + } + + return widths; + } + + private (int Min, int Max) MeasureColumn(TableColumn column, Encoding encoding, int maxWidth) + { + // Predetermined width? + if (column.Width != null) + { + var padding = column.GetPadding(); + return (column.Width.Value + padding, column.Width.Value + padding); + } + + var columnIndex = _columns.IndexOf(column); + var rows = _rows.Select(row => row[columnIndex]); + + var minWidths = new List(); + var maxWidths = new List(); + foreach (var row in rows) + { + var measure = ((IRenderable)row).Measure(encoding, maxWidth); + minWidths.Add(measure.Min); + maxWidths.Add(measure.Max); + } + + return (minWidths.Count > 0 ? minWidths.Max() : 1, + maxWidths.Count > 0 ? maxWidths.Max() : maxWidth); + } + + private int GetExtraWidth(bool includePadding) + { + var edges = 2; + var separators = _columns.Count - 1; + var padding = includePadding ? _columns.Select(x => x.GetPadding()).Sum() : 0; + return separators + edges + padding; + } + } +} diff --git a/src/Spectre.Console/Composition/Table.cs b/src/Spectre.Console/Composition/Table.cs index f0efbcc..f7a2433 100644 --- a/src/Spectre.Console/Composition/Table.cs +++ b/src/Spectre.Console/Composition/Table.cs @@ -10,31 +10,50 @@ namespace Spectre.Console /// /// Represents a table. /// - public sealed class Table : IRenderable + public sealed partial class Table : IRenderable { - private readonly List _columns; + private readonly List _columns; private readonly List> _rows; - private readonly Border _border; - private readonly BorderKind _borderKind; - private readonly bool _showHeaders; /// /// Gets the number of columns in the table. /// public int ColumnCount => _columns.Count; + /// + /// Gets the number of rows in the table. + /// + public int RowCount => _rows.Count; + + /// + /// Gets or sets the kind of border to use. + /// + public BorderKind Border { get; set; } = BorderKind.Square; + + /// + /// Gets or sets a value indicating whether or not table headers should be shown. + /// + public bool ShowHeaders { get; set; } = true; + + /// + /// Gets or sets a value indicating whether or not the table should + /// fit the available space. If false, the table width will be + /// auto calculated. Defaults to false. + /// + public bool Expand { get; set; } = false; + + /// + /// Gets or sets the width of the table. + /// + public int? Width { get; set; } = null; + /// /// Initializes a new instance of the class. /// - /// The border to use. - /// Whether or not to show table headers. - public Table(BorderKind border = BorderKind.Square, bool showHeaders = true) + public Table() { - _columns = new List(); + _columns = new List(); _rows = new List>(); - _border = Border.GetBorder(border); - _borderKind = border; - _showHeaders = showHeaders; } /// @@ -48,7 +67,21 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(column)); } - _columns.Add(Text.New(column)); + _columns.Add(new TableColumn(column)); + } + + /// + /// Adds a column to the table. + /// + /// The column to add. + public void AddColumn(TableColumn column) + { + if (column is null) + { + throw new ArgumentNullException(nameof(column)); + } + + _columns.Add(column); } /// @@ -62,7 +95,7 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(columns)); } - _columns.AddRange(columns.Select(column => Text.New(column))); + _columns.AddRange(columns.Select(column => new TableColumn(column))); } /// @@ -90,66 +123,55 @@ namespace Spectre.Console } /// - public int Measure(Encoding encoding, int maxWidth) + Measurement IRenderable.Measure(Encoding encoding, int maxWidth) { - // Calculate the max width for each column - var maxColumnWidth = (maxWidth - (2 + (_columns.Count * 2) + (_columns.Count - 1))) / _columns.Count; - var columnWidths = _columns.Select(c => c.Measure(encoding, maxColumnWidth)).ToArray(); - for (var rowIndex = 0; rowIndex < _rows.Count; rowIndex++) + if (Width != null) { - for (var columnIndex = 0; columnIndex < _rows[rowIndex].Count; columnIndex++) - { - var columnWidth = _rows[rowIndex][columnIndex].Measure(encoding, maxColumnWidth); - if (columnWidth > columnWidths[columnIndex]) - { - columnWidths[columnIndex] = columnWidth; - } - } + maxWidth = Math.Min(Width.Value, maxWidth); } - // We now know the max width of each column, so let's recalculate the width - return columnWidths.Sum() + 2 + (_columns.Count * 2) + (_columns.Count - 1); + maxWidth -= GetExtraWidth(includePadding: true); + + var measurements = _columns.Select(column => MeasureColumn(column, encoding, maxWidth)).ToList(); + var min = measurements.Sum(x => x.Min) + GetExtraWidth(includePadding: true); + var max = Width ?? measurements.Sum(x => x.Max) + GetExtraWidth(includePadding: true); + + return new Measurement(min, max); } /// - public IEnumerable Render(Encoding encoding, int width) + IEnumerable IRenderable.Render(Encoding encoding, int width) { - var showBorder = _borderKind != BorderKind.None; - var hideBorder = _borderKind == BorderKind.None; + var border = Composition.Border.GetBorder(Border); - var leftRightBorderWidth = _borderKind == BorderKind.None ? 0 : 2; - var columnPadding = _borderKind == BorderKind.None ? _columns.Count : _columns.Count * 2; - var separatorCount = _borderKind == BorderKind.None ? 0 : _columns.Count - 1; + var showBorder = Border != BorderKind.None; + var hideBorder = Border == BorderKind.None; - // Calculate the max width for each column. - var maxColumnWidth = (width - (leftRightBorderWidth + columnPadding + separatorCount)) / _columns.Count; - var columnWidths = _columns.Select(c => c.Measure(encoding, maxColumnWidth)).ToArray(); - for (var rowIndex = 0; rowIndex < _rows.Count; rowIndex++) + var maxWidth = width; + if (Width != null) { - for (var columnIndex = 0; columnIndex < _rows[rowIndex].Count; columnIndex++) - { - var columnWidth = _rows[rowIndex][columnIndex].Measure(encoding, maxColumnWidth); - if (columnWidth > columnWidths[columnIndex]) - { - columnWidths[columnIndex] = columnWidth; - } - } + maxWidth = Math.Min(Width.Value, maxWidth); } - // We now know the max width of each column, so let's recalculate the width - width = columnWidths.Sum() + leftRightBorderWidth + columnPadding + separatorCount; + maxWidth -= GetExtraWidth(includePadding: true); + + // Calculate the column and table widths + var columnWidths = CalculateColumnWidths(encoding, maxWidth); + + // Update the table width. + width = columnWidths.Sum() + GetExtraWidth(includePadding: false); var rows = new List>(); - if (_showHeaders) + if (ShowHeaders) { // Add columns to top of rows - rows.Add(new List(_columns)); + rows.Add(new List(_columns.Select(c => c.Text))); } - // Add tows. + // Add rows. rows.AddRange(_rows); - // Iterate all rows. + // Iterate all rows var result = new List(); foreach (var (index, firstRow, lastRow, row) in rows.Enumerate()) { @@ -159,7 +181,7 @@ namespace Spectre.Console var cells = new List>(); foreach (var (rowWidth, cell) in columnWidths.Zip(row, (f, s) => (f, s))) { - var lines = Segment.SplitLines(cell.Render(encoding, rowWidth)); + var lines = Segment.SplitLines(((IRenderable)cell).Render(encoding, rowWidth)); cellHeight = Math.Max(cellHeight, lines.Count); cells.Add(lines); } @@ -167,20 +189,20 @@ namespace Spectre.Console // Show top of header? if (firstRow && showBorder) { - result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopLeft))); + result.Add(new Segment(border.GetPart(BorderPart.HeaderTopLeft))); foreach (var (columnIndex, _, lastColumn, columnWidth) in columnWidths.Enumerate()) { - result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop))); // Left padding - result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop, columnWidth))); - result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop))); // Right padding + result.Add(new Segment(border.GetPart(BorderPart.HeaderTop))); // Left padding + result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, columnWidth))); + result.Add(new Segment(border.GetPart(BorderPart.HeaderTop))); // Right padding if (!lastColumn) { - result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopSeparator))); + result.Add(new Segment(border.GetPart(BorderPart.HeaderTopSeparator))); } } - result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopRight))); + result.Add(new Segment(border.GetPart(BorderPart.HeaderTopRight))); result.Add(Segment.LineBreak()); } @@ -188,21 +210,24 @@ namespace Spectre.Console foreach (var cellRowIndex in Enumerable.Range(0, cellHeight)) { // Make cells the same shape - MakeSameHeight(cellHeight, cells); + cells = Segment.MakeSameHeight(cellHeight, cells); - var w00t = cells.Enumerate().ToArray(); - foreach (var (cellIndex, firstCell, lastCell, cell) in w00t) + foreach (var (cellIndex, firstCell, lastCell, cell) in cells.Enumerate()) { if (firstCell && showBorder) { // Show left column edge - result.Add(new Segment(_border.GetPart(BorderPart.CellLeft))); + result.Add(new Segment(border.GetPart(BorderPart.CellLeft))); } // Pad column on left side. if (showBorder) { - result.Add(new Segment(" ")); + var leftPadding = _columns[cellIndex].LeftPadding; + if (leftPadding > 0) + { + result.Add(new Segment(new string(' ', leftPadding))); + } } // Add content @@ -218,18 +243,22 @@ namespace Spectre.Console // Pad column on the right side if (showBorder || (hideBorder && !lastCell)) { - result.Add(new Segment(" ")); + var rightPadding = _columns[cellIndex].RightPadding; + if (rightPadding > 0) + { + result.Add(new Segment(new string(' ', rightPadding))); + } } if (lastCell && showBorder) { // Add right column edge - result.Add(new Segment(_border.GetPart(BorderPart.CellRight))); + result.Add(new Segment(border.GetPart(BorderPart.CellRight))); } else if (showBorder || (hideBorder && !lastCell)) { // Add column separator - result.Add(new Segment(_border.GetPart(BorderPart.CellSeparator))); + result.Add(new Segment(border.GetPart(BorderPart.CellSeparator))); } } @@ -237,42 +266,42 @@ namespace Spectre.Console } // Show bottom of header? - if (firstRow && showBorder) + if (firstRow && showBorder && ShowHeaders) { - result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomLeft))); + result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomLeft))); foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate()) { - result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom))); // Left padding - result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom, columnWidth))); - result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom))); // Right padding + result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom))); // Left padding + result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom, columnWidth))); + result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom))); // Right padding if (!lastColumn) { - result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomSeparator))); + result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomSeparator))); } } - result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomRight))); + result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomRight))); result.Add(Segment.LineBreak()); } // Show bottom of footer? if (lastRow && showBorder) { - result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomLeft))); + result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft))); foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate()) { - result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom))); - result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom, columnWidth))); - result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom))); + result.Add(new Segment(border.GetPart(BorderPart.FooterBottom))); + result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, columnWidth))); + result.Add(new Segment(border.GetPart(BorderPart.FooterBottom))); if (!lastColumn) { - result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomSeparator))); + result.Add(new Segment(border.GetPart(BorderPart.FooterBottomSeparator))); } } - result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomRight))); + result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight))); result.Add(Segment.LineBreak()); } } @@ -280,18 +309,9 @@ namespace Spectre.Console return result; } - private static void MakeSameHeight(int cellHeight, List> cells) + private bool ShouldExpand() { - foreach (var cell in cells) - { - if (cell.Count < cellHeight) - { - while (cell.Count != cellHeight) - { - cell.Add(new SegmentLine()); - } - } - } + return Expand || Width != null; } } } diff --git a/src/Spectre.Console/Composition/TableColumn.cs b/src/Spectre.Console/Composition/TableColumn.cs new file mode 100644 index 0000000..3b19de1 --- /dev/null +++ b/src/Spectre.Console/Composition/TableColumn.cs @@ -0,0 +1,67 @@ +using System; + +namespace Spectre.Console +{ + /// + /// Represents a table column. + /// + public sealed class TableColumn + { + /// + /// Gets the text associated with the column. + /// + public Text Text { get; } + + /// + /// Gets or sets the width of the column. + /// If null, the column will adapt to it's contents. + /// + public int? Width { get; set; } + + /// + /// Gets or sets the left padding. + /// + public int LeftPadding { get; set; } + + /// + /// Gets or sets the right padding. + /// + public int RightPadding { get; set; } + + /// + /// Gets or sets the ratio to use when calculating column width. + /// If null, the column will adapt to it's contents. + /// + public int? Ratio { get; set; } + + /// + /// Gets or sets a value indicating whether wrapping of + /// text within the column should be prevented. + /// + public bool NoWrap { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The table column text. + public TableColumn(string text) + { + Text = Text.New(text ?? throw new ArgumentNullException(nameof(text))); + Width = null; + LeftPadding = 1; + RightPadding = 1; + Ratio = null; + NoWrap = false; + } + + internal int GetPadding() + { + return LeftPadding + RightPadding; + } + + internal bool IsFlexible() + { + return Width == null; + } + } +} diff --git a/src/Spectre.Console/Composition/Text.cs b/src/Spectre.Console/Composition/Text.cs index c7a91bd..da892f3 100644 --- a/src/Spectre.Console/Composition/Text.cs +++ b/src/Spectre.Console/Composition/Text.cs @@ -98,19 +98,22 @@ namespace Spectre.Console } /// - public int Measure(Encoding encoding, int maxWidth) + Measurement IRenderable.Measure(Encoding encoding, int maxWidth) { - var lines = Segment.SplitLines(Render(encoding, maxWidth)); + var lines = Segment.SplitLines(((IRenderable)this).Render(encoding, maxWidth)); if (lines.Count == 0) { - return 0; + return new Measurement(0, maxWidth); } - return lines.Max(x => x.Length); + var max = lines.Max(line => line.Length); + var min = lines.SelectMany(line => line.Select(segment => segment.Text.Length)).Max(); + + return new Measurement(min, max); } /// - public IEnumerable Render(Encoding encoding, int width) + IEnumerable IRenderable.Render(Encoding encoding, int width) { if (string.IsNullOrWhiteSpace(_text)) { diff --git a/src/Spectre.Console/ConsoleExtensions.WriteLine.cs b/src/Spectre.Console/ConsoleExtensions.WriteLine.cs index 731b4fa..98546ec 100644 --- a/src/Spectre.Console/ConsoleExtensions.WriteLine.cs +++ b/src/Spectre.Console/ConsoleExtensions.WriteLine.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using Spectre.Console.Internal; namespace Spectre.Console { @@ -19,7 +20,12 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(console)); } - console.Write(Environment.NewLine); + using (console.PushColor(Color.Default, true)) + using (console.PushColor(Color.Default, false)) + using (console.PushDecoration(Decoration.None)) + { + console.Write(Environment.NewLine); + } } /// diff --git a/src/Spectre.Console/Internal/Extensions/EnumerableExtensions.cs b/src/Spectre.Console/Internal/Extensions/EnumerableExtensions.cs index 48b9da0..0529de4 100644 --- a/src/Spectre.Console/Internal/Extensions/EnumerableExtensions.cs +++ b/src/Spectre.Console/Internal/Extensions/EnumerableExtensions.cs @@ -6,6 +6,19 @@ namespace Spectre.Console.Internal { internal static class EnumerableExtensions { + public static void ForEach(this IEnumerable source, Action action) + { + foreach (var item in source) + { + action(item); + } + } + + public static bool AnyTrue(this IEnumerable source) + { + return source.Any(b => b); + } + public static IEnumerable<(int Index, bool First, bool Last, T Item)> Enumerate(this IEnumerable source) { if (source is null) @@ -40,5 +53,18 @@ namespace Spectre.Console.Internal { return source.Select((value, index) => func(value, index)); } + + public static IEnumerable<(TFirst First, TSecond Second)> Zip( + this IEnumerable source, IEnumerable first) + { + return source.Zip(first, (first, second) => (first, second)); + } + + public static IEnumerable<(TFirst First, TSecond Second, TThird Third)> Zip( + this IEnumerable first, IEnumerable second, IEnumerable third) + { + return first.Zip(second, (a, b) => (a, b)) + .Zip(third, (a, b) => (a.a, a.b, b)); + } } } diff --git a/src/Spectre.Console/Internal/Utilities/Ratio.cs b/src/Spectre.Console/Internal/Utilities/Ratio.cs new file mode 100644 index 0000000..b4acdbd --- /dev/null +++ b/src/Spectre.Console/Internal/Utilities/Ratio.cs @@ -0,0 +1,75 @@ +// Ported from Rich by Will McGugan, licensed under MIT. +// https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/_ratio.py + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Spectre.Console.Internal +{ + internal static class Ratio + { + public static List Reduce(int total, List ratios, List maximums, List values) + { + ratios = ratios.Zip(maximums, (a, b) => (ratio: a, max: b)).Select(a => a.max > 0 ? a.ratio : 0).ToList(); + var totalRatio = ratios.Sum(); + if (totalRatio == 0) + { + return values; + } + + var totalRemaining = total; + var result = new List(); + + foreach (var (ratio, maximum, value) in ratios.Zip(maximums, values)) + { + if (ratio > 0 && totalRatio > 0) + { + var distributed = (int)Math.Min(maximum, Math.Round(ratio * totalRemaining / (double)totalRatio)); + result.Add(value - distributed); + totalRemaining -= distributed; + totalRatio -= ratio; + } + else + { + result.Add(value); + } + } + + return result; + } + + public static List Distribute(int total, List ratios, List minimums = null) + { + if (minimums != null) + { + ratios = ratios.Zip(minimums, (a, b) => (ratio: a, min: b)).Select(a => a.min > 0 ? a.ratio : 0).ToList(); + } + + var totalRatio = ratios.Sum(); + Debug.Assert(totalRatio > 0, "Sum or ratios must be > 0"); + + var totalRemaining = total; + var distributedTotal = new List(); + + if (minimums == null) + { + minimums = ratios.Select(_ => 0).ToList(); + } + + foreach (var (ratio, minimum) in ratios.Zip(minimums, (a, b) => (a, b))) + { + var distributed = (totalRatio > 0) + ? Math.Max(minimum, (int)Math.Ceiling(ratio * totalRemaining / (double)totalRatio)) + : totalRemaining; + + distributedTotal.Add(distributed); + totalRatio -= ratio; + totalRemaining -= distributed; + } + + return distributedTotal; + } + } +} diff --git a/src/Spectre.Console/Spectre.Console.csproj b/src/Spectre.Console/Spectre.Console.csproj index 908f387..de6b91f 100644 --- a/src/Spectre.Console/Spectre.Console.csproj +++ b/src/Spectre.Console/Spectre.Console.csproj @@ -22,6 +22,9 @@ ConsoleExtensions.cs + + **/Table.cs +