From c3ec6a73638fb9cc18499b5ed5a2b7c73a75a5d1 Mon Sep 17 00:00:00 2001 From: Patrik Svensson <patriksvensson@users.noreply.github.com> Date: Tue, 15 Nov 2022 10:12:17 +0100 Subject: [PATCH] Add Layout widget (#1041) * Add width to panels * Add height to panels * Replace RenderContext with RenderOptions * Remove exclusivity from alternative buffer * Add Layout widget * Add Align widget --- examples/Console/Borders/Program.cs | 2 +- examples/Console/Canvas/Program.cs | 2 +- examples/Console/Colors/Program.cs | 8 +- examples/Console/Exceptions/Program.cs | 8 +- examples/Console/Figlet/Program.cs | 4 +- examples/Console/Layout/Layout.csproj | 15 + examples/Console/Layout/Program.cs | 60 ++++ examples/Console/Panels/Program.cs | 4 +- examples/Console/Paths/Program.cs | 4 +- examples/Console/Prompt/Program.cs | 4 +- examples/Console/Rules/Program.cs | 6 +- examples/Examples.sln | 14 + examples/Shared/ColorBox.cs | 4 +- src/Spectre.Console.Cli/Internal/Composer.cs | 8 +- src/Spectre.Console.ImageSharp/CanvasImage.cs | 6 +- .../TestCapabilities.cs | 14 +- .../TestConsoleExtensions.cs | 25 ++ src/Spectre.Console.v3.ncrunchsolution | 6 + .../Extensions/AlignExtensions.cs | 106 ++++++ .../Extensions/AlignableExtensions.cs | 2 +- .../AnsiConsoleExtensions.Screen.cs | 17 +- .../Extensions/HasJustificationExtensions.cs | 80 +++++ .../Extensions/LayoutExtensions.cs | 58 ++++ .../Extensions/PanelExtensions.cs | 4 +- .../Extensions/RenderOptionsExtensions.cs | 10 + .../Extensions/RenderableExtensions.cs | 4 +- .../Extensions/VisibilityExtensions.cs | 43 +++ src/Spectre.Console/HorizontalAlignment.cs | 22 ++ src/Spectre.Console/IAlignable.cs | 2 +- src/Spectre.Console/IHasJustification.cs | 12 + src/Spectre.Console/IHasVisibility.cs | 13 + src/Spectre.Console/IRenderable.cs | 8 +- src/Spectre.Console/Internal/Aligner.cs | 47 +++ .../Internal/IRatioResolvable.cs | 22 ++ .../Internal/Polyfill/IsExternalInit.cs | 15 + src/Spectre.Console/Internal/Ratio.cs | 72 ++++ .../Internal/Text/Encoding/HtmlEncoder.cs | 2 +- .../Internal/Text/Encoding/TextEncoder.cs | 2 +- src/Spectre.Console/Justify.cs | 4 +- .../Live/LiveDisplayRenderer.cs | 2 +- src/Spectre.Console/Live/LiveRenderable.cs | 14 +- .../Live/Progress/Columns/DownloadedColumn.cs | 2 +- .../Progress/Columns/ElapsedTimeColumn.cs | 4 +- .../Live/Progress/Columns/PercentageColumn.cs | 6 +- .../Progress/Columns/ProgressBarColumn.cs | 2 +- .../Progress/Columns/RemainingTimeColumn.cs | 4 +- .../Live/Progress/Columns/SpinnerColumn.cs | 16 +- .../Progress/Columns/TaskDescriptionColumn.cs | 4 +- .../Progress/Columns/TransferSpeedColumn.cs | 2 +- .../Live/Progress/ProgressColumn.cs | 8 +- .../Live/Progress/ProgressRenderer.cs | 2 +- .../Renderers/DefaultProgressRenderer.cs | 4 +- .../Renderers/FallbackProgressRenderer.cs | 2 +- .../Renderers/FallbackStatusRenderer.cs | 2 +- .../Prompts/List/ListPromptRenderHook.cs | 2 +- src/Spectre.Console/Region.cs | 43 +++ src/Spectre.Console/Rendering/IRenderHook.cs | 4 +- .../Rendering/JustInTimeRenderable.cs | 4 +- .../Rendering/RenderContext.cs | 78 ----- .../Rendering/RenderOptions.cs | 63 ++++ .../Rendering/RenderPipeline.cs | 6 +- src/Spectre.Console/Rendering/Renderable.cs | 16 +- src/Spectre.Console/Rendering/Segment.cs | 37 ++- src/Spectre.Console/Rendering/SegmentShape.cs | 9 +- src/Spectre.Console/Size.cs | 29 ++ src/Spectre.Console/VerticalAlignment.cs | 22 ++ src/Spectre.Console/Widgets/Align.cs | 146 ++++++++ src/Spectre.Console/Widgets/Calendar.cs | 3 + src/Spectre.Console/Widgets/Canvas.cs | 4 +- .../Widgets/Charts/BarChart.cs | 8 +- .../Widgets/Charts/BreakdownBar.cs | 4 +- .../Widgets/Charts/BreakdownChart.cs | 20 +- .../Widgets/Charts/BreakdownTags.cs | 8 +- src/Spectre.Console/Widgets/Columns.cs | 10 +- src/Spectre.Console/Widgets/ControlCode.cs | 6 +- .../Widgets/Figlet/FigletText.cs | 14 +- src/Spectre.Console/Widgets/Grid.cs | 1 + src/Spectre.Console/Widgets/Layout/Layout.cs | 311 ++++++++++++++++++ .../Widgets/Layout/LayoutPlaceholder.cs | 31 ++ .../Widgets/Layout/LayoutRender.cs | 14 + .../Widgets/Layout/LayoutSplitter.cs | 48 +++ src/Spectre.Console/Widgets/Markup.cs | 16 +- src/Spectre.Console/Widgets/Padder.cs | 10 +- src/Spectre.Console/Widgets/Panel.cs | 89 +++-- src/Spectre.Console/Widgets/PanelHeader.cs | 8 +- src/Spectre.Console/Widgets/Paragraph.cs | 20 +- src/Spectre.Console/Widgets/ProgressBar.cs | 20 +- src/Spectre.Console/Widgets/Rows.cs | 8 +- src/Spectre.Console/Widgets/Rule.cs | 30 +- src/Spectre.Console/Widgets/Table/Table.cs | 19 +- .../Widgets/Table/TableAccessor.cs | 4 +- .../Widgets/Table/TableMeasurer.cs | 2 +- .../Widgets/Table/TableRenderer.cs | 4 +- .../Widgets/Table/TableRendererContext.cs | 7 +- src/Spectre.Console/Widgets/Text.cs | 16 +- src/Spectre.Console/Widgets/TextPath.cs | 20 +- src/Spectre.Console/Widgets/Tree.cs | 20 +- .../Spectre.Console.Cli.Tests.csproj | 14 +- .../Align/Center_Bottom.Output.verified.txt | 15 + .../Align/Center_Middle.Output.verified.txt | 15 + .../Align/Center_Top.Output.verified.txt | 15 + .../Align/Left_Bottom.Output.verified.txt | 15 + .../Align/Left_Middle.Output.verified.txt | 15 + .../Align/Left_Top.Output.verified.txt | 15 + .../Align/Right_Bottom.Output.verified.txt | 15 + .../Align/Right_Middle.Output.verified.txt | 15 + .../Align/Right_Top.Output.verified.txt | 15 + .../Render_Empty_Layout.Output.verified.txt | 15 + ...Render_Fallback_Layout.Output.verified.txt | 15 + .../Layout/Render_Layout.Output.verified.txt | 15 + ...er_Layout_With_Columns.Output.verified.txt | 15 + ...ut_With_Nested_Columns.Output.verified.txt | 15 + ...ayout_With_Nested_Rows.Output.verified.txt | 15 + ...ested_Rows_And_Columns.Output.verified.txt | 15 + ...espect_To_Minimum_Size.Output.verified.txt | 15 + ..._With_Respect_To_Ratio.Output.verified.txt | 15 + ...t_With_Respect_To_Size.Output.verified.txt | 15 + ...ender_Layout_With_Rows.Output.verified.txt | 15 + ...out_Invisible_Children.Output.verified.txt | 15 + .../Panel/Render_Height.Output.verified.txt | 25 ++ .../Panel/Render_Width.Output.verified.txt | 3 + .../Render_Width_Height.Output.verified.txt | 25 ++ .../Render_Width_MaxWidth.Output.verified.txt | 3 + .../Render_Centered.Align_Widget.verified.txt | 6 + ...nder_LeftAligned.Align_Widget.verified.txt | 6 + ...der_RightAligned.Align_Widget.verified.txt | 6 + .../Spectre.Console.Tests.csproj | 3 +- .../Live/Progress/ProgressColumnFixture.cs | 2 +- .../Unit/Rendering/RenderHookTests.cs | 2 +- .../Unit/Widgets/AlignTests.cs | 155 +++++++++ .../Unit/Widgets/FigletTests.cs | 6 +- .../Unit/Widgets/LayoutTests.cs | 268 +++++++++++++++ .../Unit/Widgets/PanelTests.cs | 76 ++++- .../Unit/Widgets/RuleTests.cs | 4 +- .../Unit/Widgets/Table/TableTests.cs | 62 +++- .../Unit/Widgets/TextPathTests.cs | 2 +- .../Unit/Widgets/TextTests.cs | 8 +- 137 files changed, 2651 insertions(+), 387 deletions(-) create mode 100644 examples/Console/Layout/Layout.csproj create mode 100644 examples/Console/Layout/Program.cs create mode 100644 src/Spectre.Console.v3.ncrunchsolution create mode 100644 src/Spectre.Console/Extensions/AlignExtensions.cs create mode 100644 src/Spectre.Console/Extensions/HasJustificationExtensions.cs create mode 100644 src/Spectre.Console/Extensions/LayoutExtensions.cs create mode 100644 src/Spectre.Console/Extensions/RenderOptionsExtensions.cs create mode 100644 src/Spectre.Console/Extensions/VisibilityExtensions.cs create mode 100644 src/Spectre.Console/HorizontalAlignment.cs create mode 100644 src/Spectre.Console/IHasJustification.cs create mode 100644 src/Spectre.Console/IHasVisibility.cs create mode 100644 src/Spectre.Console/Internal/IRatioResolvable.cs create mode 100644 src/Spectre.Console/Internal/Polyfill/IsExternalInit.cs create mode 100644 src/Spectre.Console/Region.cs delete mode 100644 src/Spectre.Console/Rendering/RenderContext.cs create mode 100644 src/Spectre.Console/Rendering/RenderOptions.cs create mode 100644 src/Spectre.Console/Size.cs create mode 100644 src/Spectre.Console/VerticalAlignment.cs create mode 100644 src/Spectre.Console/Widgets/Align.cs create mode 100644 src/Spectre.Console/Widgets/Layout/Layout.cs create mode 100644 src/Spectre.Console/Widgets/Layout/LayoutPlaceholder.cs create mode 100644 src/Spectre.Console/Widgets/Layout/LayoutRender.cs create mode 100644 src/Spectre.Console/Widgets/Layout/LayoutSplitter.cs create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Bottom.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Middle.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Top.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Bottom.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Middle.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Top.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Bottom.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Middle.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Top.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Empty_Layout.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Fallback_Layout.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Columns.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Columns.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Rows.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Rows_And_Columns.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Minimum_Size.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Ratio.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Size.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Rows.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_Without_Invisible_Children.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Height.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width_Height.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width_MaxWidth.Output.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_Centered.Align_Widget.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_LeftAligned.Align_Widget.verified.txt create mode 100644 test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_RightAligned.Align_Widget.verified.txt create mode 100644 test/Spectre.Console.Tests/Unit/Widgets/AlignTests.cs create mode 100644 test/Spectre.Console.Tests/Unit/Widgets/LayoutTests.cs diff --git a/examples/Console/Borders/Program.cs b/examples/Console/Borders/Program.cs index 854ef7f..0dca101 100644 --- a/examples/Console/Borders/Program.cs +++ b/examples/Console/Borders/Program.cs @@ -84,7 +84,7 @@ public static class Program private static void HorizontalRule(string title) { AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule($"[white bold]{title}[/]").RuleStyle("grey").LeftAligned()); + AnsiConsole.Write(new Rule($"[white bold]{title}[/]").RuleStyle("grey").LeftJustified()); AnsiConsole.WriteLine(); } } diff --git a/examples/Console/Canvas/Program.cs b/examples/Console/Canvas/Program.cs index 57135a3..941d39f 100644 --- a/examples/Console/Canvas/Program.cs +++ b/examples/Console/Canvas/Program.cs @@ -40,7 +40,7 @@ public static class Program private static void Render(IRenderable canvas, string title) { AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule($"[yellow]{title}[/]").LeftAligned().RuleStyle("grey")); + AnsiConsole.Write(new Rule($"[yellow]{title}[/]").LeftJustified().RuleStyle("grey")); AnsiConsole.WriteLine(); AnsiConsole.Write(canvas); } diff --git a/examples/Console/Colors/Program.cs b/examples/Console/Colors/Program.cs index 8ee60db..946cb3d 100644 --- a/examples/Console/Colors/Program.cs +++ b/examples/Console/Colors/Program.cs @@ -23,7 +23,7 @@ public static class Program { AnsiConsole.ResetColors(); AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("[yellow bold underline]3-bit Colors[/]").RuleStyle("grey").LeftAligned()); + AnsiConsole.Write(new Rule("[yellow bold underline]3-bit Colors[/]").RuleStyle("grey").LeftJustified()); AnsiConsole.WriteLine(); for (var i = 0; i < 8; i++) @@ -46,7 +46,7 @@ public static class Program { AnsiConsole.ResetColors(); AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("[yellow bold underline]4-bit Colors[/]").RuleStyle("grey").LeftAligned()); + AnsiConsole.Write(new Rule("[yellow bold underline]4-bit Colors[/]").RuleStyle("grey").LeftJustified()); AnsiConsole.WriteLine(); for (var i = 0; i < 16; i++) @@ -69,7 +69,7 @@ public static class Program { AnsiConsole.ResetColors(); AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("[yellow bold underline]8-bit Colors[/]").RuleStyle("grey").LeftAligned()); + AnsiConsole.Write(new Rule("[yellow bold underline]8-bit Colors[/]").RuleStyle("grey").LeftJustified()); AnsiConsole.WriteLine(); for (var i = 0; i < 16; i++) @@ -96,7 +96,7 @@ public static class Program { AnsiConsole.ResetColors(); AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("[yellow bold underline]24-bit Colors[/]").RuleStyle("grey").LeftAligned()); + AnsiConsole.Write(new Rule("[yellow bold underline]24-bit Colors[/]").RuleStyle("grey").LeftJustified()); AnsiConsole.WriteLine(); AnsiConsole.Write(new ColorBox(width: 80, height: 15)); diff --git a/examples/Console/Exceptions/Program.cs b/examples/Console/Exceptions/Program.cs index d78e647..032908a 100644 --- a/examples/Console/Exceptions/Program.cs +++ b/examples/Console/Exceptions/Program.cs @@ -19,17 +19,17 @@ public static class Program catch (Exception ex) { AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("Default").LeftAligned()); + AnsiConsole.Write(new Rule("Default").LeftJustified()); AnsiConsole.WriteLine(); AnsiConsole.WriteException(ex); AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("Compact").LeftAligned()); + AnsiConsole.Write(new Rule("Compact").LeftJustified()); AnsiConsole.WriteLine(); AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks); AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("Compact + Custom colors").LeftAligned()); + AnsiConsole.Write(new Rule("Compact + Custom colors").LeftJustified()); AnsiConsole.WriteLine(); AnsiConsole.WriteException(ex, new ExceptionSettings { @@ -56,7 +56,7 @@ public static class Program catch (Exception ex) { AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("Async").LeftAligned()); + AnsiConsole.Write(new Rule("Async").LeftJustified()); AnsiConsole.WriteLine(); AnsiConsole.WriteException(ex, ExceptionFormats.ShortenPaths); } diff --git a/examples/Console/Figlet/Program.cs b/examples/Console/Figlet/Program.cs index cda5ed5..a831014 100644 --- a/examples/Console/Figlet/Program.cs +++ b/examples/Console/Figlet/Program.cs @@ -6,8 +6,8 @@ public static class Program { public static void Main(string[] args) { - AnsiConsole.Write(new FigletText("Left aligned").LeftAligned().Color(Color.Red)); + AnsiConsole.Write(new FigletText("Left aligned").LeftJustified().Color(Color.Red)); AnsiConsole.Write(new FigletText("Centered").Centered().Color(Color.Green)); - AnsiConsole.Write(new FigletText("Right aligned").RightAligned().Color(Color.Blue)); + AnsiConsole.Write(new FigletText("Right aligned").RightJustified().Color(Color.Blue)); } } diff --git a/examples/Console/Layout/Layout.csproj b/examples/Console/Layout/Layout.csproj new file mode 100644 index 0000000..5bb2daa --- /dev/null +++ b/examples/Console/Layout/Layout.csproj @@ -0,0 +1,15 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net7.0</TargetFramework> + <ExampleTitle>Layout</ExampleTitle> + <ExampleDescription>Demonstrates how to use layouts.</ExampleDescription> + <ExampleGroup>Widgets</ExampleGroup> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\..\Shared\Shared.csproj" /> + </ItemGroup> + +</Project> diff --git a/examples/Console/Layout/Program.cs b/examples/Console/Layout/Program.cs new file mode 100644 index 0000000..835b614 --- /dev/null +++ b/examples/Console/Layout/Program.cs @@ -0,0 +1,60 @@ +using System; +using Spectre.Console; + +namespace Layouts; + +public static class Program +{ + public static void Main() + { + var layout = CreateLayout(); + AnsiConsole.Write(layout); + + Console.ReadKey(true); + } + + private static Layout CreateLayout() + { + var layout = new Layout(); + + layout.SplitRows( + new Layout("Top") + .SplitColumns( + new Layout("Left") + .SplitRows( + new Layout("LeftTop"), + new Layout("LeftBottom")), + new Layout("Right").Ratio(2), + new Layout("RightRight").Size(3)), + new Layout("Bottom")); + + layout["LeftBottom"].Update( + new Panel("[blink]PRESS ANY KEY TO QUIT[/]") + .Expand() + .BorderColor(Color.Yellow) + .Padding(0, 0)); + + layout["Right"].Update( + new Panel( + new Table() + .AddColumns("[blue]Qux[/]", "[green]Corgi[/]") + .AddRow("9", "8") + .AddRow("7", "6") + .Expand()) + .Header("A [yellow]Table[/] in a [blue]Panel[/] (Ratio=2)") + .Expand()); + + layout["RightRight"].Update( + new Panel("Explicit-size-is-[yellow]3[/]") + .BorderColor(Color.Yellow) + .Padding(0, 0)); + + layout["Bottom"].Update( + new Panel( + new FigletText("Hello World")) + .Header("Some [green]Figlet[/] text") + .Expand()); + + return layout; + } +} diff --git a/examples/Console/Panels/Program.cs b/examples/Console/Panels/Program.cs index 79d18a8..dc9ecb8 100644 --- a/examples/Console/Panels/Program.cs +++ b/examples/Console/Panels/Program.cs @@ -17,7 +17,7 @@ public static class Program // Left adjusted panel with text AnsiConsole.Write( - new Panel(new Text("Left adjusted\nLeft").LeftAligned()) + new Panel(new Text("Left adjusted\nLeft").LeftJustified()) .Expand() .SquareBorder() .Header("[red]Left[/]")); @@ -32,7 +32,7 @@ public static class Program // Right adjusted, rounded panel with text AnsiConsole.Write( - new Panel(new Text("Right adjusted\nRight").RightAligned()) + new Panel(new Text("Right adjusted\nRight").RightJustified()) .Expand() .RoundedBorder() .Header("[blue]Right[/]") diff --git a/examples/Console/Paths/Program.cs b/examples/Console/Paths/Program.cs index baeac89..ff88429 100644 --- a/examples/Console/Paths/Program.cs +++ b/examples/Console/Paths/Program.cs @@ -58,9 +58,9 @@ public static class Program var table = new Table().BorderColor(Color.Grey).Title("Aligned").RoundedBorder(); table.AddColumns("[grey]Alignment[/]", "[grey]Path[/]"); - table.AddRow(new Text("Left"), new TextPath(path).LeftAligned()); + table.AddRow(new Text("Left"), new TextPath(path).LeftJustified()); table.AddRow(new Text("Center"), new TextPath(path).Centered()); - table.AddRow(new Text("Right"), new TextPath(path).RightAligned()); + table.AddRow(new Text("Right"), new TextPath(path).RightJustified()); AnsiConsole.Write(table); } diff --git a/examples/Console/Prompt/Program.cs b/examples/Console/Prompt/Program.cs index 07f01cc..58dadac 100644 --- a/examples/Console/Prompt/Program.cs +++ b/examples/Console/Prompt/Program.cs @@ -46,7 +46,7 @@ namespace Prompt // Summary AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("[yellow]Results[/]").RuleStyle("grey").LeftAligned()); + AnsiConsole.Write(new Rule("[yellow]Results[/]").RuleStyle("grey").LeftJustified()); AnsiConsole.Write(new Table().AddColumns("[grey]Question[/]", "[grey]Answer[/]") .RoundedBorder() .BorderColor(Color.Grey) @@ -63,7 +63,7 @@ namespace Prompt private static void WriteDivider(string text) { AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule($"[yellow]{text}[/]").RuleStyle("grey").LeftAligned()); + AnsiConsole.Write(new Rule($"[yellow]{text}[/]").RuleStyle("grey").LeftJustified()); } public static bool AskConfirmation() diff --git a/examples/Console/Rules/Program.cs b/examples/Console/Rules/Program.cs index 2f158eb..442ed3f 100644 --- a/examples/Console/Rules/Program.cs +++ b/examples/Console/Rules/Program.cs @@ -11,14 +11,14 @@ public static class Program new Rule() .RuleStyle(Style.Parse("yellow")) .AsciiBorder() - .LeftAligned()); + .LeftJustified()); // Left aligned title Render( new Rule("[blue]Left aligned[/]") .RuleStyle(Style.Parse("red")) .DoubleBorder() - .LeftAligned()); + .LeftJustified()); // Centered title Render( @@ -31,7 +31,7 @@ public static class Program Render( new Rule("[red]Right aligned[/]") .RuleStyle(Style.Parse("blue")) - .RightAligned()); + .RightJustified()); } private static void Render(Rule rule) diff --git a/examples/Examples.sln b/examples/Examples.sln index 317ec66..01a0700 100644 --- a/examples/Examples.sln +++ b/examples/Examples.sln @@ -77,6 +77,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paths", "Console\Paths\Path EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Cli", "..\src\Spectre.Console.Cli\Spectre.Console.Cli.csproj", "{EFAADF6A-C77D-41EC-83F5-BBB4FFC5A6D7}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Layout", "Console\Layout\Layout.csproj", "{A9FDE73A-8452-4CA3-B366-3F900597E132}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -507,6 +509,18 @@ Global {EFAADF6A-C77D-41EC-83F5-BBB4FFC5A6D7}.Release|x64.Build.0 = Release|Any CPU {EFAADF6A-C77D-41EC-83F5-BBB4FFC5A6D7}.Release|x86.ActiveCfg = Release|Any CPU {EFAADF6A-C77D-41EC-83F5-BBB4FFC5A6D7}.Release|x86.Build.0 = Release|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Debug|x64.ActiveCfg = Debug|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Debug|x64.Build.0 = Debug|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Debug|x86.ActiveCfg = Debug|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Debug|x86.Build.0 = Debug|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Release|Any CPU.Build.0 = Release|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Release|x64.ActiveCfg = Release|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Release|x64.Build.0 = Release|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Release|x86.ActiveCfg = Release|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/examples/Shared/ColorBox.cs b/examples/Shared/ColorBox.cs index ef00787..7438c9d 100644 --- a/examples/Shared/ColorBox.cs +++ b/examples/Shared/ColorBox.cs @@ -20,12 +20,12 @@ public sealed class ColorBox : Renderable _width = width; } - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { return new Measurement(1, GetWidth(maxWidth)); } - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { maxWidth = GetWidth(maxWidth); diff --git a/src/Spectre.Console.Cli/Internal/Composer.cs b/src/Spectre.Console.Cli/Internal/Composer.cs index 2d8964b..e692e90 100644 --- a/src/Spectre.Console.Cli/Internal/Composer.cs +++ b/src/Spectre.Console.Cli/Internal/Composer.cs @@ -83,14 +83,14 @@ internal sealed class Composer : IRenderable return this; } - public Measurement Measure(RenderContext context, int maxWidth) + public Measurement Measure(RenderOptions options, int maxWidth) { - return ((IRenderable)new Markup(_content.ToString())).Measure(context, maxWidth); + return ((IRenderable)new Markup(_content.ToString())).Measure(options, maxWidth); } - public IEnumerable<Segment> Render(RenderContext context, int maxWidth) + public IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { - return ((IRenderable)new Markup(_content.ToString())).Render(context, maxWidth); + return ((IRenderable)new Markup(_content.ToString())).Render(options, maxWidth); } public override string ToString() diff --git a/src/Spectre.Console.ImageSharp/CanvasImage.cs b/src/Spectre.Console.ImageSharp/CanvasImage.cs index 999ad91..aba8b01 100644 --- a/src/Spectre.Console.ImageSharp/CanvasImage.cs +++ b/src/Spectre.Console.ImageSharp/CanvasImage.cs @@ -71,7 +71,7 @@ public sealed class CanvasImage : Renderable } /// <inheritdoc/> - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { if (PixelWidth < 0) { @@ -88,7 +88,7 @@ public sealed class CanvasImage : Renderable } /// <inheritdoc/> - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { var image = Image; @@ -138,6 +138,6 @@ public sealed class CanvasImage : Renderable } } - return ((IRenderable)canvas).Render(context, maxWidth); + return ((IRenderable)canvas).Render(options, maxWidth); } } \ No newline at end of file diff --git a/src/Spectre.Console.Testing/TestCapabilities.cs b/src/Spectre.Console.Testing/TestCapabilities.cs index 1064bd5..d4f702b 100644 --- a/src/Spectre.Console.Testing/TestCapabilities.cs +++ b/src/Spectre.Console.Testing/TestCapabilities.cs @@ -27,11 +27,17 @@ public sealed class TestCapabilities : IReadOnlyCapabilities public bool Unicode { get; set; } /// <summary> - /// Creates a <see cref="RenderContext"/> with the same capabilities as this instace. + /// Creates a <see cref="RenderOptions"/> with the same capabilities as this instace. /// </summary> - /// <returns>A <see cref="RenderContext"/> with the same capabilities as this instace.</returns> - public RenderContext CreateRenderContext() + /// <param name="console">The console.</param> + /// <returns>A <see cref="RenderOptions"/> with the same capabilities as this instace.</returns> + public RenderOptions CreateRenderContext(IAnsiConsole console) { - return new RenderContext(this); + if (console is null) + { + throw new ArgumentNullException(nameof(console)); + } + + return RenderOptions.Create(console, this); } } \ No newline at end of file diff --git a/src/Spectre.Console.Testing/TestConsoleExtensions.cs b/src/Spectre.Console.Testing/TestConsoleExtensions.cs index c2397d0..545414c 100644 --- a/src/Spectre.Console.Testing/TestConsoleExtensions.cs +++ b/src/Spectre.Console.Testing/TestConsoleExtensions.cs @@ -52,6 +52,31 @@ public static class TestConsoleExtensions return console; } + /// <summary> + /// Sets the console height. + /// </summary> + /// <param name="console">The console.</param> + /// <param name="width">The console height.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static TestConsole Height(this TestConsole console, int width) + { + console.Profile.Height = width; + return console; + } + + /// <summary> + /// Sets the console size. + /// </summary> + /// <param name="console">The console.</param> + /// <param name="size">The console size.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static TestConsole Size(this TestConsole console, Size size) + { + console.Profile.Width = size.Width; + console.Profile.Height = size.Height; + return console; + } + /// <summary> /// Turns on emitting of VT/ANSI sequences. /// </summary> diff --git a/src/Spectre.Console.v3.ncrunchsolution b/src/Spectre.Console.v3.ncrunchsolution new file mode 100644 index 0000000..10420ac --- /dev/null +++ b/src/Spectre.Console.v3.ncrunchsolution @@ -0,0 +1,6 @@ +<SolutionConfiguration> + <Settings> + <AllowParallelTestExecution>True</AllowParallelTestExecution> + <SolutionConfigured>True</SolutionConfigured> + </Settings> +</SolutionConfiguration> \ No newline at end of file diff --git a/src/Spectre.Console/Extensions/AlignExtensions.cs b/src/Spectre.Console/Extensions/AlignExtensions.cs new file mode 100644 index 0000000..f2c700e --- /dev/null +++ b/src/Spectre.Console/Extensions/AlignExtensions.cs @@ -0,0 +1,106 @@ +namespace Spectre.Console.Extensions; + +/// <summary> +/// Contains extension methods for <see cref="Align"/>. +/// </summary> +public static class AlignExtensions +{ + /// <summary> + /// Sets the width. + /// </summary> + /// <param name="align">The <see cref="Align"/> object.</param> + /// <param name="width">The width, or <c>null</c> for no explicit width.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static Align Width(this Align align, int? width) + { + if (align is null) + { + throw new ArgumentNullException(nameof(align)); + } + + align.Width = width; + return align; + } + + /// <summary> + /// Sets the height. + /// </summary> + /// <param name="align">The <see cref="Align"/> object.</param> + /// <param name="height">The height, or <c>null</c> for no explicit height.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static Align Height(this Align align, int? height) + { + if (align is null) + { + throw new ArgumentNullException(nameof(align)); + } + + align.Height = height; + return align; + } + + /// <summary> + /// Sets the vertical alignment. + /// </summary> + /// <param name="align">The <see cref="Align"/> object.</param> + /// <param name="vertical">The vertical alignment, or <c>null</c> for no vertical alignment.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static Align VerticalAlignment(this Align align, VerticalAlignment? vertical) + { + if (align is null) + { + throw new ArgumentNullException(nameof(align)); + } + + align.Vertical = vertical; + return align; + } + + /// <summary> + /// Sets the <see cref="Align"/> object to be top aligned. + /// </summary> + /// <param name="align">The <see cref="Align"/> object.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static Align TopAligned(this Align align) + { + if (align is null) + { + throw new ArgumentNullException(nameof(align)); + } + + align.Vertical = Console.VerticalAlignment.Top; + return align; + } + + /// <summary> + /// Sets the <see cref="Align"/> object to be middle aligned. + /// </summary> + /// <param name="align">The <see cref="Align"/> object.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static Align MiddleAligned(this Align align) + { + if (align is null) + { + throw new ArgumentNullException(nameof(align)); + } + + align.Vertical = Console.VerticalAlignment.Middle; + return align; + } + + /// <summary> + /// Sets the <see cref="Align"/> object to be bottom aligned. + /// </summary> + /// <param name="align">The <see cref="Align"/> object.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static Align BottomAligned(this Align align) + { + if (align is null) + { + throw new ArgumentNullException(nameof(align)); + } + + align.Vertical = Console.VerticalAlignment.Bottom; + return align; + } +} diff --git a/src/Spectre.Console/Extensions/AlignableExtensions.cs b/src/Spectre.Console/Extensions/AlignableExtensions.cs index 4efc934..8039f31 100644 --- a/src/Spectre.Console/Extensions/AlignableExtensions.cs +++ b/src/Spectre.Console/Extensions/AlignableExtensions.cs @@ -77,4 +77,4 @@ public static class AlignableExtensions obj.Alignment = Justify.Right; return obj; } -} \ No newline at end of file +} diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Screen.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Screen.cs index f41ba46..e562cba 100644 --- a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Screen.cs +++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Screen.cs @@ -27,19 +27,18 @@ public static partial class AnsiConsoleExtensions throw new NotSupportedException("Alternate buffers are not supported by your terminal."); } - console.ExclusivityMode.Run<object?>(() => - { - // Switch to alternate screen - console.Write(new ControlCode("\u001b[?1049h\u001b[H")); + // Switch to alternate screen + console.Write(new ControlCode("\u001b[?1049h\u001b[H")); + try + { // Execute custom action action(); - + } + finally + { // Switch back to primary screen console.Write(new ControlCode("\u001b[?1049l")); - - // Dummy result - return null; - }); + } } } \ No newline at end of file diff --git a/src/Spectre.Console/Extensions/HasJustificationExtensions.cs b/src/Spectre.Console/Extensions/HasJustificationExtensions.cs new file mode 100644 index 0000000..63b8c4d --- /dev/null +++ b/src/Spectre.Console/Extensions/HasJustificationExtensions.cs @@ -0,0 +1,80 @@ +namespace Spectre.Console; + +/// <summary> +/// Contains extension methods for <see cref="IHasJustification"/>. +/// </summary> +public static class HasJustificationExtensions +{ + /// <summary> + /// Sets the justification for an <see cref="IHasJustification"/> object. + /// </summary> + /// <typeparam name="T">The type that can be justified.</typeparam> + /// <param name="obj">The alignable object.</param> + /// <param name="alignment">The alignment.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static T Justify<T>(this T obj, Justify? alignment) + where T : class, IHasJustification + { + if (obj is null) + { + throw new System.ArgumentNullException(nameof(obj)); + } + + obj.Justification = alignment; + return obj; + } + + /// <summary> + /// Sets the <see cref="IHasJustification"/> object to be left justified. + /// </summary> + /// <typeparam name="T">The type that can be justified.</typeparam> + /// <param name="obj">The alignable object.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static T LeftJustified<T>(this T obj) + where T : class, IHasJustification + { + if (obj is null) + { + throw new System.ArgumentNullException(nameof(obj)); + } + + obj.Justification = Console.Justify.Left; + return obj; + } + + /// <summary> + /// Sets the <see cref="IHasJustification"/> object to be centered. + /// </summary> + /// <typeparam name="T">The type that can be justified.</typeparam> + /// <param name="obj">The alignable object.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static T Centered<T>(this T obj) + where T : class, IHasJustification + { + if (obj is null) + { + throw new System.ArgumentNullException(nameof(obj)); + } + + obj.Justification = Console.Justify.Center; + return obj; + } + + /// <summary> + /// Sets the <see cref="IHasJustification"/> object to be right justified. + /// </summary> + /// <typeparam name="T">The type that can be justified.</typeparam> + /// <param name="obj">The alignable object.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static T RightJustified<T>(this T obj) + where T : class, IHasJustification + { + if (obj is null) + { + throw new System.ArgumentNullException(nameof(obj)); + } + + obj.Justification = Console.Justify.Right; + return obj; + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Extensions/LayoutExtensions.cs b/src/Spectre.Console/Extensions/LayoutExtensions.cs new file mode 100644 index 0000000..6acea7b --- /dev/null +++ b/src/Spectre.Console/Extensions/LayoutExtensions.cs @@ -0,0 +1,58 @@ +namespace Spectre.Console; + +/// <summary> +/// Contains extension methods for <see cref="Layout"/>. +/// </summary> +public static class LayoutExtensions +{ + /// <summary> + /// Sets the ratio of the layout. + /// </summary> + /// <param name="layout">The layout.</param> + /// <param name="ratio">The ratio.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static Layout Ratio(this Layout layout, int ratio) + { + if (layout is null) + { + throw new ArgumentNullException(nameof(layout)); + } + + layout.Ratio = ratio; + return layout; + } + + /// <summary> + /// Sets the size of the layout. + /// </summary> + /// <param name="layout">The layout.</param> + /// <param name="size">The size.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static Layout Size(this Layout layout, int size) + { + if (layout is null) + { + throw new ArgumentNullException(nameof(layout)); + } + + layout.Size = size; + return layout; + } + + /// <summary> + /// Sets the minimum width of the layout. + /// </summary> + /// <param name="layout">The layout.</param> + /// <param name="size">The size.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static Layout MinimumSize(this Layout layout, int size) + { + if (layout is null) + { + throw new ArgumentNullException(nameof(layout)); + } + + layout.MinimumSize = size; + return layout; + } +} diff --git a/src/Spectre.Console/Extensions/PanelExtensions.cs b/src/Spectre.Console/Extensions/PanelExtensions.cs index cc957e4..d837465 100644 --- a/src/Spectre.Console/Extensions/PanelExtensions.cs +++ b/src/Spectre.Console/Extensions/PanelExtensions.cs @@ -24,7 +24,7 @@ public static class PanelExtensions throw new ArgumentNullException(nameof(text)); } - alignment ??= panel.Header?.Alignment; + alignment ??= panel.Header?.Justification; return Header(panel, new PanelHeader(text, alignment)); } @@ -44,7 +44,7 @@ public static class PanelExtensions if (panel.Header != null) { // Update existing style - panel.Header.Alignment = alignment; + panel.Header.Justification = alignment; } else { diff --git a/src/Spectre.Console/Extensions/RenderOptionsExtensions.cs b/src/Spectre.Console/Extensions/RenderOptionsExtensions.cs new file mode 100644 index 0000000..6e501da --- /dev/null +++ b/src/Spectre.Console/Extensions/RenderOptionsExtensions.cs @@ -0,0 +1,10 @@ +namespace Spectre.Console; + +internal static class RenderOptionsExtensions +{ + public static BoxBorder GetSafeBorder<T>(this RenderOptions options, T border) + where T : IHasBoxBorder, IHasBorder + { + return BoxExtensions.GetSafeBorder(border.Border, !options.Unicode && border.UseSafeBorder); + } +} diff --git a/src/Spectre.Console/Extensions/RenderableExtensions.cs b/src/Spectre.Console/Extensions/RenderableExtensions.cs index 9e72be9..4761b2a 100644 --- a/src/Spectre.Console/Extensions/RenderableExtensions.cs +++ b/src/Spectre.Console/Extensions/RenderableExtensions.cs @@ -23,13 +23,13 @@ public static class RenderableExtensions throw new ArgumentNullException(nameof(renderable)); } - var context = new RenderContext(console.Profile.Capabilities); + var context = RenderOptions.Create(console, console.Profile.Capabilities); var renderables = console.Pipeline.Process(context, new[] { renderable }); return GetSegments(console, context, renderables); } - private static IEnumerable<Segment> GetSegments(IAnsiConsole console, RenderContext options, IEnumerable<IRenderable> renderables) + private static IEnumerable<Segment> GetSegments(IAnsiConsole console, RenderOptions options, IEnumerable<IRenderable> renderables) { var result = new List<Segment>(); foreach (var renderable in renderables) diff --git a/src/Spectre.Console/Extensions/VisibilityExtensions.cs b/src/Spectre.Console/Extensions/VisibilityExtensions.cs new file mode 100644 index 0000000..38bc04b --- /dev/null +++ b/src/Spectre.Console/Extensions/VisibilityExtensions.cs @@ -0,0 +1,43 @@ +namespace Spectre.Console; + +/// <summary> +/// Contains extension methods for <see cref="IHasVisibility"/>. +/// </summary> +public static class VisibilityExtensions +{ + /// <summary> + /// Marks the specified object as being invisible. + /// </summary> + /// <typeparam name="T">An object implementing <see cref="IHasVisibility"/>.</typeparam> + /// <param name="obj">The object to hide.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static T Invisible<T>(this T obj) + where T : class, IHasVisibility + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + obj.IsVisible = false; + return obj; + } + + /// <summary> + /// Marks the specified object as being visible. + /// </summary> + /// <typeparam name="T">An object implementing <see cref="IHasVisibility"/>.</typeparam> + /// <param name="obj">The object to show.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public static T Visible<T>(this T obj) + where T : class, IHasVisibility + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + obj.IsVisible = true; + return obj; + } +} \ No newline at end of file diff --git a/src/Spectre.Console/HorizontalAlignment.cs b/src/Spectre.Console/HorizontalAlignment.cs new file mode 100644 index 0000000..e8f6c21 --- /dev/null +++ b/src/Spectre.Console/HorizontalAlignment.cs @@ -0,0 +1,22 @@ +namespace Spectre.Console; + +/// <summary> +/// Represents horizontal alignment. +/// </summary> +public enum HorizontalAlignment +{ + /// <summary> + /// Left aligned. + /// </summary> + Left, + + /// <summary> + /// Centered. + /// </summary> + Center, + + /// <summary> + /// Right aligned. + /// </summary> + Right, +} diff --git a/src/Spectre.Console/IAlignable.cs b/src/Spectre.Console/IAlignable.cs index 1f4f0c0..6207bfa 100644 --- a/src/Spectre.Console/IAlignable.cs +++ b/src/Spectre.Console/IAlignable.cs @@ -9,4 +9,4 @@ public interface IAlignable /// Gets or sets the alignment. /// </summary> Justify? Alignment { get; set; } -} \ No newline at end of file +} diff --git a/src/Spectre.Console/IHasJustification.cs b/src/Spectre.Console/IHasJustification.cs new file mode 100644 index 0000000..72b75db --- /dev/null +++ b/src/Spectre.Console/IHasJustification.cs @@ -0,0 +1,12 @@ +namespace Spectre.Console; + +/// <summary> +/// Represents something that has justification. +/// </summary> +public interface IHasJustification +{ + /// <summary> + /// Gets or sets the justification. + /// </summary> + Justify? Justification { get; set; } +} \ No newline at end of file diff --git a/src/Spectre.Console/IHasVisibility.cs b/src/Spectre.Console/IHasVisibility.cs new file mode 100644 index 0000000..e6bb2e3 --- /dev/null +++ b/src/Spectre.Console/IHasVisibility.cs @@ -0,0 +1,13 @@ +namespace Spectre.Console; + +/// <summary> +/// Represents something that can be hidden. +/// </summary> +public interface IHasVisibility +{ + /// <summary> + /// Gets or sets a value indicating whether or not the object should + /// be visible or not. + /// </summary> + bool IsVisible { get; set; } +} \ No newline at end of file diff --git a/src/Spectre.Console/IRenderable.cs b/src/Spectre.Console/IRenderable.cs index af27d38..6e5b281 100644 --- a/src/Spectre.Console/IRenderable.cs +++ b/src/Spectre.Console/IRenderable.cs @@ -8,16 +8,16 @@ public interface IRenderable /// <summary> /// Measures the renderable object. /// </summary> - /// <param name="context">The render context.</param> + /// <param name="options">The render options.</param> /// <param name="maxWidth">The maximum allowed width.</param> /// <returns>The minimum and maximum width of the object.</returns> - Measurement Measure(RenderContext context, int maxWidth); + Measurement Measure(RenderOptions options, int maxWidth); /// <summary> /// Renders the object. /// </summary> - /// <param name="context">The render context.</param> + /// <param name="options">The render options.</param> /// <param name="maxWidth">The maximum allowed width.</param> /// <returns>A collection of segments.</returns> - IEnumerable<Segment> Render(RenderContext context, int maxWidth); + IEnumerable<Segment> Render(RenderOptions options, int maxWidth); } \ No newline at end of file diff --git a/src/Spectre.Console/Internal/Aligner.cs b/src/Spectre.Console/Internal/Aligner.cs index 92c7ff3..760c7b2 100644 --- a/src/Spectre.Console/Internal/Aligner.cs +++ b/src/Spectre.Console/Internal/Aligner.cs @@ -89,4 +89,51 @@ internal static class Aligner throw new NotSupportedException("Unknown alignment"); } } + + public static void AlignHorizontally<T>(T segments, HorizontalAlignment alignment, int maxWidth) + where T : List<Segment> + { + var width = Segment.CellCount(segments); + if (width >= maxWidth) + { + return; + } + + switch (alignment) + { + case HorizontalAlignment.Left: + { + var diff = maxWidth - width; + segments.Add(Segment.Padding(diff)); + break; + } + + case HorizontalAlignment.Right: + { + var diff = maxWidth - width; + segments.Insert(0, Segment.Padding(diff)); + break; + } + + case HorizontalAlignment.Center: + { + // Left side. + var diff = (maxWidth - width) / 2; + segments.Insert(0, Segment.Padding(diff)); + + // Right side + segments.Add(Segment.Padding(diff)); + var remainder = (maxWidth - width) % 2; + if (remainder != 0) + { + segments.Add(Segment.Padding(remainder)); + } + + break; + } + + default: + throw new NotSupportedException("Unknown alignment"); + } + } } \ No newline at end of file diff --git a/src/Spectre.Console/Internal/IRatioResolvable.cs b/src/Spectre.Console/Internal/IRatioResolvable.cs new file mode 100644 index 0000000..83d89d0 --- /dev/null +++ b/src/Spectre.Console/Internal/IRatioResolvable.cs @@ -0,0 +1,22 @@ +namespace Spectre.Console; + +/// <summary> +/// Represents something that can be used to resolve ratios. +/// </summary> +internal interface IRatioResolvable +{ + /// <summary> + /// Gets the ratio. + /// </summary> + int Ratio { get; } + + /// <summary> + /// Gets the size. + /// </summary> + int? Size { get; } + + /// <summary> + /// Gets the minimum size. + /// </summary> + int MinimumSize { get; } +} diff --git a/src/Spectre.Console/Internal/Polyfill/IsExternalInit.cs b/src/Spectre.Console/Internal/Polyfill/IsExternalInit.cs new file mode 100644 index 0000000..e152620 --- /dev/null +++ b/src/Spectre.Console/Internal/Polyfill/IsExternalInit.cs @@ -0,0 +1,15 @@ +#if NETSTANDARD2_0 +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + /// <summary> + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// </summary> + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit + { + } +} +#endif \ No newline at end of file diff --git a/src/Spectre.Console/Internal/Ratio.cs b/src/Spectre.Console/Internal/Ratio.cs index a0f3fc9..5865b72 100644 --- a/src/Spectre.Console/Internal/Ratio.cs +++ b/src/Spectre.Console/Internal/Ratio.cs @@ -5,6 +5,78 @@ namespace Spectre.Console; internal static class Ratio { + public static List<int> Resolve(int total, IEnumerable<IRatioResolvable> edges) + { + static (int Div, float Mod) DivMod(float x, float y) + { + return ((int)(x / y), x % y); + } + + static int? GetEdgeWidth(IRatioResolvable edge) + { + if (edge.Size != null && edge.Size < edge.MinimumSize) + { + return edge.MinimumSize; + } + + return edge.Size; + } + + var sizes = edges.Select(x => GetEdgeWidth(x)).ToArray(); + + while (sizes.Any(s => s == null)) + { + // Get all edges and map them back to their index. + // Ignore edges which have a explicit size. + var flexibleEdges = sizes.Zip(edges, (a, b) => (Size: a, Edge: b)) + .Enumerate() + .Select(x => (x.Index, x.Item.Size, x.Item.Edge)) + .Where(x => x.Size == null) + .ToList(); + + // Get the remaining space + var remaining = total - sizes.Select(size => size ?? 0).Sum(); + if (remaining <= 0) + { + // No more room for flexible edges. + return sizes + .Zip(edges, (size, edge) => (Size: size, Edge: edge)) + .Select(zip => zip.Size ?? zip.Edge.MinimumSize) + .Select(size => size > 0 ? size : 1) + .ToList(); + } + + var portion = (float)remaining / flexibleEdges.Sum(x => Math.Max(1, x.Edge.Ratio)); + + var invalidate = false; + foreach (var (index, size, edge) in flexibleEdges) + { + if (portion * edge.Ratio <= edge.MinimumSize) + { + sizes[index] = edge.MinimumSize; + + // New fixed size will invalidate calculations, + // so we need to repeat the process + invalidate = true; + break; + } + } + + if (!invalidate) + { + var remainder = 0f; + foreach (var flexibleEdge in flexibleEdges) + { + var (div, mod) = DivMod((portion * flexibleEdge.Edge.Ratio) + remainder, 1); + remainder = mod; + sizes[flexibleEdge.Index] = div; + } + } + } + + return sizes.Select(x => x ?? 1).ToList(); + } + public static List<int> Reduce(int total, List<int> ratios, List<int> maximums, List<int> values) { ratios = ratios.Zip(maximums, (a, b) => (ratio: a, max: b)).Select(a => a.max > 0 ? a.ratio : 0).ToList(); diff --git a/src/Spectre.Console/Internal/Text/Encoding/HtmlEncoder.cs b/src/Spectre.Console/Internal/Text/Encoding/HtmlEncoder.cs index d021e26..31f36a0 100644 --- a/src/Spectre.Console/Internal/Text/Encoding/HtmlEncoder.cs +++ b/src/Spectre.Console/Internal/Text/Encoding/HtmlEncoder.cs @@ -4,7 +4,7 @@ internal sealed class HtmlEncoder : IAnsiConsoleEncoder { public string Encode(IAnsiConsole console, IEnumerable<IRenderable> renderables) { - var context = new RenderContext(new EncoderCapabilities(ColorSystem.TrueColor)); + var context = RenderOptions.Create(console, new EncoderCapabilities(ColorSystem.TrueColor)); var builder = new StringBuilder(); builder.Append("<pre style=\"font-size:90%;font-family:consolas,'Courier New',monospace\">\n"); diff --git a/src/Spectre.Console/Internal/Text/Encoding/TextEncoder.cs b/src/Spectre.Console/Internal/Text/Encoding/TextEncoder.cs index ac2bc61..2570699 100644 --- a/src/Spectre.Console/Internal/Text/Encoding/TextEncoder.cs +++ b/src/Spectre.Console/Internal/Text/Encoding/TextEncoder.cs @@ -4,7 +4,7 @@ internal sealed class TextEncoder : IAnsiConsoleEncoder { public string Encode(IAnsiConsole console, IEnumerable<IRenderable> renderables) { - var context = new RenderContext(new EncoderCapabilities(ColorSystem.TrueColor)); + var context = RenderOptions.Create(console, new EncoderCapabilities(ColorSystem.TrueColor)); var builder = new StringBuilder(); foreach (var renderable in renderables) diff --git a/src/Spectre.Console/Justify.cs b/src/Spectre.Console/Justify.cs index dc81437..a4578de 100644 --- a/src/Spectre.Console/Justify.cs +++ b/src/Spectre.Console/Justify.cs @@ -6,12 +6,12 @@ namespace Spectre.Console; public enum Justify { /// <summary> - /// Left aligned. + /// Left justified. /// </summary> Left = 0, /// <summary> - /// Right aligned. + /// Right justified. /// </summary> Right = 1, diff --git a/src/Spectre.Console/Live/LiveDisplayRenderer.cs b/src/Spectre.Console/Live/LiveDisplayRenderer.cs index 3a95df1..a9a1a52 100644 --- a/src/Spectre.Console/Live/LiveDisplayRenderer.cs +++ b/src/Spectre.Console/Live/LiveDisplayRenderer.cs @@ -41,7 +41,7 @@ internal sealed class LiveDisplayRenderer : IRenderHook } } - public IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables) + public IEnumerable<IRenderable> Process(RenderOptions options, IEnumerable<IRenderable> renderables) { lock (_context.Lock) { diff --git a/src/Spectre.Console/Live/LiveRenderable.cs b/src/Spectre.Console/Live/LiveRenderable.cs index 9152540..40107f9 100644 --- a/src/Spectre.Console/Live/LiveRenderable.cs +++ b/src/Spectre.Console/Live/LiveRenderable.cs @@ -67,7 +67,7 @@ internal sealed class LiveRenderable : Renderable } } - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { lock (_lock) { @@ -75,10 +75,10 @@ internal sealed class LiveRenderable : Renderable if (_renderable != null) { - var segments = _renderable.Render(context, maxWidth); + var segments = _renderable.Render(options, maxWidth); var lines = Segment.SplitLines(segments); - var shape = SegmentShape.Calculate(context, lines); + var shape = SegmentShape.Calculate(options, lines); if (shape.Height > _console.Profile.Height) { if (Overflow == VerticalOverflow.Crop) @@ -97,12 +97,12 @@ internal sealed class LiveRenderable : Renderable lines.RemoveRange(0, start); } - shape = SegmentShape.Calculate(context, lines); + shape = SegmentShape.Calculate(options, lines); } else if (Overflow == VerticalOverflow.Ellipsis) { var ellipsisText = _console.Profile.Capabilities.Unicode ? "…" : "..."; - var ellipsis = new SegmentLine(((IRenderable)new Markup($"[yellow]{ellipsisText}[/]")).Render(context, maxWidth)); + var ellipsis = new SegmentLine(((IRenderable)new Markup($"[yellow]{ellipsisText}[/]")).Render(options, maxWidth)); if (OverflowCropping == VerticalOverflowCropping.Bottom) { @@ -120,14 +120,14 @@ internal sealed class LiveRenderable : Renderable lines.Insert(0, ellipsis); } - shape = SegmentShape.Calculate(context, lines); + shape = SegmentShape.Calculate(options, lines); } DidOverflow = true; } _shape = _shape == null ? shape : _shape.Value.Inflate(shape); - _shape.Value.Apply(context, ref lines); + _shape.Value.Apply(options, ref lines); foreach (var (_, _, last, line) in lines.Enumerate()) { diff --git a/src/Spectre.Console/Live/Progress/Columns/DownloadedColumn.cs b/src/Spectre.Console/Live/Progress/Columns/DownloadedColumn.cs index fa8e496..6162986 100644 --- a/src/Spectre.Console/Live/Progress/Columns/DownloadedColumn.cs +++ b/src/Spectre.Console/Live/Progress/Columns/DownloadedColumn.cs @@ -11,7 +11,7 @@ public sealed class DownloadedColumn : ProgressColumn public CultureInfo? Culture { get; set; } /// <inheritdoc/> - public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime) + public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime) { var total = new FileSize(task.MaxValue); diff --git a/src/Spectre.Console/Live/Progress/Columns/ElapsedTimeColumn.cs b/src/Spectre.Console/Live/Progress/Columns/ElapsedTimeColumn.cs index a005be6..a29fdfb 100644 --- a/src/Spectre.Console/Live/Progress/Columns/ElapsedTimeColumn.cs +++ b/src/Spectre.Console/Live/Progress/Columns/ElapsedTimeColumn.cs @@ -14,7 +14,7 @@ public sealed class ElapsedTimeColumn : ProgressColumn public Style Style { get; set; } = new Style(foreground: Color.Blue); /// <inheritdoc/> - public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime) + public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime) { var elapsed = task.ElapsedTime; if (elapsed == null) @@ -31,7 +31,7 @@ public sealed class ElapsedTimeColumn : ProgressColumn } /// <inheritdoc/> - public override int? GetColumnWidth(RenderContext context) + public override int? GetColumnWidth(RenderOptions options) { return 8; } diff --git a/src/Spectre.Console/Live/Progress/Columns/PercentageColumn.cs b/src/Spectre.Console/Live/Progress/Columns/PercentageColumn.cs index f0bf597..0dbef1c 100644 --- a/src/Spectre.Console/Live/Progress/Columns/PercentageColumn.cs +++ b/src/Spectre.Console/Live/Progress/Columns/PercentageColumn.cs @@ -16,15 +16,15 @@ public sealed class PercentageColumn : ProgressColumn public Style CompletedStyle { get; set; } = new Style(foreground: Color.Green); /// <inheritdoc/> - public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime) + public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime) { var percentage = (int)task.Percentage; var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain; - return new Text($"{percentage}%", style).RightAligned(); + return new Text($"{percentage}%", style).RightJustified(); } /// <inheritdoc/> - public override int? GetColumnWidth(RenderContext context) + public override int? GetColumnWidth(RenderOptions options) { return 4; } diff --git a/src/Spectre.Console/Live/Progress/Columns/ProgressBarColumn.cs b/src/Spectre.Console/Live/Progress/Columns/ProgressBarColumn.cs index 7b0b018..16c7c19 100644 --- a/src/Spectre.Console/Live/Progress/Columns/ProgressBarColumn.cs +++ b/src/Spectre.Console/Live/Progress/Columns/ProgressBarColumn.cs @@ -31,7 +31,7 @@ public sealed class ProgressBarColumn : ProgressColumn public Style IndeterminateStyle { get; set; } = ProgressBar.DefaultPulseStyle; /// <inheritdoc/> - public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime) + public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime) { return new ProgressBar { diff --git a/src/Spectre.Console/Live/Progress/Columns/RemainingTimeColumn.cs b/src/Spectre.Console/Live/Progress/Columns/RemainingTimeColumn.cs index 49aeae5..3ce467a 100644 --- a/src/Spectre.Console/Live/Progress/Columns/RemainingTimeColumn.cs +++ b/src/Spectre.Console/Live/Progress/Columns/RemainingTimeColumn.cs @@ -14,7 +14,7 @@ public sealed class RemainingTimeColumn : ProgressColumn public Style Style { get; set; } = new Style(foreground: Color.Blue); /// <inheritdoc/> - public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime) + public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime) { var remaining = task.RemainingTime; if (remaining == null) @@ -31,7 +31,7 @@ public sealed class RemainingTimeColumn : ProgressColumn } /// <inheritdoc/> - public override int? GetColumnWidth(RenderContext context) + public override int? GetColumnWidth(RenderOptions options) { return 8; } diff --git a/src/Spectre.Console/Live/Progress/Columns/SpinnerColumn.cs b/src/Spectre.Console/Live/Progress/Columns/SpinnerColumn.cs index 429d484..425de14 100644 --- a/src/Spectre.Console/Live/Progress/Columns/SpinnerColumn.cs +++ b/src/Spectre.Console/Live/Progress/Columns/SpinnerColumn.cs @@ -95,9 +95,9 @@ public sealed class SpinnerColumn : ProgressColumn } /// <inheritdoc/> - public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime) + public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime) { - var useAscii = !context.Unicode && _spinner.IsUnicode; + var useAscii = !options.Unicode && _spinner.IsUnicode; var spinner = useAscii ? Spinner.Known.Ascii : _spinner ?? Spinner.Known.Default; if (!task.IsStarted) @@ -123,24 +123,24 @@ public sealed class SpinnerColumn : ProgressColumn } /// <inheritdoc/> - public override int? GetColumnWidth(RenderContext context) + public override int? GetColumnWidth(RenderOptions options) { - return GetMaxWidth(context); + return GetMaxWidth(options); } - private int GetMaxWidth(RenderContext context) + private int GetMaxWidth(RenderOptions options) { lock (_lock) { if (_maxWidth == null) { - var useAscii = !context.Unicode && _spinner.IsUnicode; + var useAscii = !options.Unicode && _spinner.IsUnicode; var spinner = useAscii ? Spinner.Known.Ascii : _spinner ?? Spinner.Known.Default; _maxWidth = Math.Max( Math.Max( - ((IRenderable)new Markup(PendingText ?? " ")).Measure(context, int.MaxValue).Max, - ((IRenderable)new Markup(CompletedText ?? " ")).Measure(context, int.MaxValue).Max), + ((IRenderable)new Markup(PendingText ?? " ")).Measure(options, int.MaxValue).Max, + ((IRenderable)new Markup(CompletedText ?? " ")).Measure(options, int.MaxValue).Max), spinner.Frames.Max(frame => Cell.GetCellLength(frame))); } diff --git a/src/Spectre.Console/Live/Progress/Columns/TaskDescriptionColumn.cs b/src/Spectre.Console/Live/Progress/Columns/TaskDescriptionColumn.cs index b6ffb0d..4e3c39f 100644 --- a/src/Spectre.Console/Live/Progress/Columns/TaskDescriptionColumn.cs +++ b/src/Spectre.Console/Live/Progress/Columns/TaskDescriptionColumn.cs @@ -14,9 +14,9 @@ public sealed class TaskDescriptionColumn : ProgressColumn public Justify Alignment { get; set; } = Justify.Right; /// <inheritdoc/> - public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime) + public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime) { var text = task.Description?.RemoveNewLines()?.Trim(); - return new Markup(text ?? string.Empty).Overflow(Overflow.Ellipsis).Alignment(Alignment); + return new Markup(text ?? string.Empty).Overflow(Overflow.Ellipsis).Justify(Alignment); } } \ No newline at end of file diff --git a/src/Spectre.Console/Live/Progress/Columns/TransferSpeedColumn.cs b/src/Spectre.Console/Live/Progress/Columns/TransferSpeedColumn.cs index 7e6e783..84f2743 100644 --- a/src/Spectre.Console/Live/Progress/Columns/TransferSpeedColumn.cs +++ b/src/Spectre.Console/Live/Progress/Columns/TransferSpeedColumn.cs @@ -11,7 +11,7 @@ public sealed class TransferSpeedColumn : ProgressColumn public CultureInfo? Culture { get; set; } /// <inheritdoc/> - public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime) + public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime) { if (task.Speed == null) { diff --git a/src/Spectre.Console/Live/Progress/ProgressColumn.cs b/src/Spectre.Console/Live/Progress/ProgressColumn.cs index 2aa593d..c10538e 100644 --- a/src/Spectre.Console/Live/Progress/ProgressColumn.cs +++ b/src/Spectre.Console/Live/Progress/ProgressColumn.cs @@ -13,18 +13,18 @@ public abstract class ProgressColumn /// <summary> /// Gets a renderable representing the column. /// </summary> - /// <param name="context">The render context.</param> + /// <param name="options">The render options.</param> /// <param name="task">The task.</param> /// <param name="deltaTime">The elapsed time since last call.</param> /// <returns>A renderable representing the column.</returns> - public abstract IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime); + public abstract IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime); /// <summary> /// Gets the width of the column. /// </summary> - /// <param name="context">The context.</param> + /// <param name="options">The render options.</param> /// <returns>The width of the column, or <c>null</c> to calculate.</returns> - public virtual int? GetColumnWidth(RenderContext context) + public virtual int? GetColumnWidth(RenderOptions options) { return null; } diff --git a/src/Spectre.Console/Live/Progress/ProgressRenderer.cs b/src/Spectre.Console/Live/Progress/ProgressRenderer.cs index 598ce5a..5840b20 100644 --- a/src/Spectre.Console/Live/Progress/ProgressRenderer.cs +++ b/src/Spectre.Console/Live/Progress/ProgressRenderer.cs @@ -13,5 +13,5 @@ internal abstract class ProgressRenderer : IRenderHook } public abstract void Update(ProgressContext context); - public abstract IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables); + public abstract IEnumerable<IRenderable> Process(RenderOptions options, IEnumerable<IRenderable> renderables); } \ No newline at end of file diff --git a/src/Spectre.Console/Live/Progress/Renderers/DefaultProgressRenderer.cs b/src/Spectre.Console/Live/Progress/Renderers/DefaultProgressRenderer.cs index 9715402..8155751 100644 --- a/src/Spectre.Console/Live/Progress/Renderers/DefaultProgressRenderer.cs +++ b/src/Spectre.Console/Live/Progress/Renderers/DefaultProgressRenderer.cs @@ -64,7 +64,7 @@ internal sealed class DefaultProgressRenderer : ProgressRenderer _stopwatch.Start(); } - var renderContext = new RenderContext(_console.Profile.Capabilities); + var renderContext = RenderOptions.Create(_console, _console.Profile.Capabilities); var delta = _stopwatch.Elapsed - _lastUpdate; _lastUpdate = _stopwatch.Elapsed; @@ -105,7 +105,7 @@ internal sealed class DefaultProgressRenderer : ProgressRenderer } } - public override IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables) + public override IEnumerable<IRenderable> Process(RenderOptions options, IEnumerable<IRenderable> renderables) { lock (_lock) { diff --git a/src/Spectre.Console/Live/Progress/Renderers/FallbackProgressRenderer.cs b/src/Spectre.Console/Live/Progress/Renderers/FallbackProgressRenderer.cs index 90c54b3..e40f54c 100644 --- a/src/Spectre.Console/Live/Progress/Renderers/FallbackProgressRenderer.cs +++ b/src/Spectre.Console/Live/Progress/Renderers/FallbackProgressRenderer.cs @@ -58,7 +58,7 @@ internal sealed class FallbackProgressRenderer : ProgressRenderer } } - public override IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables) + public override IEnumerable<IRenderable> Process(RenderOptions options, IEnumerable<IRenderable> renderables) { lock (_lock) { diff --git a/src/Spectre.Console/Live/Progress/Renderers/FallbackStatusRenderer.cs b/src/Spectre.Console/Live/Progress/Renderers/FallbackStatusRenderer.cs index f8f445e..69e025c 100644 --- a/src/Spectre.Console/Live/Progress/Renderers/FallbackStatusRenderer.cs +++ b/src/Spectre.Console/Live/Progress/Renderers/FallbackStatusRenderer.cs @@ -34,7 +34,7 @@ internal sealed class FallbackStatusRenderer : ProgressRenderer } } - public override IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables) + public override IEnumerable<IRenderable> Process(RenderOptions options, IEnumerable<IRenderable> renderables) { lock (_lock) { diff --git a/src/Spectre.Console/Prompts/List/ListPromptRenderHook.cs b/src/Spectre.Console/Prompts/List/ListPromptRenderHook.cs index 53fa21f..090a2f6 100644 --- a/src/Spectre.Console/Prompts/List/ListPromptRenderHook.cs +++ b/src/Spectre.Console/Prompts/List/ListPromptRenderHook.cs @@ -32,7 +32,7 @@ internal sealed class ListPromptRenderHook<T> : IRenderHook _console.Write(new ControlCode(string.Empty)); } - public IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables) + public IEnumerable<IRenderable> Process(RenderOptions options, IEnumerable<IRenderable> renderables) { lock (_lock) { diff --git a/src/Spectre.Console/Region.cs b/src/Spectre.Console/Region.cs new file mode 100644 index 0000000..a5aeb05 --- /dev/null +++ b/src/Spectre.Console/Region.cs @@ -0,0 +1,43 @@ +namespace Spectre.Console; + +/// <summary> +/// Represents a region. +/// </summary> +[DebuggerDisplay("[X={X,nq}, Y={Y,nq}, W={Width,nq}, H={Height,nq}]")] +public struct Region +{ + /// <summary> + /// Gets the x-coordinate. + /// </summary> + public int X { get; } + + /// <summary> + /// Gets the y-coordinate. + /// </summary> + public int Y { get; } + + /// <summary> + /// Gets the width. + /// </summary> + public int Width { get; } + + /// <summary> + /// Gets the height. + /// </summary> + public int Height { get; } + + /// <summary> + /// Initializes a new instance of the <see cref="Region"/> struct. + /// </summary> + /// <param name="x">The x-coordinate.</param> + /// <param name="y">The y-coordinate.</param> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> + public Region(int x, int y, int width, int height) + { + X = x; + Y = y; + Width = width; + Height = height; + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Rendering/IRenderHook.cs b/src/Spectre.Console/Rendering/IRenderHook.cs index 417a500..49c92d6 100644 --- a/src/Spectre.Console/Rendering/IRenderHook.cs +++ b/src/Spectre.Console/Rendering/IRenderHook.cs @@ -8,8 +8,8 @@ public interface IRenderHook /// <summary> /// Processes the specified renderables. /// </summary> - /// <param name="context">The render context.</param> + /// <param name="options">The render options.</param> /// <param name="renderables">The renderables to process.</param> /// <returns>The processed renderables.</returns> - IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables); + IEnumerable<IRenderable> Process(RenderOptions options, IEnumerable<IRenderable> renderables); } \ No newline at end of file diff --git a/src/Spectre.Console/Rendering/JustInTimeRenderable.cs b/src/Spectre.Console/Rendering/JustInTimeRenderable.cs index dd85e40..f30b370 100644 --- a/src/Spectre.Console/Rendering/JustInTimeRenderable.cs +++ b/src/Spectre.Console/Rendering/JustInTimeRenderable.cs @@ -10,13 +10,13 @@ public abstract class JustInTimeRenderable : Renderable private IRenderable? _rendered; /// <inheritdoc/> - protected sealed override Measurement Measure(RenderContext context, int maxWidth) + protected sealed override Measurement Measure(RenderOptions context, int maxWidth) { return GetInner().Measure(context, maxWidth); } /// <inheritdoc/> - protected sealed override IEnumerable<Segment> Render(RenderContext context, int width) + protected sealed override IEnumerable<Segment> Render(RenderOptions context, int width) { return GetInner().Render(context, width); } diff --git a/src/Spectre.Console/Rendering/RenderContext.cs b/src/Spectre.Console/Rendering/RenderContext.cs deleted file mode 100644 index c8d15f8..0000000 --- a/src/Spectre.Console/Rendering/RenderContext.cs +++ /dev/null @@ -1,78 +0,0 @@ -namespace Spectre.Console.Rendering; - -/// <summary> -/// Represents a render context. -/// </summary> -public sealed class RenderContext -{ - private readonly IReadOnlyCapabilities _capabilities; - - /// <summary> - /// Gets the current color system. - /// </summary> - public ColorSystem ColorSystem => _capabilities.ColorSystem; - - /// <summary> - /// Gets a value indicating whether or not VT/Ansi codes are supported. - /// </summary> - public bool Ansi => _capabilities.Ansi; - - /// <summary> - /// Gets a value indicating whether or not unicode is supported. - /// </summary> - public bool Unicode => _capabilities.Unicode; - - /// <summary> - /// Gets the current justification. - /// </summary> - public Justify? Justification { get; } - - /// <summary> - /// Gets a value indicating whether the context want items to render without - /// line breaks and return a single line where applicable. - /// </summary> - internal bool SingleLine { get; } - - /// <summary> - /// Initializes a new instance of the <see cref="RenderContext"/> class. - /// </summary> - /// <param name="capabilities">The capabilities.</param> - /// <param name="justification">The justification.</param> - public RenderContext(IReadOnlyCapabilities capabilities, Justify? justification = null) - : this(capabilities, justification, false) - { - } - - private RenderContext(IReadOnlyCapabilities capabilities, Justify? justification = null, bool singleLine = false) - { - _capabilities = capabilities ?? throw new ArgumentNullException(nameof(capabilities)); - - Justification = justification; - SingleLine = singleLine; - } - - /// <summary> - /// Creates a new context with the specified justification. - /// </summary> - /// <param name="justification">The justification.</param> - /// <returns>A new <see cref="RenderContext"/> instance.</returns> - public RenderContext WithJustification(Justify? justification) - { - return new RenderContext(_capabilities, justification, SingleLine); - } - - /// <summary> - /// Creates a new context that tell <see cref="IRenderable"/> instances - /// to not care about splitting things in new lines. Whether or not to - /// comply to the request is up to the item being rendered. - /// </summary> - /// <remarks> - /// Use with care since this has the potential to mess things up. - /// Only use this kind of context with items that you know about. - /// </remarks> - /// <returns>A new <see cref="RenderContext"/> instance.</returns> - internal RenderContext WithSingleLine() - { - return new RenderContext(_capabilities, Justification, true); - } -} \ No newline at end of file diff --git a/src/Spectre.Console/Rendering/RenderOptions.cs b/src/Spectre.Console/Rendering/RenderOptions.cs new file mode 100644 index 0000000..3dcf04a --- /dev/null +++ b/src/Spectre.Console/Rendering/RenderOptions.cs @@ -0,0 +1,63 @@ +namespace Spectre.Console.Rendering; + +/// <summary> +/// Represents render options. +/// </summary> +/// <param name="Capabilities">The capabilities.</param> +/// <param name="ConsoleSize">The console size.</param> +public record class RenderOptions(IReadOnlyCapabilities Capabilities, Size ConsoleSize) +{ + /// <summary> + /// Gets the current color system. + /// </summary> + public ColorSystem ColorSystem => Capabilities.ColorSystem; + + /// <summary> + /// Gets a value indicating whether or not VT/Ansi codes are supported. + /// </summary> + public bool Ansi => Capabilities.Ansi; + + /// <summary> + /// Gets a value indicating whether or not unicode is supported. + /// </summary> + public bool Unicode => Capabilities.Unicode; + + /// <summary> + /// Gets the current justification. + /// </summary> + public Justify? Justification { get; init; } + + /// <summary> + /// Gets the requested height. + /// </summary> + public int? Height { get; init; } + + /// <summary> + /// Gets a value indicating whether the context want items to render without + /// line breaks and return a single line where applicable. + /// </summary> + internal bool SingleLine { get; init; } + + /// <summary> + /// Creates a <see cref="RenderOptions"/> instance from a <see cref="IAnsiConsole"/>. + /// </summary> + /// <param name="console">The console.</param> + /// <param name="capabilities">The capabilities, or <c>null</c> to use the provided console's capabilities.</param> + /// <returns>A <see cref="RenderOptions"/> representing the provided <see cref="IAnsiConsole"/>.</returns> + public static RenderOptions Create(IAnsiConsole console, IReadOnlyCapabilities? capabilities = null) + { + if (console is null) + { + throw new ArgumentNullException(nameof(console)); + } + + return new RenderOptions( + capabilities ?? console.Profile.Capabilities, + new Size(console.Profile.Width, console.Profile.Height)) + { + Justification = null, + Height = null, + SingleLine = false, + }; + } +} diff --git a/src/Spectre.Console/Rendering/RenderPipeline.cs b/src/Spectre.Console/Rendering/RenderPipeline.cs index 367fffd..6865c1b 100644 --- a/src/Spectre.Console/Rendering/RenderPipeline.cs +++ b/src/Spectre.Console/Rendering/RenderPipeline.cs @@ -44,17 +44,17 @@ public sealed class RenderPipeline /// <summary> /// Processes the specified renderables. /// </summary> - /// <param name="context">The render context.</param> + /// <param name="options">The render options.</param> /// <param name="renderables">The renderables to process.</param> /// <returns>The processed renderables.</returns> - public IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables) + public IEnumerable<IRenderable> Process(RenderOptions options, IEnumerable<IRenderable> renderables) { lock (_lock) { var current = renderables; for (var index = _hooks.Count - 1; index >= 0; index--) { - current = _hooks[index].Process(context, current); + current = _hooks[index].Process(options, current); } return current; diff --git a/src/Spectre.Console/Rendering/Renderable.cs b/src/Spectre.Console/Rendering/Renderable.cs index 2b22f5d..1234053 100644 --- a/src/Spectre.Console/Rendering/Renderable.cs +++ b/src/Spectre.Console/Rendering/Renderable.cs @@ -7,25 +7,25 @@ public abstract class Renderable : IRenderable { /// <inheritdoc/> [DebuggerStepThrough] - Measurement IRenderable.Measure(RenderContext context, int maxWidth) + Measurement IRenderable.Measure(RenderOptions options, int maxWidth) { - return Measure(context, maxWidth); + return Measure(options, maxWidth); } /// <inheritdoc/> [DebuggerStepThrough] - IEnumerable<Segment> IRenderable.Render(RenderContext context, int maxWidth) + IEnumerable<Segment> IRenderable.Render(RenderOptions options, int maxWidth) { - return Render(context, maxWidth); + return Render(options, maxWidth); } /// <summary> /// Measures the renderable object. /// </summary> - /// <param name="context">The render context.</param> + /// <param name="options">The render options.</param> /// <param name="maxWidth">The maximum allowed width.</param> /// <returns>The minimum and maximum width of the object.</returns> - protected virtual Measurement Measure(RenderContext context, int maxWidth) + protected virtual Measurement Measure(RenderOptions options, int maxWidth) { return new Measurement(maxWidth, maxWidth); } @@ -33,8 +33,8 @@ public abstract class Renderable : IRenderable /// <summary> /// Renders the object. /// </summary> - /// <param name="context">The render context.</param> + /// <param name="options">The render options.</param> /// <param name="maxWidth">The maximum allowed width.</param> /// <returns>A collection of segments.</returns> - protected abstract IEnumerable<Segment> Render(RenderContext context, int maxWidth); + protected abstract IEnumerable<Segment> Render(RenderOptions options, int maxWidth); } \ No newline at end of file diff --git a/src/Spectre.Console/Rendering/Segment.cs b/src/Spectre.Console/Rendering/Segment.cs index 53bd2f9..9f36a77 100644 --- a/src/Spectre.Console/Rendering/Segment.cs +++ b/src/Spectre.Console/Rendering/Segment.cs @@ -201,8 +201,9 @@ public class Segment /// </summary> /// <param name="segments">The segments to split into lines.</param> /// <param name="maxWidth">The maximum width.</param> + /// <param name="height">The height (if any).</param> /// <returns>A list of lines.</returns> - public static List<SegmentLine> SplitLines(IEnumerable<Segment> segments, int maxWidth) + public static List<SegmentLine> SplitLines(IEnumerable<Segment> segments, int maxWidth, int? height = null) { if (segments is null) { @@ -294,6 +295,25 @@ public class Segment lines.Add(line); } + // Got a height specified? + if (height != null) + { + if (lines.Count >= height) + { + // Remove lines + lines.RemoveRange(height.Value, lines.Count - height.Value); + } + else + { + // Add lines + var missing = height - lines.Count; + for (var i = 0; i < missing; i++) + { + lines.Add(new SegmentLine()); + } + } + } + return lines; } @@ -549,6 +569,21 @@ public class Segment return cells; } + internal static List<SegmentLine> MakeWidth(int expectedWidth, List<SegmentLine> lines) + { + foreach (var line in lines) + { + var width = line.CellCount(); + if (width < expectedWidth) + { + var diff = expectedWidth - width; + line.Add(new Segment(new string(' ', diff))); + } + } + + return lines; + } + internal static List<string> SplitSegment(string text, int maxCellLength) { var list = new List<string>(); diff --git a/src/Spectre.Console/Rendering/SegmentShape.cs b/src/Spectre.Console/Rendering/SegmentShape.cs index 6b45c38..fd8c7ff 100644 --- a/src/Spectre.Console/Rendering/SegmentShape.cs +++ b/src/Spectre.Console/Rendering/SegmentShape.cs @@ -11,13 +11,8 @@ internal readonly struct SegmentShape Height = height; } - public static SegmentShape Calculate(RenderContext context, List<SegmentLine> lines) + public static SegmentShape Calculate(RenderOptions options, List<SegmentLine> lines) { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - if (lines is null) { throw new ArgumentNullException(nameof(lines)); @@ -36,7 +31,7 @@ internal readonly struct SegmentShape Math.Max(Height, other.Height)); } - public void Apply(RenderContext context, ref List<SegmentLine> lines) + public void Apply(RenderOptions options, ref List<SegmentLine> lines) { foreach (var line in lines) { diff --git a/src/Spectre.Console/Size.cs b/src/Spectre.Console/Size.cs new file mode 100644 index 0000000..5a074c8 --- /dev/null +++ b/src/Spectre.Console/Size.cs @@ -0,0 +1,29 @@ +namespace Spectre.Console; + +/// <summary> +/// Represents a size. +/// </summary> +[DebuggerDisplay("{Width,nq}x{Height,nq}")] +public struct Size +{ + /// <summary> + /// Gets the width. + /// </summary> + public int Width { get; } + + /// <summary> + /// Gets the height. + /// </summary> + public int Height { get; } + + /// <summary> + /// Initializes a new instance of the <see cref="Size"/> struct. + /// </summary> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> + public Size(int width, int height) + { + Width = width; + Height = height; + } +} diff --git a/src/Spectre.Console/VerticalAlignment.cs b/src/Spectre.Console/VerticalAlignment.cs new file mode 100644 index 0000000..dfffb77 --- /dev/null +++ b/src/Spectre.Console/VerticalAlignment.cs @@ -0,0 +1,22 @@ +namespace Spectre.Console; + +/// <summary> +/// Represents vertical alignment. +/// </summary> +public enum VerticalAlignment +{ + /// <summary> + /// Top aligned. + /// </summary> + Top, + + /// <summary> + /// Middle aligned. + /// </summary> + Middle, + + /// <summary> + /// Bottom aligned. + /// </summary> + Bottom, +} diff --git a/src/Spectre.Console/Widgets/Align.cs b/src/Spectre.Console/Widgets/Align.cs new file mode 100644 index 0000000..9da88b4 --- /dev/null +++ b/src/Spectre.Console/Widgets/Align.cs @@ -0,0 +1,146 @@ +namespace Spectre.Console; + +/// <summary> +/// Represents a renderable used to align content. +/// </summary> +public sealed class Align : Renderable +{ + private readonly IRenderable _renderable; + + /// <summary> + /// Gets or sets the horizontal alignment. + /// </summary> + public HorizontalAlignment Horizontal { get; set; } = HorizontalAlignment.Left; + + /// <summary> + /// Gets or sets the vertical alignment. + /// </summary> + public VerticalAlignment? Vertical { get; set; } + + /// <summary> + /// Gets or sets the width. + /// </summary> + public int? Width { get; set; } + + /// <summary> + /// Gets or sets the height. + /// </summary> + public int? Height { get; set; } + + /// <summary> + /// Initializes a new instance of the <see cref="Align"/> class. + /// </summary> + /// <param name="renderable">The renderable to align.</param> + /// <param name="horizontal">The horizontal alignment.</param> + /// <param name="vertical">The vertical alignment, or <c>null</c> if none.</param> + public Align(IRenderable renderable, HorizontalAlignment horizontal, VerticalAlignment? vertical = null) + { + _renderable = renderable ?? throw new ArgumentNullException(nameof(renderable)); + + Horizontal = horizontal; + Vertical = vertical; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Align"/> class that is left aligned. + /// </summary> + /// <param name="renderable">The <see cref="IRenderable"/> to align.</param> + /// <param name="vertical">The vertical alignment, or <c>null</c> if none.</param> + /// <returns>A new <see cref="Align"/> object.</returns> + public static Align Left(IRenderable renderable, VerticalAlignment? vertical = null) + { + return new Align(renderable, HorizontalAlignment.Left, vertical); + } + + /// <summary> + /// Initializes a new instance of the <see cref="Align"/> class that is center aligned. + /// </summary> + /// <param name="renderable">The <see cref="IRenderable"/> to align.</param> + /// <param name="vertical">The vertical alignment, or <c>null</c> if none.</param> + /// <returns>A new <see cref="Align"/> object.</returns> + public static Align Center(IRenderable renderable, VerticalAlignment? vertical = null) + { + return new Align(renderable, HorizontalAlignment.Center, vertical); + } + + /// <summary> + /// Initializes a new instance of the <see cref="Align"/> class that is right aligned. + /// </summary> + /// <param name="renderable">The <see cref="IRenderable"/> to align.</param> + /// <param name="vertical">The vertical alignment, or <c>null</c> if none.</param> + /// <returns>A new <see cref="Align"/> object.</returns> + public static Align Right(IRenderable renderable, VerticalAlignment? vertical = null) + { + return new Align(renderable, HorizontalAlignment.Right, vertical); + } + + /// <inheritdoc/> + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) + { + var rendered = _renderable.Render(options with { Height = null }, maxWidth); + var lines = Segment.SplitLines(rendered); + + var width = Math.Min(Width ?? maxWidth, maxWidth); + var height = Height ?? options.Height; + + var blank = new SegmentLine(new[] { new Segment(new string(' ', width)) }); + + // Align vertically + if (Vertical != null && height != null) + { + switch (Vertical) + { + case VerticalAlignment.Top: + { + var diff = Height - lines.Count; + for (var i = 0; i < diff; i++) + { + lines.Add(blank); + } + + break; + } + + case VerticalAlignment.Middle: + { + var top = (height - lines.Count) / 2; + var bottom = height - top - lines.Count; + + for (var i = 0; i < top; i++) + { + lines.Insert(0, blank); + } + + for (var i = 0; i < bottom; i++) + { + lines.Add(blank); + } + + break; + } + + case VerticalAlignment.Bottom: + { + var diff = Height - lines.Count; + for (var i = 0; i < diff; i++) + { + lines.Insert(0, blank); + } + + break; + } + + default: + throw new NotSupportedException("Unknown vertical alignment"); + } + } + + // Align horizontally + foreach (var line in lines) + { + Aligner.AlignHorizontally(line, Horizontal, width); + } + + return new SegmentLineEnumerator(lines); + } +} diff --git a/src/Spectre.Console/Widgets/Calendar.cs b/src/Spectre.Console/Widgets/Calendar.cs index 3f4ece6..6e2bcd9 100644 --- a/src/Spectre.Console/Widgets/Calendar.cs +++ b/src/Spectre.Console/Widgets/Calendar.cs @@ -107,6 +107,7 @@ public sealed class Calendar : JustInTimeRenderable, IHasCulture, IHasTableBorde } /// <inheritdoc/> + [Obsolete("Use the Align widget instead. This property will be removed in a later release.")] public Justify? Alignment { get => _alignment; @@ -162,6 +163,7 @@ public sealed class Calendar : JustInTimeRenderable, IHasCulture, IHasTableBorde { var culture = Culture ?? CultureInfo.InvariantCulture; +#pragma warning disable CS0618 // Type or member is obsolete var table = new Table { Border = _border, @@ -169,6 +171,7 @@ public sealed class Calendar : JustInTimeRenderable, IHasCulture, IHasTableBorde BorderStyle = _borderStyle, Alignment = _alignment, }; +#pragma warning restore CS0618 // Type or member is obsolete if (ShowHeader) { diff --git a/src/Spectre.Console/Widgets/Canvas.cs b/src/Spectre.Console/Widgets/Canvas.cs index 2718a49..cdabca0 100644 --- a/src/Spectre.Console/Widgets/Canvas.cs +++ b/src/Spectre.Console/Widgets/Canvas.cs @@ -70,7 +70,7 @@ public sealed class Canvas : Renderable } /// <inheritdoc/> - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { if (PixelWidth < 0) { @@ -88,7 +88,7 @@ public sealed class Canvas : Renderable } /// <inheritdoc/> - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { if (PixelWidth < 0) { diff --git a/src/Spectre.Console/Widgets/Charts/BarChart.cs b/src/Spectre.Console/Widgets/Charts/BarChart.cs index 457116b..b6367b7 100644 --- a/src/Spectre.Console/Widgets/Charts/BarChart.cs +++ b/src/Spectre.Console/Widgets/Charts/BarChart.cs @@ -52,14 +52,14 @@ public sealed class BarChart : Renderable, IHasCulture } /// <inheritdoc/> - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { var width = Math.Min(Width ?? maxWidth, maxWidth); return new Measurement(width, width); } /// <inheritdoc/> - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { var width = Math.Min(Width ?? maxWidth, maxWidth); var maxValue = Math.Max(MaxValue ?? 0d, Data.Max(item => item.Value)); @@ -72,7 +72,7 @@ public sealed class BarChart : Renderable, IHasCulture if (!string.IsNullOrWhiteSpace(Label)) { - grid.AddRow(Text.Empty, new Markup(Label).Alignment(LabelAlignment)); + grid.AddRow(Text.Empty, new Markup(Label).Justify(LabelAlignment)); } foreach (var item in Data) @@ -93,6 +93,6 @@ public sealed class BarChart : Renderable, IHasCulture }); } - return ((IRenderable)grid).Render(context, width); + return ((IRenderable)grid).Render(options, width); } } \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/Charts/BreakdownBar.cs b/src/Spectre.Console/Widgets/Charts/BreakdownBar.cs index 4db0bc4..fa1e5f9 100644 --- a/src/Spectre.Console/Widgets/Charts/BreakdownBar.cs +++ b/src/Spectre.Console/Widgets/Charts/BreakdownBar.cs @@ -11,13 +11,13 @@ internal sealed class BreakdownBar : Renderable _data = data ?? throw new ArgumentNullException(nameof(data)); } - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { var width = Math.Min(Width ?? maxWidth, maxWidth); return new Measurement(width, width); } - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { var width = Math.Min(Width ?? maxWidth, maxWidth); diff --git a/src/Spectre.Console/Widgets/Charts/BreakdownChart.cs b/src/Spectre.Console/Widgets/Charts/BreakdownChart.cs index b7f973c..85067c9 100644 --- a/src/Spectre.Console/Widgets/Charts/BreakdownChart.cs +++ b/src/Spectre.Console/Widgets/Charts/BreakdownChart.cs @@ -3,7 +3,7 @@ namespace Spectre.Console; /// <summary> /// A renderable breakdown chart. /// </summary> -public sealed class BreakdownChart : Renderable, IHasCulture +public sealed class BreakdownChart : Renderable, IHasCulture, IExpandable { /// <summary> /// Gets the breakdown chart data. @@ -43,6 +43,13 @@ public sealed class BreakdownChart : Renderable, IHasCulture /// <remarks>Defaults to invariant culture.</remarks> public CultureInfo? Culture { get; set; } + /// <summary> + /// Gets or sets a value indicating whether or not the object should + /// expand to the available space. If <c>false</c>, the object's + /// width will be auto calculated. + /// </summary> + public bool Expand { get; set; } = true; + /// <summary> /// Initializes a new instance of the <see cref="BreakdownChart"/> class. /// </summary> @@ -53,14 +60,14 @@ public sealed class BreakdownChart : Renderable, IHasCulture } /// <inheritdoc/> - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { var width = Math.Min(Width ?? maxWidth, maxWidth); return new Measurement(width, width); } /// <inheritdoc/> - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { var width = Math.Min(Width ?? maxWidth, maxWidth); @@ -90,6 +97,11 @@ public sealed class BreakdownChart : Renderable, IHasCulture }); } - return ((IRenderable)grid).Render(context, width); + if (!Expand) + { + grid.Collapse(); + } + + return ((IRenderable)grid).Render(options, width); } } \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/Charts/BreakdownTags.cs b/src/Spectre.Console/Widgets/Charts/BreakdownTags.cs index 669660a..a747345 100644 --- a/src/Spectre.Console/Widgets/Charts/BreakdownTags.cs +++ b/src/Spectre.Console/Widgets/Charts/BreakdownTags.cs @@ -14,13 +14,13 @@ internal sealed class BreakdownTags : Renderable _data = data ?? throw new ArgumentNullException(nameof(data)); } - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { var width = Math.Min(Width ?? maxWidth, maxWidth); return new Measurement(width, width); } - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { var culture = Culture ?? CultureInfo.InvariantCulture; @@ -29,13 +29,13 @@ internal sealed class BreakdownTags : Renderable { var panel = new Panel(GetTag(item, culture)); panel.Inline = true; - panel.Padding = new Padding(0, 0); + panel.Padding = new Padding(0, 0, 2, 0); panel.NoBorder(); panels.Add(panel); } - foreach (var segment in ((IRenderable)new Columns(panels).Padding(0, 0)).Render(context, maxWidth)) + foreach (var segment in ((IRenderable)new Columns(panels).Padding(0, 0)).Render(options, maxWidth)) { yield return segment; } diff --git a/src/Spectre.Console/Widgets/Columns.cs b/src/Spectre.Console/Widgets/Columns.cs index d6bb8de..de20d35 100644 --- a/src/Spectre.Console/Widgets/Columns.cs +++ b/src/Spectre.Console/Widgets/Columns.cs @@ -55,11 +55,11 @@ public sealed class Columns : Renderable, IPaddable, IExpandable } /// <inheritdoc/> - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { var maxPadding = Math.Max(Padding.GetLeftSafe(), Padding.GetRightSafe()); - var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray(); + var itemWidths = _items.Select(item => item.Measure(options, maxWidth).Max).ToArray(); var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding); if (columnCount == 0) { @@ -83,11 +83,11 @@ public sealed class Columns : Renderable, IPaddable, IExpandable } /// <inheritdoc/> - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { var maxPadding = Math.Max(Padding.GetLeftSafe(), Padding.GetRightSafe()); - var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray(); + var itemWidths = _items.Select(item => item.Measure(options, maxWidth).Max).ToArray(); var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding); if (columnCount == 0) { @@ -121,7 +121,7 @@ public sealed class Columns : Renderable, IPaddable, IExpandable table.AddRow(_items.Skip(start).Take(columnCount).ToArray()); } - return ((IRenderable)table).Render(context, maxWidth); + return ((IRenderable)table).Render(options, maxWidth); } // Algorithm borrowed from https://github.com/willmcgugan/rich/blob/master/rich/columns.py diff --git a/src/Spectre.Console/Widgets/ControlCode.cs b/src/Spectre.Console/Widgets/ControlCode.cs index fa7b46e..b83d833 100644 --- a/src/Spectre.Console/Widgets/ControlCode.cs +++ b/src/Spectre.Console/Widgets/ControlCode.cs @@ -9,14 +9,14 @@ internal sealed class ControlCode : Renderable _segment = Segment.Control(control); } - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { return new Measurement(0, 0); } - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { - if (context.Ansi) + if (options.Ansi) { yield return _segment; } diff --git a/src/Spectre.Console/Widgets/Figlet/FigletText.cs b/src/Spectre.Console/Widgets/Figlet/FigletText.cs index f17d6a5..be4ecde 100644 --- a/src/Spectre.Console/Widgets/Figlet/FigletText.cs +++ b/src/Spectre.Console/Widgets/Figlet/FigletText.cs @@ -3,7 +3,7 @@ namespace Spectre.Console; /// <summary> /// Represents text rendered with a FIGlet font. /// </summary> -public sealed class FigletText : Renderable, IAlignable +public sealed class FigletText : Renderable, IHasJustification { private readonly FigletFont _font; private readonly string _text; @@ -14,7 +14,7 @@ public sealed class FigletText : Renderable, IAlignable public Color? Color { get; set; } /// <inheritdoc/> - public Justify? Alignment { get; set; } + public Justify? Justification { get; set; } /// <summary> /// Initializes a new instance of the <see cref="FigletText"/> class. @@ -37,10 +37,10 @@ public sealed class FigletText : Renderable, IAlignable } /// <inheritdoc/> - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { var style = new Style(Color ?? Console.Color.Default); - var alignment = Alignment ?? Justify.Left; + var alignment = Justification ?? Console.Justify.Left; foreach (var row in GetRows(maxWidth)) { @@ -49,7 +49,7 @@ public sealed class FigletText : Renderable, IAlignable var line = new Segment(string.Concat(row.Select(x => x.Lines[index])), style); var lineWidth = line.CellCount(); - if (alignment == Justify.Left) + if (alignment == Console.Justify.Left) { yield return line; @@ -58,7 +58,7 @@ public sealed class FigletText : Renderable, IAlignable yield return Segment.Padding(maxWidth - lineWidth); } } - else if (alignment == Justify.Center) + else if (alignment == Console.Justify.Center) { var left = (maxWidth - lineWidth) / 2; var right = left + ((maxWidth - lineWidth) % 2); @@ -67,7 +67,7 @@ public sealed class FigletText : Renderable, IAlignable yield return line; yield return Segment.Padding(right); } - else if (alignment == Justify.Right) + else if (alignment == Console.Justify.Right) { if (lineWidth < maxWidth) { diff --git a/src/Spectre.Console/Widgets/Grid.cs b/src/Spectre.Console/Widgets/Grid.cs index 448ef1b..7a14798 100644 --- a/src/Spectre.Console/Widgets/Grid.cs +++ b/src/Spectre.Console/Widgets/Grid.cs @@ -30,6 +30,7 @@ public sealed class Grid : JustInTimeRenderable, IExpandable, IAlignable } /// <inheritdoc/> + [Obsolete("Use the Align widget instead. This property will be removed in a later release.")] public Justify? Alignment { get => _alignment; diff --git a/src/Spectre.Console/Widgets/Layout/Layout.cs b/src/Spectre.Console/Widgets/Layout/Layout.cs new file mode 100644 index 0000000..f3bc0c4 --- /dev/null +++ b/src/Spectre.Console/Widgets/Layout/Layout.cs @@ -0,0 +1,311 @@ +namespace Spectre.Console; + +/// <summary> +/// Represents a renderable to divide a fixed height into rows or columns. +/// </summary> +public sealed class Layout : Renderable, IRatioResolvable, IHasVisibility +{ + private LayoutSplitter _splitter; + private Layout[] _children; + private IRenderable _renderable; + private int _ratio; + private int _minimumSize; + private int? _size; + + /// <summary> + /// Gets or sets the name. + /// </summary> + public string? Name { get; set; } + + /// <summary> + /// Gets or sets the ratio. + /// </summary> + /// <remarks> + /// Defaults to <c>1</c>. + /// Must be greater than <c>0</c>. + /// </remarks> + public int Ratio + { + get => _ratio; + set + { + if (value < 1) + { + throw new InvalidOperationException("Ratio must be equal to or greater than 1"); + } + + _ratio = value; + } + } + + /// <summary> + /// Gets or sets the minimum width. + /// </summary> + /// <remarks> + /// Defaults to <c>1</c>. + /// Must be greater than <c>0</c>. + /// </remarks> + public int MinimumSize + { + get => _minimumSize; + set + { + if (value < 1) + { + throw new InvalidOperationException("Minimum size must be equal to or greater than 1"); + } + + _minimumSize = value; + } + } + + /// <summary> + /// Gets or sets the width. + /// </summary> + /// <remarks> + /// Defaults to <c>null</c>. + /// Must be greater than <c>0</c>. + /// </remarks> + public int? Size + { + get => _size; + set + { + if (value < 1) + { + throw new InvalidOperationException("Size must be equal to or greater than 1"); + } + + _size = value; + } + } + + /// <summary> + /// Gets or sets a value indicating whether or not the layout should + /// be visible or not. + /// </summary> + /// <remarks>Defaults to <c>true</c>.</remarks> + public bool IsVisible { get; set; } = true; + + /// <summary> + /// Gets the splitter used for this layout. + /// </summary> + internal LayoutSplitter Splitter => _splitter; + + /// <summary> + /// Gets the <see cref="IRenderable"/> associated with this layout. + /// </summary> + internal IRenderable Renderable => _renderable; + + /// <summary> + /// Gets a child layout by it's name. + /// </summary> + /// <param name="name">The layout name.</param> + /// <returns>The specified child <see cref="Layout"/>.</returns> + public Layout this[string name] + { + get => GetLayout(name); + } + + /// <summary> + /// Initializes a new instance of the <see cref="Layout"/> class. + /// </summary> + /// <param name="name">The layout name.</param> + public Layout(string name) + : this(name, null) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="Layout"/> class. + /// </summary> + /// <param name="renderable">The renderable.</param> + public Layout(IRenderable renderable) + : this(null, renderable) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="Layout"/> class. + /// </summary> + /// <param name="name">The layout name.</param> + /// <param name="renderable">The renderable.</param> + public Layout(string? name = null, IRenderable? renderable = null) + { + _splitter = LayoutSplitter.Null; + _children = Array.Empty<Layout>(); + _renderable = renderable ?? new LayoutPlaceholder(this); + _ratio = 1; + _size = null; + + Name = name; + } + + /// <summary> + /// Gets a child layout by it's name. + /// </summary> + /// <param name="name">The layout name.</param> + /// <returns>The specified child <see cref="Layout"/>.</returns> + public Layout GetLayout(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException($"'{nameof(name)}' cannot be null or empty.", nameof(name)); + } + + var stack = new Stack<Layout>(); + stack.Push(this); + + while (stack.Count > 0) + { + var current = stack.Pop(); + if (name.Equals(current.Name, StringComparison.OrdinalIgnoreCase)) + { + return current; + } + + foreach (var layout in current.GetChildren()) + { + stack.Push(layout); + } + } + + throw new InvalidOperationException($"Could not find layout '{name}'"); + } + + /// <summary> + /// Splits the layout into rows. + /// </summary> + /// <param name="children">The layout to split into rows.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public Layout SplitRows(params Layout[] children) + { + Split(LayoutSplitter.Row, children); + return this; + } + + /// <summary> + /// Splits the layout into columns. + /// </summary> + /// <param name="children">The layout to split into columns.</param> + /// <returns>The same instance so that multiple calls can be chained.</returns> + public Layout SplitColumns(params Layout[] children) + { + Split(LayoutSplitter.Column, children); + return this; + } + + /// <summary> + /// Updates the containing <see cref="IRenderable"/>. + /// </summary> + /// <param name="renderable">The renderable to use for this layout.</param> + /// /// <returns>The same instance so that multiple calls can be chained.</returns> + public Layout Update(IRenderable renderable) + { + _renderable = renderable ?? new LayoutPlaceholder(this); + return this; + } + + /// <inheritdoc/> + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) + { + var height = options.Height ?? options.ConsoleSize.Height; + var map = MakeRenderMap(options, maxWidth); + + var layoutLines = new List<SegmentLine>(); + layoutLines.AddRange(Enumerable.Range(0, height).Select(x => new SegmentLine())); + + foreach (var (region, lines) in map.Values.Select(x => (x.Region, x.Render))) + { + foreach (var line in layoutLines + .Skip(region.Y) + .Take(region.Y + region.Height) + .Enumerate().Select(x => (Index: x.Index + region.Y, Line: x.Item)) + .Zip(lines, (first, second) => (first.Index, Line: second))) + { + layoutLines[line.Index].AddRange(line.Line); + } + } + + // Return all the segments in all the lines + foreach (var (_, _, last, line) in layoutLines.Enumerate()) + { + foreach (var segment in line) + { + yield return segment; + } + + if (!last) + { + yield return Segment.LineBreak; + } + } + } + + private IEnumerable<Layout> GetChildren(bool visibleOnly = false) + { + return visibleOnly ? _children.Where(c => c.IsVisible) : _children; + } + + private bool HasChildren(bool visibleOnly = false) + { + return visibleOnly ? _children.Any(c => c.IsVisible) : _children.Any(); + } + + private void Split(LayoutSplitter splitter, Layout[] layouts) + { + if (_children.Length > 0) + { + throw new InvalidOperationException("Cannot split the same layout twice"); + } + + _splitter = splitter ?? throw new ArgumentNullException(nameof(splitter)); + _children = layouts ?? throw new ArgumentNullException(nameof(layouts)); + } + + private Dictionary<Layout, LayoutRender> MakeRenderMap(RenderOptions options, int maxWidth) + { + var result = new Dictionary<Layout, LayoutRender>(); + + var renderWidth = maxWidth; + var renderHeight = options.Height ?? options.ConsoleSize.Height; + var regionMap = MakeRegionMap(maxWidth, renderHeight); + + foreach (var (layout, region) in regionMap.Where(x => !x.Layout.HasChildren(visibleOnly: true))) + { + var segments = layout.Renderable.Render(options with { Height = region.Height }, region.Width); + + var lines = Segment.SplitLines(segments, region.Width, region.Height); + lines = Segment.MakeWidth(region.Width, lines); + + result[layout] = new LayoutRender(region, lines); + } + + return result; + } + + private IEnumerable<(Layout Layout, Region Region)> MakeRegionMap(int width, int height) + { + var stack = new Stack<(Layout Layout, Region Region)>(); + stack.Push((this, new Region(0, 0, width, height))); + + var result = new List<(Layout Layout, Region Region)>(); + + while (stack.Count > 0) + { + var current = stack.Pop(); + result.Add(current); + + if (current.Layout.HasChildren(visibleOnly: true)) + { + foreach (var childAndRegion in current.Layout.Splitter + .Divide(current.Region, current.Layout.GetChildren(visibleOnly: true))) + { + stack.Push(childAndRegion); + } + } + } + + return result.ReverseEnumerable(); + } +} diff --git a/src/Spectre.Console/Widgets/Layout/LayoutPlaceholder.cs b/src/Spectre.Console/Widgets/Layout/LayoutPlaceholder.cs new file mode 100644 index 0000000..46b959a --- /dev/null +++ b/src/Spectre.Console/Widgets/Layout/LayoutPlaceholder.cs @@ -0,0 +1,31 @@ +namespace Spectre.Console; + +internal sealed class LayoutPlaceholder : Renderable +{ + public Layout Layout { get; } + + public LayoutPlaceholder(Layout layout) + { + Layout = layout ?? throw new ArgumentNullException(nameof(layout)); + } + + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) + { + var width = maxWidth; + var height = options.Height ?? options.ConsoleSize.Height; + var title = Layout.Name != null + ? $"{Layout.Name} ({width} x {height})" + : $"{width} x {height}"; + + var panel = new Panel( + Align.Center(new Text("Placeholder"), VerticalAlignment.Middle)) + { + Width = maxWidth, + Height = options.Height ?? options.ConsoleSize.Height, + Header = new PanelHeader(title), + Border = BoxBorder.Rounded, + }; + + return ((IRenderable)panel).Render(options, maxWidth); + } +} diff --git a/src/Spectre.Console/Widgets/Layout/LayoutRender.cs b/src/Spectre.Console/Widgets/Layout/LayoutRender.cs new file mode 100644 index 0000000..a608be7 --- /dev/null +++ b/src/Spectre.Console/Widgets/Layout/LayoutRender.cs @@ -0,0 +1,14 @@ +namespace Spectre.Console; + +[DebuggerDisplay("{Region,nq}")] +internal sealed class LayoutRender +{ + public Region Region { get; } + public List<SegmentLine> Render { get; } + + public LayoutRender(Region region, List<SegmentLine> render) + { + Region = region; + Render = render ?? throw new ArgumentNullException(nameof(render)); + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/Layout/LayoutSplitter.cs b/src/Spectre.Console/Widgets/Layout/LayoutSplitter.cs new file mode 100644 index 0000000..6be913c --- /dev/null +++ b/src/Spectre.Console/Widgets/Layout/LayoutSplitter.cs @@ -0,0 +1,48 @@ +namespace Spectre.Console; + +internal abstract class LayoutSplitter +{ + public static LayoutSplitter Column { get; } = new ColumnSplitter(); + public static LayoutSplitter Row { get; } = new RowSplitter(); + public static LayoutSplitter Null { get; } = new NullSplitter(); + + public abstract IEnumerable<(Layout Child, Region Region)> Divide(Region region, IEnumerable<Layout> layouts); + + private sealed class NullSplitter : LayoutSplitter + { + public override IEnumerable<(Layout Child, Region Region)> Divide(Region region, IEnumerable<Layout> layouts) + { + yield break; + } + } + + private sealed class ColumnSplitter : LayoutSplitter + { + public override IEnumerable<(Layout Child, Region Region)> Divide(Region region, IEnumerable<Layout> children) + { + var widths = Ratio.Resolve(region.Width, children); + var offset = 0; + + foreach (var (child, childWidth) in children.Zip(widths, (child, width) => (child, width))) + { + yield return (child, new Region(region.X + offset, region.Y, childWidth, region.Height)); + offset += childWidth; + } + } + } + + private sealed class RowSplitter : LayoutSplitter + { + public override IEnumerable<(Layout Child, Region Region)> Divide(Region region, IEnumerable<Layout> children) + { + var heights = Ratio.Resolve(region.Height, children); + var offset = 0; + + foreach (var (child, childHeight) in children.Zip(heights, (child, height) => (child, height))) + { + yield return (child, new Region(region.X, region.Y + offset, region.Width, childHeight)); + offset += childHeight; + } + } + } +} diff --git a/src/Spectre.Console/Widgets/Markup.cs b/src/Spectre.Console/Widgets/Markup.cs index 7b23376..5e43809 100644 --- a/src/Spectre.Console/Widgets/Markup.cs +++ b/src/Spectre.Console/Widgets/Markup.cs @@ -4,15 +4,15 @@ namespace Spectre.Console; /// A renderable piece of markup text. /// </summary> [SuppressMessage("Naming", "CA1724:Type names should not match namespaces")] -public sealed class Markup : Renderable, IAlignable, IOverflowable +public sealed class Markup : Renderable, IHasJustification, IOverflowable { private readonly Paragraph _paragraph; /// <inheritdoc/> - public Justify? Alignment + public Justify? Justification { - get => _paragraph.Alignment; - set => _paragraph.Alignment = value; + get => _paragraph.Justification; + set => _paragraph.Justification = value; } /// <inheritdoc/> @@ -43,15 +43,15 @@ public sealed class Markup : Renderable, IAlignable, IOverflowable } /// <inheritdoc/> - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { - return ((IRenderable)_paragraph).Measure(context, maxWidth); + return ((IRenderable)_paragraph).Measure(options, maxWidth); } /// <inheritdoc/> - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { - return ((IRenderable)_paragraph).Render(context, maxWidth); + return ((IRenderable)_paragraph).Render(options, maxWidth); } /// <summary> diff --git a/src/Spectre.Console/Widgets/Padder.cs b/src/Spectre.Console/Widgets/Padder.cs index 9f2d376..eeecdf1 100644 --- a/src/Spectre.Console/Widgets/Padder.cs +++ b/src/Spectre.Console/Widgets/Padder.cs @@ -29,10 +29,10 @@ public sealed class Padder : Renderable, IPaddable, IExpandable } /// <inheritdoc/> - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { var paddingWidth = Padding?.GetWidth() ?? 0; - var measurement = _child.Measure(context, maxWidth - paddingWidth); + var measurement = _child.Measure(options, maxWidth - paddingWidth); return new Measurement( measurement.Min + paddingWidth, @@ -40,14 +40,14 @@ public sealed class Padder : Renderable, IPaddable, IExpandable } /// <inheritdoc/> - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { var paddingWidth = Padding?.GetWidth() ?? 0; var childWidth = maxWidth - paddingWidth; if (!Expand) { - var measurement = _child.Measure(context, maxWidth - paddingWidth); + var measurement = _child.Measure(options, maxWidth - paddingWidth); childWidth = measurement.Max; } @@ -66,7 +66,7 @@ public sealed class Padder : Renderable, IPaddable, IExpandable result.Add(Segment.LineBreak); } - var child = _child.Render(context, maxWidth - paddingWidth); + var child = _child.Render(options, maxWidth - paddingWidth); foreach (var line in Segment.SplitLines(child)) { // Left padding diff --git a/src/Spectre.Console/Widgets/Panel.cs b/src/Spectre.Console/Widgets/Panel.cs index 0c263cb..05d41d4 100644 --- a/src/Spectre.Console/Widgets/Panel.cs +++ b/src/Spectre.Console/Widgets/Panel.cs @@ -35,6 +35,16 @@ public sealed class Panel : Renderable, IHasBoxBorder, IHasBorder, IExpandable, /// </summary> public PanelHeader? Header { get; set; } + /// <summary> + /// Gets or sets the width of the panel. + /// </summary> + public int? Width { get; set; } + + /// <summary> + /// Gets or sets the height of the panel. + /// </summary> + public int? Height { get; set; } + /// <summary> /// Gets or sets a value indicating whether or not the panel is inlined. /// </summary> @@ -59,54 +69,73 @@ public sealed class Panel : Renderable, IHasBoxBorder, IHasBorder, IExpandable, } /// <inheritdoc/> - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { var child = new Padder(_child, Padding); - var childWidth = ((IRenderable)child).Measure(context, maxWidth); + return Measure(options, maxWidth, child); + } + + private Measurement Measure(RenderOptions options, int maxWidth, IRenderable child) + { + var edgeWidth = (options.GetSafeBorder(this) is not NoBoxBorder) ? EdgeWidth : 0; + var childWidth = child.Measure(options, maxWidth - edgeWidth); + + if (Width != null) + { + var width = Width.Value - edgeWidth; + if (width > childWidth.Max) + { + childWidth = new Measurement( + childWidth.Min, + width); + } + } + return new Measurement( - childWidth.Min + EdgeWidth, - childWidth.Max + EdgeWidth); + childWidth.Min + edgeWidth, + childWidth.Max + edgeWidth); } /// <inheritdoc/> - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { - var edgeWidth = EdgeWidth; - - var border = BoxExtensions.GetSafeBorder(Border, !context.Unicode && UseSafeBorder); + var border = options.GetSafeBorder(this); var borderStyle = BorderStyle ?? Style.Plain; - var showBorder = true; - if (border is NoBoxBorder) - { - showBorder = false; - edgeWidth = 0; - } + var showBorder = border is not NoBoxBorder; + var edgeWidth = showBorder ? EdgeWidth : 0; var child = new Padder(_child, Padding); - var childWidth = maxWidth - edgeWidth; + var width = Measure(options, maxWidth, child); + + var panelWidth = Math.Min(!Expand ? width.Max : maxWidth, maxWidth); + var innerWidth = panelWidth - edgeWidth; + + var height = Height != null + ? Height - 2 + : options.Height != null + ? options.Height - 2 + : null; if (!Expand) { - var measurement = ((IRenderable)child).Measure(context, maxWidth - edgeWidth); - childWidth = measurement.Max; + // Set the height to the explicit height (or null) + // if the panel isn't expandable. + height = Height != null ? Height - 2 : null; } - var panelWidth = childWidth + edgeWidth; - panelWidth = Math.Min(panelWidth, maxWidth); - childWidth = panelWidth - edgeWidth; - + // Start building the panel var result = new List<Segment>(); + // Panel top if (showBorder) { - // Panel top - AddTopBorder(result, context, border, borderStyle, panelWidth); + AddTopBorder(result, options, border, borderStyle, panelWidth); } // Split the child segments into lines. - var childSegments = ((IRenderable)child).Render(context, childWidth); - foreach (var (_, _, last, line) in Segment.SplitLines(childSegments, childWidth).Enumerate()) + var childSegments = ((IRenderable)child).Render(options with { Height = height }, innerWidth); + foreach (var (_, _, last, line) in Segment.SplitLines(childSegments, innerWidth, height).Enumerate()) { if (line.Count == 1 && line[0].IsWhiteSpace) { @@ -125,9 +154,9 @@ public sealed class Panel : Renderable, IHasBoxBorder, IHasBorder, IExpandable, // Do we need to pad the panel? var length = line.Sum(segment => segment.CellCount()); - if (length < childWidth) + if (length < innerWidth) { - var diff = childWidth - length; + var diff = innerWidth - length; content.Add(Segment.Padding(diff)); } @@ -170,7 +199,7 @@ public sealed class Panel : Renderable, IHasBoxBorder, IHasBorder, IExpandable, } private void AddTopBorder( - List<Segment> result, RenderContext context, BoxBorder border, + List<Segment> result, RenderOptions options, BoxBorder border, Style borderStyle, int panelWidth) { var rule = new Rule @@ -180,14 +209,14 @@ public sealed class Panel : Renderable, IHasBoxBorder, IHasBorder, IExpandable, TitlePadding = 1, TitleSpacing = 0, Title = Header?.Text, - Alignment = Header?.Alignment ?? Justify.Left, + Justification = Header?.Justification ?? Justify.Left, }; // Top left border result.Add(new Segment(border.GetPart(BoxBorderPart.TopLeft), borderStyle)); // Top border (and header text if specified) - result.AddRange(((IRenderable)rule).Render(context, panelWidth - 2).Where(x => !x.IsLineBreak)); + result.AddRange(((IRenderable)rule).Render(options, panelWidth - 2).Where(x => !x.IsLineBreak)); // Top right border result.Add(new Segment(border.GetPart(BoxBorderPart.TopRight), borderStyle)); diff --git a/src/Spectre.Console/Widgets/PanelHeader.cs b/src/Spectre.Console/Widgets/PanelHeader.cs index 951dcbd..97cdd33 100644 --- a/src/Spectre.Console/Widgets/PanelHeader.cs +++ b/src/Spectre.Console/Widgets/PanelHeader.cs @@ -3,7 +3,7 @@ namespace Spectre.Console; /// <summary> /// Represents a panel header. /// </summary> -public sealed class PanelHeader : IAlignable +public sealed class PanelHeader : IHasJustification { /// <summary> /// Gets the panel header text. @@ -13,7 +13,7 @@ public sealed class PanelHeader : IAlignable /// <summary> /// Gets or sets the panel header alignment. /// </summary> - public Justify? Alignment { get; set; } + public Justify? Justification { get; set; } /// <summary> /// Initializes a new instance of the <see cref="PanelHeader"/> class. @@ -23,7 +23,7 @@ public sealed class PanelHeader : IAlignable public PanelHeader(string text, Justify? alignment = null) { Text = text ?? throw new ArgumentNullException(nameof(text)); - Alignment = alignment; + Justification = alignment; } /// <summary> @@ -57,7 +57,7 @@ public sealed class PanelHeader : IAlignable /// <returns>The same instance so that multiple calls can be chained.</returns> public PanelHeader SetAlignment(Justify alignment) { - Alignment = alignment; + Justification = alignment; return this; } } \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/Paragraph.cs b/src/Spectre.Console/Widgets/Paragraph.cs index 5ac2f8e..96012d4 100644 --- a/src/Spectre.Console/Widgets/Paragraph.cs +++ b/src/Spectre.Console/Widgets/Paragraph.cs @@ -5,14 +5,14 @@ namespace Spectre.Console; /// of the paragraph can have individual styling. /// </summary> [DebuggerDisplay("{_text,nq}")] -public sealed class Paragraph : Renderable, IAlignable, IOverflowable +public sealed class Paragraph : Renderable, IHasJustification, IOverflowable { private readonly List<SegmentLine> _lines; /// <summary> /// Gets or sets the alignment of the whole paragraph. /// </summary> - public Justify? Alignment { get; set; } + public Justify? Justification { get; set; } /// <summary> /// Gets or sets the text overflow strategy. @@ -115,7 +115,7 @@ public sealed class Paragraph : Renderable, IAlignable, IOverflowable } /// <inheritdoc/> - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { if (_lines.Count == 0) { @@ -129,11 +129,11 @@ public sealed class Paragraph : Renderable, IAlignable, IOverflowable } /// <inheritdoc/> - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { - if (context is null) + if (options is null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(options)); } if (_lines.Count == 0) @@ -141,13 +141,13 @@ public sealed class Paragraph : Renderable, IAlignable, IOverflowable return Array.Empty<Segment>(); } - var lines = context.SingleLine + var lines = options.SingleLine ? new List<SegmentLine>(_lines) : SplitLines(maxWidth); // Justify lines - var justification = context.Justification ?? Alignment ?? Justify.Left; - if (justification != Justify.Left) + var justification = options.Justification ?? Justification ?? Console.Justify.Left; + if (justification != Console.Justify.Left) { foreach (var line in lines) { @@ -155,7 +155,7 @@ public sealed class Paragraph : Renderable, IAlignable, IOverflowable } } - if (context.SingleLine) + if (options.SingleLine) { // Return the first line return lines[0].Where(segment => !segment.IsLineBreak); diff --git a/src/Spectre.Console/Widgets/ProgressBar.cs b/src/Spectre.Console/Widgets/ProgressBar.cs index 67ef339..6b59a42 100644 --- a/src/Spectre.Console/Widgets/ProgressBar.cs +++ b/src/Spectre.Console/Widgets/ProgressBar.cs @@ -23,13 +23,13 @@ internal sealed class ProgressBar : Renderable, IHasCulture internal static Style DefaultPulseStyle { get; } = new Style(foreground: Color.DodgerBlue1, background: Color.Grey23); - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { var width = Math.Min(Width ?? maxWidth, maxWidth); return new Measurement(4, width); } - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { var width = Math.Min(Width ?? maxWidth, maxWidth); var completedBarCount = Math.Min(MaxValue, Math.Max(0, Value)); @@ -37,7 +37,7 @@ internal sealed class ProgressBar : Renderable, IHasCulture if (IsIndeterminate && !isCompleted) { - foreach (var segment in RenderIndeterminate(context, width)) + foreach (var segment in RenderIndeterminate(options, width)) { yield return segment; } @@ -45,7 +45,7 @@ internal sealed class ProgressBar : Renderable, IHasCulture yield break; } - var bar = !context.Unicode ? AsciiBar : UnicodeBar; + var bar = !options.Unicode ? AsciiBar : UnicodeBar; var style = isCompleted ? FinishedStyle : CompletedStyle; var barCount = Math.Max(0, (int)(width * (completedBarCount / MaxValue))); @@ -84,29 +84,29 @@ internal sealed class ProgressBar : Renderable, IHasCulture } } - var legacy = context.ColorSystem == ColorSystem.NoColors || context.ColorSystem == ColorSystem.Legacy; + var legacy = options.ColorSystem == ColorSystem.NoColors || options.ColorSystem == ColorSystem.Legacy; var remainingToken = ShowRemaining && !legacy ? bar : ' '; yield return new Segment(new string(remainingToken, diff), RemainingStyle); } } - private IEnumerable<Segment> RenderIndeterminate(RenderContext context, int width) + private IEnumerable<Segment> RenderIndeterminate(RenderOptions options, int width) { - var bar = context.Unicode ? UnicodeBar.ToString() : AsciiBar.ToString(); + var bar = options.Unicode ? UnicodeBar.ToString() : AsciiBar.ToString(); var style = IndeterminateStyle ?? DefaultPulseStyle; IEnumerable<Segment> GetPulseSegments() { // For 1-bit and 3-bit colors, fall back to // a simpler versions with only two colors. - if (context.ColorSystem == ColorSystem.NoColors || - context.ColorSystem == ColorSystem.Legacy) + if (options.ColorSystem == ColorSystem.NoColors || + options.ColorSystem == ColorSystem.Legacy) { // First half of the pulse var segments = Enumerable.Repeat(new Segment(bar, new Style(style.Foreground)), PULSESIZE / 2); // Second half of the pulse - var legacy = context.ColorSystem == ColorSystem.NoColors || context.ColorSystem == ColorSystem.Legacy; + var legacy = options.ColorSystem == ColorSystem.NoColors || options.ColorSystem == ColorSystem.Legacy; var bar2 = legacy ? " " : bar; segments = segments.Concat(Enumerable.Repeat(new Segment(bar2, new Style(style.Background)), PULSESIZE - (PULSESIZE / 2))); diff --git a/src/Spectre.Console/Widgets/Rows.cs b/src/Spectre.Console/Widgets/Rows.cs index 32f88cf..9539440 100644 --- a/src/Spectre.Console/Widgets/Rows.cs +++ b/src/Spectre.Console/Widgets/Rows.cs @@ -29,7 +29,7 @@ public sealed class Rows : Renderable, IExpandable } /// <inheritdoc/> - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { if (Expand) { @@ -37,7 +37,7 @@ public sealed class Rows : Renderable, IExpandable } else { - var measurements = _children.Select(c => c.Measure(context, maxWidth)); + var measurements = _children.Select(c => c.Measure(options, maxWidth)); return new Measurement( measurements.Min(c => c.Min), measurements.Min(c => c.Max)); @@ -45,13 +45,13 @@ public sealed class Rows : Renderable, IExpandable } /// <inheritdoc/> - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { var result = new List<Segment>(); foreach (var child in _children) { - var segments = child.Render(context, maxWidth); + var segments = child.Render(options, maxWidth); foreach (var (_, _, last, segment) in segments.Enumerate()) { result.Add(segment); diff --git a/src/Spectre.Console/Widgets/Rule.cs b/src/Spectre.Console/Widgets/Rule.cs index 210318c..0bcef99 100644 --- a/src/Spectre.Console/Widgets/Rule.cs +++ b/src/Spectre.Console/Widgets/Rule.cs @@ -3,7 +3,7 @@ namespace Spectre.Console; /// <summary> /// A renderable horizontal rule. /// </summary> -public sealed class Rule : Renderable, IAlignable, IHasBoxBorder +public sealed class Rule : Renderable, IHasJustification, IHasBoxBorder { /// <summary> /// Gets or sets the rule title markup text. @@ -16,9 +16,9 @@ public sealed class Rule : Renderable, IAlignable, IHasBoxBorder public Style? Style { get; set; } /// <summary> - /// Gets or sets the rule's title alignment. + /// Gets or sets the rule's title justification. /// </summary> - public Justify? Alignment { get; set; } + public Justify? Justification { get; set; } /// <inheritdoc/> public BoxBorder Border { get; set; } = BoxBorder.Square; @@ -43,17 +43,17 @@ public sealed class Rule : Renderable, IAlignable, IHasBoxBorder } /// <inheritdoc/> - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { var extraLength = (2 * TitlePadding) + (2 * TitleSpacing); if (Title == null || maxWidth <= extraLength) { - return GetLineWithoutTitle(context, maxWidth); + return GetLineWithoutTitle(options, maxWidth); } // Get the title and make sure it fits. - var title = GetTitleSegments(context, Title, maxWidth - extraLength); + var title = GetTitleSegments(options, Title, maxWidth - extraLength); if (Segment.CellCount(title) > maxWidth - extraLength) { // Truncate the title @@ -61,11 +61,11 @@ public sealed class Rule : Renderable, IAlignable, IHasBoxBorder if (!title.Any()) { // We couldn't fit the title at all. - return GetLineWithoutTitle(context, maxWidth); + return GetLineWithoutTitle(options, maxWidth); } } - var (left, right) = GetLineSegments(context, maxWidth, title); + var (left, right) = GetLineSegments(options, maxWidth, title); var segments = new List<Segment>(); segments.Add(left); @@ -76,9 +76,9 @@ public sealed class Rule : Renderable, IAlignable, IHasBoxBorder return segments; } - private IEnumerable<Segment> GetLineWithoutTitle(RenderContext context, int maxWidth) + private IEnumerable<Segment> GetLineWithoutTitle(RenderOptions options, int maxWidth) { - var border = Border.GetSafeBorder(safe: !context.Unicode); + var border = Border.GetSafeBorder(safe: !options.Unicode); var text = border.GetPart(BoxBorderPart.Top).Repeat(maxWidth); return new[] @@ -88,21 +88,21 @@ public sealed class Rule : Renderable, IAlignable, IHasBoxBorder }; } - private IEnumerable<Segment> GetTitleSegments(RenderContext context, string title, int width) + private IEnumerable<Segment> GetTitleSegments(RenderOptions options, string title, int width) { title = title.NormalizeNewLines().ReplaceExact("\n", " ").Trim(); var markup = new Markup(title, Style); - return ((IRenderable)markup).Render(context.WithSingleLine(), width); + return ((IRenderable)markup).Render(options with { SingleLine = true }, width); } - private (Segment Left, Segment Right) GetLineSegments(RenderContext context, int width, IEnumerable<Segment> title) + private (Segment Left, Segment Right) GetLineSegments(RenderOptions options, int width, IEnumerable<Segment> title) { var titleLength = Segment.CellCount(title); - var border = Border.GetSafeBorder(safe: !context.Unicode); + var border = Border.GetSafeBorder(safe: !options.Unicode); var borderPart = border.GetPart(BoxBorderPart.Top); - var alignment = Alignment ?? Justify.Center; + var alignment = Justification ?? Justify.Center; if (alignment == Justify.Left) { var left = new Segment(borderPart.Repeat(TitlePadding) + new string(' ', TitleSpacing), Style ?? Style.Plain); diff --git a/src/Spectre.Console/Widgets/Table/Table.cs b/src/Spectre.Console/Widgets/Table/Table.cs index a22dd63..c0d83e1 100644 --- a/src/Spectre.Console/Widgets/Table/Table.cs +++ b/src/Spectre.Console/Widgets/Table/Table.cs @@ -59,6 +59,7 @@ public sealed class Table : Renderable, IHasTableBorder, IExpandable, IAlignable public TableTitle? Caption { get; set; } /// <inheritdoc/> + [Obsolete("Use the Align widget instead. This property will be removed in a later release.")] public Justify? Alignment { get; set; } // Whether this is a grid or not. @@ -100,14 +101,14 @@ public sealed class Table : Renderable, IHasTableBorder, IExpandable, IAlignable } /// <inheritdoc/> - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { - if (context is null) + if (options is null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(options)); } - var measurer = new TableMeasurer(this, context); + var measurer = new TableMeasurer(this, options); // Calculate the total cell width var totalCellWidth = measurer.CalculateTotalCellWidth(maxWidth); @@ -120,14 +121,14 @@ public sealed class Table : Renderable, IHasTableBorder, IExpandable, IAlignable } /// <inheritdoc/> - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { - if (context is null) + if (options is null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(options)); } - var measurer = new TableMeasurer(this, context); + var measurer = new TableMeasurer(this, options); // Calculate the column and table width var totalCellWidth = measurer.CalculateTotalCellWidth(maxWidth); @@ -139,7 +140,7 @@ public sealed class Table : Renderable, IHasTableBorder, IExpandable, IAlignable // Render the table return TableRenderer.Render( - new TableRendererContext(this, context, rows, tableWidth, maxWidth), + new TableRendererContext(this, options, rows, tableWidth, maxWidth), columnWidths); } diff --git a/src/Spectre.Console/Widgets/Table/TableAccessor.cs b/src/Spectre.Console/Widgets/Table/TableAccessor.cs index 2979a0d..3dda3ee 100644 --- a/src/Spectre.Console/Widgets/Table/TableAccessor.cs +++ b/src/Spectre.Console/Widgets/Table/TableAccessor.cs @@ -4,12 +4,12 @@ internal abstract class TableAccessor { private readonly Table _table; - public RenderContext Options { get; } + public RenderOptions Options { get; } public IReadOnlyList<TableColumn> Columns => _table.Columns; public virtual IReadOnlyList<TableRow> Rows => _table.Rows; public bool Expand => _table.Expand || _table.Width != null; - protected TableAccessor(Table table, RenderContext options) + protected TableAccessor(Table table, RenderOptions options) { _table = table ?? throw new ArgumentNullException(nameof(table)); Options = options ?? throw new ArgumentNullException(nameof(options)); diff --git a/src/Spectre.Console/Widgets/Table/TableMeasurer.cs b/src/Spectre.Console/Widgets/Table/TableMeasurer.cs index d7a26cd..d436a08 100644 --- a/src/Spectre.Console/Widgets/Table/TableMeasurer.cs +++ b/src/Spectre.Console/Widgets/Table/TableMeasurer.cs @@ -8,7 +8,7 @@ internal sealed class TableMeasurer : TableAccessor private readonly TableBorder _border; private readonly bool _padRightCell; - public TableMeasurer(Table table, RenderContext options) + public TableMeasurer(Table table, RenderOptions options) : base(table, options) { _explicitWidth = table.Width; diff --git a/src/Spectre.Console/Widgets/Table/TableRenderer.cs b/src/Spectre.Console/Widgets/Table/TableRenderer.cs index fadade9..467fe61 100644 --- a/src/Spectre.Console/Widgets/Table/TableRenderer.cs +++ b/src/Spectre.Console/Widgets/Table/TableRenderer.cs @@ -26,7 +26,7 @@ internal static class TableRenderer foreach (var (columnIndex, _, _, (rowWidth, cell)) in columnWidths.Zip(row).Enumerate()) { var justification = context.Columns[columnIndex].Alignment; - var childContext = context.Options.WithJustification(justification); + var childContext = context.Options with { Justification = justification }; var lines = Segment.SplitLines(cell.Render(childContext, rowWidth)); cellHeight = Math.Max(cellHeight, lines.Count); @@ -159,7 +159,7 @@ internal static class TableRenderer } var paragraph = new Markup(header.Text, header.Style ?? defaultStyle) - .Alignment(Justify.Center) + .Justify(Justify.Center) .Overflow(Overflow.Ellipsis); // Render the paragraphs diff --git a/src/Spectre.Console/Widgets/Table/TableRendererContext.cs b/src/Spectre.Console/Widgets/Table/TableRendererContext.cs index 17e98c7..b549802 100644 --- a/src/Spectre.Console/Widgets/Table/TableRendererContext.cs +++ b/src/Spectre.Console/Widgets/Table/TableRendererContext.cs @@ -31,9 +31,12 @@ internal sealed class TableRendererContext : TableAccessor public bool PadRightCell => _table.PadRightCell; public TableTitle? Title => _table.Title; public TableTitle? Caption => _table.Caption; - public Justify? Alignment => _table.Alignment; - public TableRendererContext(Table table, RenderContext options, IEnumerable<TableRow> rows, int tableWidth, int maxWidth) +#pragma warning disable CS0618 // Type or member is obsolete + public Justify? Alignment => _table.Alignment; +#pragma warning restore CS0618 // Type or member is obsolete + + public TableRendererContext(Table table, RenderOptions options, IEnumerable<TableRow> rows, int tableWidth, int maxWidth) : base(table, options) { _table = table ?? throw new ArgumentNullException(nameof(table)); diff --git a/src/Spectre.Console/Widgets/Text.cs b/src/Spectre.Console/Widgets/Text.cs index e84c2f3..d715730 100644 --- a/src/Spectre.Console/Widgets/Text.cs +++ b/src/Spectre.Console/Widgets/Text.cs @@ -5,7 +5,7 @@ namespace Spectre.Console; /// </summary> [DebuggerDisplay("{_text,nq}")] [SuppressMessage("Naming", "CA1724:Type names should not match namespaces")] -public sealed class Text : Renderable, IAlignable, IOverflowable +public sealed class Text : Renderable, IHasJustification, IOverflowable { private readonly Paragraph _paragraph; @@ -32,10 +32,10 @@ public sealed class Text : Renderable, IAlignable, IOverflowable /// <summary> /// Gets or sets the text alignment. /// </summary> - public Justify? Alignment + public Justify? Justification { - get => _paragraph.Alignment; - set => _paragraph.Alignment = value; + get => _paragraph.Justification; + set => _paragraph.Justification = value; } /// <summary> @@ -58,14 +58,14 @@ public sealed class Text : Renderable, IAlignable, IOverflowable public int Lines => _paragraph.Lines; /// <inheritdoc/> - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { - return ((IRenderable)_paragraph).Measure(context, maxWidth); + return ((IRenderable)_paragraph).Measure(options, maxWidth); } /// <inheritdoc/> - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { - return ((IRenderable)_paragraph).Render(context, maxWidth); + return ((IRenderable)_paragraph).Render(options, maxWidth); } } \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/TextPath.cs b/src/Spectre.Console/Widgets/TextPath.cs index 2d03e19..92c8471 100644 --- a/src/Spectre.Console/Widgets/TextPath.cs +++ b/src/Spectre.Console/Widgets/TextPath.cs @@ -3,7 +3,7 @@ namespace Spectre.Console; /// <summary> /// Representation of a file system path. /// </summary> -public sealed class TextPath : IRenderable, IAlignable +public sealed class TextPath : IRenderable, IHasJustification { private const string Ellipsis = "..."; private const string UnicodeEllipsis = "…"; @@ -35,7 +35,7 @@ public sealed class TextPath : IRenderable, IAlignable /// <summary> /// Gets or sets the alignment. /// </summary> - public Justify? Alignment { get; set; } + public Justify? Justification { get; set; } /// <summary> /// Initializes a new instance of the <see cref="TextPath"/> class. @@ -66,9 +66,9 @@ public sealed class TextPath : IRenderable, IAlignable } /// <inheritdoc/> - public Measurement Measure(RenderContext context, int maxWidth) + public Measurement Measure(RenderOptions options, int maxWidth) { - var fitted = Fit(context, maxWidth); + var fitted = Fit(options, maxWidth); var separatorCount = fitted.Length - 1; var length = fitted.Sum(f => f.Length) + separatorCount; @@ -78,16 +78,14 @@ public sealed class TextPath : IRenderable, IAlignable } /// <inheritdoc/> - public IEnumerable<Segment> Render(RenderContext context, int maxWidth) + public IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { - var alignment = Alignment ?? Justify.Left; - var rootStyle = RootStyle ?? Style.Plain; var separatorStyle = SeparatorStyle ?? Style.Plain; var stemStyle = StemStyle ?? Style.Plain; var leafStyle = LeafStyle ?? Style.Plain; - var fitted = Fit(context, maxWidth); + var fitted = Fit(options, maxWidth); var parts = new List<Segment>(); foreach (var (_, first, last, item) in fitted.Enumerate()) { @@ -119,7 +117,7 @@ public sealed class TextPath : IRenderable, IAlignable } // Align the result - Aligner.Align(parts, Alignment, maxWidth); + Aligner.Align(parts, Justification, maxWidth); // Insert a line break parts.Add(Segment.LineBreak); @@ -127,7 +125,7 @@ public sealed class TextPath : IRenderable, IAlignable return parts; } - private string[] Fit(RenderContext context, int maxWidth) + private string[] Fit(RenderOptions options, int maxWidth) { // No parts? if (_parts.Length == 0) @@ -141,7 +139,7 @@ public sealed class TextPath : IRenderable, IAlignable return _parts; } - var ellipsis = context.Unicode ? UnicodeEllipsis : Ellipsis; + var ellipsis = options.Unicode ? UnicodeEllipsis : Ellipsis; var ellipsisLength = Cell.GetCellLength(ellipsis); if (_parts.Length >= 2) diff --git a/src/Spectre.Console/Widgets/Tree.cs b/src/Spectre.Console/Widgets/Tree.cs index 9929a53..289884e 100644 --- a/src/Spectre.Console/Widgets/Tree.cs +++ b/src/Spectre.Console/Widgets/Tree.cs @@ -47,7 +47,7 @@ public sealed class Tree : Renderable, IHasTreeNodes } /// <inheritdoc /> - protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) + protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth) { var result = new List<Segment>(); var visitedNodes = new HashSet<TreeNode>(); @@ -56,7 +56,7 @@ public sealed class Tree : Renderable, IHasTreeNodes stack.Push(new Queue<TreeNode>(new[] { _root })); var levels = new List<Segment>(); - levels.Add(GetGuide(context, TreeGuidePart.Continue)); + levels.Add(GetGuide(options, TreeGuidePart.Continue)); while (stack.Count > 0) { @@ -66,7 +66,7 @@ public sealed class Tree : Renderable, IHasTreeNodes levels.RemoveLast(); if (levels.Count > 0) { - levels.AddOrReplaceLast(GetGuide(context, TreeGuidePart.Fork)); + levels.AddOrReplaceLast(GetGuide(options, TreeGuidePart.Fork)); } continue; @@ -83,11 +83,11 @@ public sealed class Tree : Renderable, IHasTreeNodes if (isLastChild) { - levels.AddOrReplaceLast(GetGuide(context, TreeGuidePart.End)); + levels.AddOrReplaceLast(GetGuide(options, TreeGuidePart.End)); } var prefix = levels.Skip(1).ToList(); - var renderableLines = Segment.SplitLines(current.Renderable.Render(context, maxWidth - Segment.CellCount(prefix))); + var renderableLines = Segment.SplitLines(current.Renderable.Render(options, maxWidth - Segment.CellCount(prefix))); foreach (var (_, isFirstLine, _, line) in renderableLines.Enumerate()) { @@ -102,14 +102,14 @@ public sealed class Tree : Renderable, IHasTreeNodes if (isFirstLine && prefix.Count > 0) { var part = isLastChild ? TreeGuidePart.Space : TreeGuidePart.Continue; - prefix.AddOrReplaceLast(GetGuide(context, part)); + prefix.AddOrReplaceLast(GetGuide(options, part)); } } if (current.Expanded && current.Nodes.Count > 0) { - levels.AddOrReplaceLast(GetGuide(context, isLastChild ? TreeGuidePart.Space : TreeGuidePart.Continue)); - levels.Add(GetGuide(context, current.Nodes.Count == 1 ? TreeGuidePart.End : TreeGuidePart.Fork)); + levels.AddOrReplaceLast(GetGuide(options, isLastChild ? TreeGuidePart.Space : TreeGuidePart.Continue)); + levels.Add(GetGuide(options, current.Nodes.Count == 1 ? TreeGuidePart.End : TreeGuidePart.Fork)); stack.Push(new Queue<TreeNode>(current.Nodes)); } @@ -118,9 +118,9 @@ public sealed class Tree : Renderable, IHasTreeNodes return result; } - private Segment GetGuide(RenderContext context, TreeGuidePart part) + private Segment GetGuide(RenderOptions options, TreeGuidePart part) { - var guide = Guide.GetSafeTreeGuide(safe: !context.Unicode); + var guide = Guide.GetSafeTreeGuide(safe: !options.Unicode); return new Segment(guide.GetPart(part), Style ?? Style.Plain); } } \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Spectre.Console.Cli.Tests.csproj b/test/Spectre.Console.Cli.Tests/Spectre.Console.Cli.Tests.csproj index 67f5874..6f68434 100644 --- a/test/Spectre.Console.Cli.Tests/Spectre.Console.Cli.Tests.csproj +++ b/test/Spectre.Console.Cli.Tests/Spectre.Console.Cli.Tests.csproj @@ -1,8 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('Windows'))">net7.0;net48</TargetFrameworks> - <TargetFrameworks Condition="!$([MSBuild]::IsOSPlatform('Windows'))">net7.0</TargetFrameworks> + <TargetFrameworks>net7.0</TargetFrameworks> </PropertyGroup> <ItemGroup> @@ -30,15 +29,4 @@ <ProjectReference Include="..\..\src\Spectre.Console\Spectre.Console.csproj" /> </ItemGroup> - <ItemGroup> - <None Update="Expectations\Help\Description_No_Trailing_Period.Output.received.txt"> - <ParentFile>$([System.String]::Copy('%(FileName)').Split('.')[0])</ParentFile> - <DependentUpon>%(ParentFile).cs</DependentUpon> - </None> - <None Update="Expectations\Help\Description_No_Trailing_Period.Output.verified.txt"> - <ParentFile>$([System.String]::Copy('%(FileName)').Split('.')[0])</ParentFile> - <DependentUpon>%(ParentFile).cs</DependentUpon> - </None> - </ItemGroup> - </Project> diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Bottom.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Bottom.Output.verified.txt new file mode 100644 index 0000000..74ab31e --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Bottom.Output.verified.txt @@ -0,0 +1,15 @@ + + + + + + + + + + + + + ┌──────────────┐ + │ Hello World! │ + └──────────────┘ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Middle.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Middle.Output.verified.txt new file mode 100644 index 0000000..33cc191 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Middle.Output.verified.txt @@ -0,0 +1,15 @@ + + + + + + + ┌──────────────┐ + │ Hello World! │ + └──────────────┘ + + + + + + \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Top.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Top.Output.verified.txt new file mode 100644 index 0000000..7390030 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Top.Output.verified.txt @@ -0,0 +1,15 @@ + ┌──────────────┐ + │ Hello World! │ + └──────────────┘ + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Bottom.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Bottom.Output.verified.txt new file mode 100644 index 0000000..5c9e9c0 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Bottom.Output.verified.txt @@ -0,0 +1,15 @@ + + + + + + + + + + + + +┌──────────────┐ +│ Hello World! │ +└──────────────┘ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Middle.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Middle.Output.verified.txt new file mode 100644 index 0000000..f7beea5 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Middle.Output.verified.txt @@ -0,0 +1,15 @@ + + + + + + +┌──────────────┐ +│ Hello World! │ +└──────────────┘ + + + + + + \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Top.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Top.Output.verified.txt new file mode 100644 index 0000000..21d696c --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Top.Output.verified.txt @@ -0,0 +1,15 @@ +┌──────────────┐ +│ Hello World! │ +└──────────────┘ + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Bottom.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Bottom.Output.verified.txt new file mode 100644 index 0000000..d61cd86 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Bottom.Output.verified.txt @@ -0,0 +1,15 @@ + + + + + + + + + + + + + ┌──────────────┐ + │ Hello World! │ + └──────────────┘ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Middle.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Middle.Output.verified.txt new file mode 100644 index 0000000..90a5884 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Middle.Output.verified.txt @@ -0,0 +1,15 @@ + + + + + + + ┌──────────────┐ + │ Hello World! │ + └──────────────┘ + + + + + + \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Top.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Top.Output.verified.txt new file mode 100644 index 0000000..a1742a8 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Top.Output.verified.txt @@ -0,0 +1,15 @@ + ┌──────────────┐ + │ Hello World! │ + └──────────────┘ + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Empty_Layout.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Empty_Layout.Output.verified.txt new file mode 100644 index 0000000..5caaedb --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Empty_Layout.Output.verified.txt @@ -0,0 +1,15 @@ +╭─40 x 15──────────────────────────────╮ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ Placeholder │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰──────────────────────────────────────╯ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Fallback_Layout.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Fallback_Layout.Output.verified.txt new file mode 100644 index 0000000..0df2d79 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Fallback_Layout.Output.verified.txt @@ -0,0 +1,15 @@ +╭─T1 (40 x 7)──────────────────────────╮ +│ │ +│ │ +│ Placeholder │ +│ │ +│ │ +╰──────────────────────────────────────╯ +╭─T2 (40 x 8)──────────────────────────╮ +│ │ +│ │ +│ Placeholder │ +│ │ +│ │ +│ │ +╰──────────────────────────────────────╯ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout.Output.verified.txt new file mode 100644 index 0000000..614b3a1 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout.Output.verified.txt @@ -0,0 +1,15 @@ +╔══════════════════════════════════════╗ +║ Hello ║ +║ ║ +║ ║ +║ ║ +║ ║ +║ ║ +║ ║ +║ ║ +║ ║ +║ ║ +║ ║ +║ ║ +║ ║ +╚══════════════════════════════════════╝ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Columns.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Columns.Output.verified.txt new file mode 100644 index 0000000..5fea3f1 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Columns.Output.verified.txt @@ -0,0 +1,15 @@ +╭─Left (20 x 15)───╮╭─Right (20 x 15)──╮ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ Placeholder ││ Placeholder │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +╰──────────────────╯╰──────────────────╯ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Columns.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Columns.Output.verified.txt new file mode 100644 index 0000000..9c7084a --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Columns.Output.verified.txt @@ -0,0 +1,15 @@ +╭─L1…────╮╭─L2…────╮╭─R1…────╮╭─R2…────╮ +│ ││ ││ ││ │ +│ ││ ││ ││ │ +│ ││ ││ ││ │ +│ ││ ││ ││ │ +│ ││ ││ ││ │ +│ Placeh ││ Placeh ││ Placeh ││ Placeh │ +│ older ││ older ││ older ││ older │ +│ ││ ││ ││ │ +│ ││ ││ ││ │ +│ ││ ││ ││ │ +│ ││ ││ ││ │ +│ ││ ││ ││ │ +│ ││ ││ ││ │ +╰────────╯╰────────╯╰────────╯╰────────╯ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Rows.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Rows.Output.verified.txt new file mode 100644 index 0000000..bb5f7d9 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Rows.Output.verified.txt @@ -0,0 +1,15 @@ +╭─T1 (40 x 3)──────────────────────────╮ +│ Placeholder │ +╰──────────────────────────────────────╯ +╭─T2 (40 x 4)──────────────────────────╮ +│ Placeholder │ +│ │ +╰──────────────────────────────────────╯ +╭─B1 (40 x 4)──────────────────────────╮ +│ Placeholder │ +│ │ +╰──────────────────────────────────────╯ +╭─B2 (40 x 4)──────────────────────────╮ +│ Placeholder │ +│ │ +╰──────────────────────────────────────╯ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Rows_And_Columns.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Rows_And_Columns.Output.verified.txt new file mode 100644 index 0000000..258288b --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Rows_And_Columns.Output.verified.txt @@ -0,0 +1,15 @@ +╭─A (20 x 3)───────╮╭─B (20 x 3)───────╮ +│ Placeholder ││ Placeholder │ +╰──────────────────╯╰──────────────────╯ +╭─C (20 x 4)───────╮╭─D (20 x 4)───────╮ +│ Placeholder ││ Placeholder │ +│ ││ │ +╰──────────────────╯╰──────────────────╯ +╭─E (20 x 4)───────╮╭─F (20 x 4)───────╮ +│ Placeholder ││ Placeholder │ +│ ││ │ +╰──────────────────╯╰──────────────────╯ +╭─G (20 x 4)───────╮╭─H (20 x 4)───────╮ +│ Placeholder ││ Placeholder │ +│ ││ │ +╰──────────────────╯╰──────────────────╯ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Minimum_Size.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Minimum_Size.Output.verified.txt new file mode 100644 index 0000000..5a21d74 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Minimum_Size.Output.verified.txt @@ -0,0 +1,15 @@ +╭─Left (30 x 15)─────────────╮╭─Right…─╮ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ Placeh │ +│ Placeholder ││ older │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +╰────────────────────────────╯╰────────╯ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Ratio.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Ratio.Output.verified.txt new file mode 100644 index 0000000..5a21d74 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Ratio.Output.verified.txt @@ -0,0 +1,15 @@ +╭─Left (30 x 15)─────────────╮╭─Right…─╮ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ Placeh │ +│ Placeholder ││ older │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +╰────────────────────────────╯╰────────╯ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Size.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Size.Output.verified.txt new file mode 100644 index 0000000..6ffd309 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Size.Output.verified.txt @@ -0,0 +1,15 @@ +╭─Left (28 x 15)───────────╮╭─Right…───╮ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ Placehol │ +│ Placeholder ││ der │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +╰──────────────────────────╯╰──────────╯ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Rows.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Rows.Output.verified.txt new file mode 100644 index 0000000..12a6f9d --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Rows.Output.verified.txt @@ -0,0 +1,15 @@ +╭─Top (40 x 7)─────────────────────────╮ +│ │ +│ │ +│ Placeholder │ +│ │ +│ │ +╰──────────────────────────────────────╯ +╭─Bottom (40 x 8)──────────────────────╮ +│ │ +│ │ +│ Placeholder │ +│ │ +│ │ +│ │ +╰──────────────────────────────────────╯ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_Without_Invisible_Children.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_Without_Invisible_Children.Output.verified.txt new file mode 100644 index 0000000..9468df9 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_Without_Invisible_Children.Output.verified.txt @@ -0,0 +1,15 @@ +╭─B (40 x 3)───────────────────────────╮ +│ Placeholder │ +╰──────────────────────────────────────╯ +╭─C (20 x 4)───────╮╭─D (20 x 4)───────╮ +│ Placeholder ││ Placeholder │ +│ ││ │ +╰──────────────────╯╰──────────────────╯ +╭─E (20 x 4)───────╮╭─F (20 x 4)───────╮ +│ Placeholder ││ Placeholder │ +│ ││ │ +╰──────────────────╯╰──────────────────╯ +╭─G (40 x 4)───────────────────────────╮ +│ Placeholder │ +│ │ +╰──────────────────────────────────────╯ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Height.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Height.Output.verified.txt new file mode 100644 index 0000000..554308f --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Height.Output.verified.txt @@ -0,0 +1,25 @@ +┌───────────────────┐ +│ Hello World │ +│ Hello Hello Hello │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└───────────────────┘ diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width.Output.verified.txt new file mode 100644 index 0000000..dc133c3 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width.Output.verified.txt @@ -0,0 +1,3 @@ +┌───────────────────────┐ +│ Hello World │ +└───────────────────────┘ diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width_Height.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width_Height.Output.verified.txt new file mode 100644 index 0000000..2f85980 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width_Height.Output.verified.txt @@ -0,0 +1,25 @@ +┌────────────────────────────────────────────────┐ +│ Hello World │ +│ Hello Hello Hello │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└────────────────────────────────────────────────┘ diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width_MaxWidth.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width_MaxWidth.Output.verified.txt new file mode 100644 index 0000000..21ceda9 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width_MaxWidth.Output.verified.txt @@ -0,0 +1,3 @@ +┌──────────────────┐ +│ Hello World │ +└──────────────────┘ diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_Centered.Align_Widget.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_Centered.Align_Widget.verified.txt new file mode 100644 index 0000000..62617a0 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_Centered.Align_Widget.verified.txt @@ -0,0 +1,6 @@ + ┌────────┬────────┬───────┐ + │ Foo │ Bar │ Baz │ + ├────────┼────────┼───────┤ + │ Qux │ Corgi │ Waldo │ + │ Grault │ Garply │ Fred │ + └────────┴────────┴───────┘ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_LeftAligned.Align_Widget.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_LeftAligned.Align_Widget.verified.txt new file mode 100644 index 0000000..6edca8f --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_LeftAligned.Align_Widget.verified.txt @@ -0,0 +1,6 @@ +┌────────┬────────┬───────┐ +│ Foo │ Bar │ Baz │ +├────────┼────────┼───────┤ +│ Qux │ Corgi │ Waldo │ +│ Grault │ Garply │ Fred │ +└────────┴────────┴───────┘ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_RightAligned.Align_Widget.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_RightAligned.Align_Widget.verified.txt new file mode 100644 index 0000000..72a459e --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_RightAligned.Align_Widget.verified.txt @@ -0,0 +1,6 @@ + ┌────────┬────────┬───────┐ + │ Foo │ Bar │ Baz │ + ├────────┼────────┼───────┤ + │ Qux │ Corgi │ Waldo │ + │ Grault │ Garply │ Fred │ + └────────┴────────┴───────┘ \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Spectre.Console.Tests.csproj b/test/Spectre.Console.Tests/Spectre.Console.Tests.csproj index 0342253..4d74810 100644 --- a/test/Spectre.Console.Tests/Spectre.Console.Tests.csproj +++ b/test/Spectre.Console.Tests/Spectre.Console.Tests.csproj @@ -1,8 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('Windows'))">net7.0;net48</TargetFrameworks> - <TargetFrameworks Condition="!$([MSBuild]::IsOSPlatform('Windows'))">net7.0</TargetFrameworks> + <TargetFrameworks>net7.0</TargetFrameworks> </PropertyGroup> <ItemGroup> diff --git a/test/Spectre.Console.Tests/Unit/Live/Progress/ProgressColumnFixture.cs b/test/Spectre.Console.Tests/Unit/Live/Progress/ProgressColumnFixture.cs index 4b10877..9a2fac2 100644 --- a/test/Spectre.Console.Tests/Unit/Live/Progress/ProgressColumnFixture.cs +++ b/test/Spectre.Console.Tests/Unit/Live/Progress/ProgressColumnFixture.cs @@ -16,7 +16,7 @@ public sealed class ProgressColumnFixture<T> public string Render() { var console = new TestConsole(); - var context = new RenderContext(console.Profile.Capabilities); + var context = RenderOptions.Create(console, console.Profile.Capabilities); console.Write(Column.Render(context, Task, TimeSpan.Zero)); return console.Output; } diff --git a/test/Spectre.Console.Tests/Unit/Rendering/RenderHookTests.cs b/test/Spectre.Console.Tests/Unit/Rendering/RenderHookTests.cs index 219a13d..6be28da 100644 --- a/test/Spectre.Console.Tests/Unit/Rendering/RenderHookTests.cs +++ b/test/Spectre.Console.Tests/Unit/Rendering/RenderHookTests.cs @@ -4,7 +4,7 @@ public sealed class RenderHookTests { private sealed class HelloRenderHook : IRenderHook { - public IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables) + public IEnumerable<IRenderable> Process(RenderOptions options, IEnumerable<IRenderable> renderables) { return new IRenderable[] { new Text("Hello\n") }.Concat(renderables); } diff --git a/test/Spectre.Console.Tests/Unit/Widgets/AlignTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/AlignTests.cs new file mode 100644 index 0000000..e3f2923 --- /dev/null +++ b/test/Spectre.Console.Tests/Unit/Widgets/AlignTests.cs @@ -0,0 +1,155 @@ +using Spectre.Console.Extensions; + +namespace Spectre.Console.Tests.Unit; + +[UsesVerify] +[ExpectationPath("Widgets/Align")] +public sealed class AlignTests +{ + [UsesVerify] + public sealed class Left + { + [Fact] + [Expectation("Left_Top")] + public Task Should_Render_Panel_Left_Aligned_At_Top() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var align = Align.Left(new Panel("Hello World!"), VerticalAlignment.Top).Height(15); + + // When + console.Write(align); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Left_Middle")] + public Task Should_Render_Panel_Left_Aligned_At_Middle() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var align = Align.Left(new Panel("Hello World!"), VerticalAlignment.Middle).Height(15); + + // When + console.Write(align); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Left_Bottom")] + public Task Should_Render_Panel_Left_Aligned_At_Bottom() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var align = Align.Left(new Panel("Hello World!"), VerticalAlignment.Bottom).Height(15); + + // When + console.Write(align); + + // Then + return Verifier.Verify(console.Output); + } + } + + [UsesVerify] + public sealed class Center + { + [Fact] + [Expectation("Center_Top")] + public Task Should_Render_Panel_Center_Aligned_At_Top() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var align = Align.Center(new Panel("Hello World!"), VerticalAlignment.Top).Height(15); + + // When + console.Write(align); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Center_Middle")] + public Task Should_Render_Panel_Center_Aligned_At_Middle() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var align = Align.Center(new Panel("Hello World!"), VerticalAlignment.Middle).Height(15); + + // When + console.Write(align); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Center_Bottom")] + public Task Should_Render_Panel_Center_Aligned_At_Bottom() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var align = Align.Center(new Panel("Hello World!"), VerticalAlignment.Bottom).Height(15); + + // When + console.Write(align); + + // Then + return Verifier.Verify(console.Output); + } + } + + [UsesVerify] + public sealed class Right + { + [Fact] + [Expectation("Right_Top")] + public Task Should_Render_Panel_Right_Aligned_At_Top() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var align = Align.Right(new Panel("Hello World!"), VerticalAlignment.Top).Height(15); + + // When + console.Write(align); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Right_Middle")] + public Task Should_Render_Panel_Right_Aligned_At_Middle() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var align = Align.Right(new Panel("Hello World!"), VerticalAlignment.Middle).Height(15); + + // When + console.Write(align); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Right_Bottom")] + public Task Should_Render_Panel_Right_Aligned_At_Bottom() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var align = Align.Right(new Panel("Hello World!"), VerticalAlignment.Bottom).Height(15); + + // When + console.Write(align); + + // Then + return Verifier.Verify(console.Output); + } + } +} diff --git a/test/Spectre.Console.Tests/Unit/Widgets/FigletTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/FigletTests.cs index 8453901..b283176 100644 --- a/test/Spectre.Console.Tests/Unit/Widgets/FigletTests.cs +++ b/test/Spectre.Console.Tests/Unit/Widgets/FigletTests.cs @@ -60,7 +60,7 @@ public sealed class FigletTests // Given var console = new TestConsole().Width(120); var text = new FigletText(FigletFont.Default, "Spectre.Console") - .Alignment(Justify.Left); + .Justify(Justify.Left); // When console.Write(text); @@ -76,7 +76,7 @@ public sealed class FigletTests // Given var console = new TestConsole().Width(120); var text = new FigletText(FigletFont.Default, "Spectre.Console") - .Alignment(Justify.Center); + .Justify(Justify.Center); // When console.Write(text); @@ -92,7 +92,7 @@ public sealed class FigletTests // Given var console = new TestConsole().Width(120); var text = new FigletText(FigletFont.Default, "Spectre.Console") - .Alignment(Justify.Right); + .Justify(Justify.Right); // When console.Write(text); diff --git a/test/Spectre.Console.Tests/Unit/Widgets/LayoutTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/LayoutTests.cs new file mode 100644 index 0000000..fc0017e --- /dev/null +++ b/test/Spectre.Console.Tests/Unit/Widgets/LayoutTests.cs @@ -0,0 +1,268 @@ +namespace Spectre.Console.Tests.Unit; + +[UsesVerify] +[ExpectationPath("Widgets/Layout")] +public sealed class LayoutTests +{ + [Fact] + [Expectation("Render_Empty_Layout")] + public Task Should_Render_Empty_Layout() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var layout = new Layout(); + + // When + console.Write(layout); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Render_Layout")] + public Task Should_Render_Empty_Layout_With_Renderable() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var layout = new Layout().Update(new Panel("Hello").DoubleBorder().Expand()); + + // When + console.Write(layout); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Render_Layout_With_Columns")] + public Task Should_Render_Layout_With_Columns() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var layout = new Layout() + .SplitColumns( + new Layout("Left"), + new Layout("Right")); + + // When + console.Write(layout); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Render_Layout_With_Rows")] + public Task Should_Render_Layout_With_Rows() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var layout = new Layout() + .SplitRows( + new Layout("Top"), + new Layout("Bottom")); + + // When + console.Write(layout); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Render_Layout_With_Nested_Columns")] + public Task Should_Render_Layout_With_Nested_Columns() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var layout = new Layout() + .SplitColumns( + new Layout("Left") + .SplitColumns( + new Layout("L1"), + new Layout("L2")), + new Layout("Right") + .SplitColumns( + new Layout("R1"), + new Layout("R2"))); + + // When + console.Write(layout); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Render_Layout_With_Nested_Rows")] + public Task Should_Render_Layout_With_Nested_Rows() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var layout = new Layout() + .SplitRows( + new Layout("Top") + .SplitRows( + new Layout("T1"), + new Layout("T2")), + new Layout("Bottom") + .SplitRows( + new Layout("B1"), + new Layout("B2"))); + + // When + console.Write(layout); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Render_Layout_With_Nested_Rows_And_Columns")] + public Task Should_Render_Layout_With_Nested_Rows_And_Columns() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var layout = new Layout() + .SplitRows( + new Layout("Top") + .SplitRows( + new Layout("T1") + .SplitColumns( + new Layout("A"), + new Layout("B")), + new Layout("T2") + .SplitColumns( + new Layout("C"), + new Layout("D"))), + new Layout("Bottom") + .SplitRows( + new Layout("B1") + .SplitColumns( + new Layout("E"), + new Layout("F")), + new Layout("B2") + .SplitColumns( + new Layout("G"), + new Layout("H")))); + + // When + console.Write(layout); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Render_Layout_Without_Invisible_Children")] + public Task Should_Render_Layout_Without_Invisible_Children() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var layout = new Layout() + .SplitRows( + new Layout("Top") + .SplitRows( + new Layout("T1") + .SplitColumns( + new Layout("A").Invisible(), + new Layout("B")), + new Layout("T2") + .SplitColumns( + new Layout("C"), + new Layout("D"))), + new Layout("Bottom") + .SplitRows( + new Layout("B1") + .SplitColumns( + new Layout("E"), + new Layout("F")), + new Layout("B2") + .SplitColumns( + new Layout("G"), + new Layout("H").Invisible()))); + + // When + console.Write(layout); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Render_Layout_With_Respect_To_Ratio")] + public Task Should_Render_Layout_With_Respect_To_Ratio() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var layout = new Layout() + .SplitColumns( + new Layout("Left").Ratio(3), + new Layout("Right")); + + // When + console.Write(layout); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Render_Layout_With_Respect_To_Size")] + public Task Should_Render_Layout_With_Respect_To_Size() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var layout = new Layout() + .SplitColumns( + new Layout("Left").Size(28), + new Layout("Right")); + + // When + console.Write(layout); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Render_Layout_With_Respect_To_Minimum_Size")] + public Task Should_Render_Layout_With_Respect_To_Minimum_Size() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var layout = new Layout() + .SplitColumns( + new Layout("Left").Size(28).MinimumSize(30), + new Layout("Right")); + + // When + console.Write(layout); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Render_Fallback_Layout")] + public Task Should_Fall_Back_To_Parent_Layout_If_All_Children_Are_Invisible() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var layout = new Layout() + .SplitRows( + new Layout("T1").SplitColumns( + new Layout("A").Invisible(), + new Layout("B").Invisible()), + new Layout("T2").SplitColumns( + new Layout("C").Invisible(), + new Layout("D").Invisible())); + + // When + console.Write(layout); + + // Then + return Verifier.Verify(console.Output); + } +} diff --git a/test/Spectre.Console.Tests/Unit/Widgets/PanelTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/PanelTests.cs index 73a2ca9..377c94f 100644 --- a/test/Spectre.Console.Tests/Unit/Widgets/PanelTests.cs +++ b/test/Spectre.Console.Tests/Unit/Widgets/PanelTests.cs @@ -81,7 +81,7 @@ public sealed class PanelTests // When console.Write(new Panel("Hello World") { - Header = new PanelHeader("Greeting").LeftAligned(), + Header = new PanelHeader("Greeting").LeftJustified(), Expand = true, }); @@ -117,7 +117,7 @@ public sealed class PanelTests // When console.Write(new Panel("Hello World") { - Header = new PanelHeader("Greeting").RightAligned(), + Header = new PanelHeader("Greeting").RightJustified(), Expand = true, }); @@ -204,6 +204,76 @@ public sealed class PanelTests return Verifier.Verify(console.Output); } + [Fact] + [Expectation("Render_Width")] + public Task Should_Render_To_Specified_Width() + { + // Given + var console = new TestConsole(); + + // When + console.Write(new Panel(new Text("Hello World")) + { + Width = 25, + }); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Render_Width_MaxWidth")] + public Task Should_Use_Max_Width_If_Specified_Width_Is_Too_Large() + { + // Given + var console = new TestConsole(); + console.Profile.Width = 20; + + // When + console.Write(new Panel(new Text("Hello World")) + { + Width = 25, + }); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Render_Height")] + public Task Should_Render_To_Specified_Height() + { + // Given + var console = new TestConsole(); + + // When + console.Write(new Panel(new Text("Hello World\nHello Hello Hello")) + { + Height = 25, + }); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Render_Width_Height")] + public Task Should_Render_To_Specified_Width_And_Height() + { + // Given + var console = new TestConsole(); + + // When + console.Write(new Panel("Hello World\nHello Hello Hello") + { + Width = 50, + Height = 25, + }); + + // Then + return Verifier.Verify(console.Output); + } + [Fact] [Expectation("Render_Child_RightAligned")] public Task Should_Justify_Child_To_Right_Correctly() @@ -213,7 +283,7 @@ public sealed class PanelTests // When console.Write( - new Panel(new Text("Hello World").RightAligned()) + new Panel(new Text("Hello World").RightJustified()) { Expand = true, }); diff --git a/test/Spectre.Console.Tests/Unit/Widgets/RuleTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/RuleTests.cs index fbff7b5..fada3fd 100644 --- a/test/Spectre.Console.Tests/Unit/Widgets/RuleTests.cs +++ b/test/Spectre.Console.Tests/Unit/Widgets/RuleTests.cs @@ -70,7 +70,7 @@ public sealed class RuleTests // When console.Write(new Rule("Hello World") { - Alignment = Justify.Left, + Justification = Justify.Left, }); // Then @@ -87,7 +87,7 @@ public sealed class RuleTests // When console.Write(new Rule("Hello World") { - Alignment = Justify.Right, + Justification = Justify.Right, }); // Then diff --git a/test/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs index 2716fbb..a027658 100644 --- a/test/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs +++ b/test/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs @@ -187,7 +187,9 @@ public sealed class TableTests // Given var console = new TestConsole(); var table = new Table(); +#pragma warning disable CS0618 // Type or member is obsolete table.Alignment = Justify.Left; +#pragma warning restore CS0618 // Type or member is obsolete table.AddColumns("Foo", "Bar", "Baz"); table.AddRow("Qux", "Corgi", "Waldo"); table.AddRow("Grault", "Garply", "Fred"); @@ -199,6 +201,24 @@ public sealed class TableTests return Verifier.Verify(console.Output); } + [Fact] + [Expectation("Render_LeftAligned", "Align_Widget")] + public Task Should_Left_Align_Table_Correctly_When_Wrapped_In_Align_Widget() + { + // Given + var console = new TestConsole(); + var table = new Table(); + table.AddColumns("Foo", "Bar", "Baz"); + table.AddRow("Qux", "Corgi", "Waldo"); + table.AddRow("Grault", "Garply", "Fred"); + + // When + console.Write(Align.Left(table)); + + // Then + return Verifier.Verify(console.Output); + } + [Fact] [Expectation("Render_Centered")] public Task Should_Center_Table_Correctly() @@ -206,7 +226,9 @@ public sealed class TableTests // Given var console = new TestConsole(); var table = new Table(); +#pragma warning disable CS0618 // Type or member is obsolete table.Alignment = Justify.Center; +#pragma warning restore CS0618 // Type or member is obsolete table.AddColumns("Foo", "Bar", "Baz"); table.AddRow("Qux", "Corgi", "Waldo"); table.AddRow("Grault", "Garply", "Fred"); @@ -218,6 +240,24 @@ public sealed class TableTests return Verifier.Verify(console.Output); } + [Fact] + [Expectation("Render_Centered", "Align_Widget")] + public Task Should_Center_Table_Correctly_When_Wrapped_In_Align_Widget() + { + // Given + var console = new TestConsole(); + var table = new Table(); + table.AddColumns("Foo", "Bar", "Baz"); + table.AddRow("Qux", "Corgi", "Waldo"); + table.AddRow("Grault", "Garply", "Fred"); + + // When + console.Write(Align.Center(table)); + + // Then + return Verifier.Verify(console.Output); + } + [Fact] [Expectation("Render_RightAligned")] public Task Should_Right_Align_Table_Correctly() @@ -225,7 +265,9 @@ public sealed class TableTests // Given var console = new TestConsole(); var table = new Table(); +#pragma warning disable CS0618 // Type or member is obsolete table.Alignment = Justify.Right; +#pragma warning restore CS0618 // Type or member is obsolete table.AddColumns("Foo", "Bar", "Baz"); table.AddRow("Qux", "Corgi", "Waldo"); table.AddRow("Grault", "Garply", "Fred"); @@ -237,6 +279,24 @@ public sealed class TableTests return Verifier.Verify(console.Output); } + [Fact] + [Expectation("Render_RightAligned", "Align_Widget")] + public Task Should_Right_Align_Table_Correctly_When_Wrapped_In_Align_Widget() + { + // Given + var console = new TestConsole(); + var table = new Table(); + table.AddColumns("Foo", "Bar", "Baz"); + table.AddRow("Qux", "Corgi", "Waldo"); + table.AddRow("Grault", "Garply", "Fred"); + + // When + console.Write(Align.Right(table)); + + // Then + return Verifier.Verify(console.Output); + } + [Fact] [Expectation("Render_Nested")] public Task Should_Render_Table_Nested_In_Panels_Correctly() @@ -380,7 +440,7 @@ public sealed class TableTests table.AddColumn(new TableColumn(new Panel("[u]DEF[/]").BorderColor(Color.Green))); table.AddColumn(new TableColumn(new Panel("[u]GHI[/]").BorderColor(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(second, new Text("Whaat"), new Text("Lol").RightJustified()); table.AddRow(new Markup("[blue]Hej[/]"), new Markup("[yellow]Världen[/]"), Text.Empty); // When diff --git a/test/Spectre.Console.Tests/Unit/Widgets/TextPathTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/TextPathTests.cs index 0f984be..d7e5513 100644 --- a/test/Spectre.Console.Tests/Unit/Widgets/TextPathTests.cs +++ b/test/Spectre.Console.Tests/Unit/Widgets/TextPathTests.cs @@ -77,7 +77,7 @@ public sealed class TextPathTests var console = new TestConsole().Width(40); // When - console.Write(new TextPath("C:/My documents/Bar/Baz.txt").RightAligned()); + console.Write(new TextPath("C:/My documents/Bar/Baz.txt").RightJustified()); // Then console.Output.TrimEnd('\n') diff --git a/test/Spectre.Console.Tests/Unit/Widgets/TextTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/TextTests.cs index cf08e23..6b0bdfd 100644 --- a/test/Spectre.Console.Tests/Unit/Widgets/TextTests.cs +++ b/test/Spectre.Console.Tests/Unit/Widgets/TextTests.cs @@ -42,11 +42,13 @@ public sealed class TextTests public void Should_Consider_The_Longest_Word_As_Minimum_Width() { // Given + var console = new TestConsole(); var caps = new TestCapabilities { Unicode = true }; var text = new Text("Foo Bar Baz\nQux\nLol mobile"); // When - var result = ((IRenderable)text).Measure(caps.CreateRenderContext(), 80); + var result = ((IRenderable)text).Measure( + caps.CreateRenderContext(console), 80); // Then result.Min.ShouldBe(6); @@ -56,11 +58,13 @@ public sealed class TextTests public void Should_Consider_The_Longest_Line_As_Maximum_Width() { // Given + var console = new TestConsole(); var caps = new TestCapabilities { Unicode = true }; var text = new Text("Foo Bar Baz\nQux\nLol mobile"); // When - var result = ((IRenderable)text).Measure(caps.CreateRenderContext(), 80); + var result = ((IRenderable)text).Measure( + caps.CreateRenderContext(console), 80); // Then result.Max.ShouldBe(11);