diff --git a/README.jp.md b/README.jp.md index ec1aa1f..d9a072d 100644 --- a/README.jp.md +++ b/README.jp.md @@ -96,21 +96,22 @@ Spectre.Consoleでできることを見るために、 ``` > dotnet example -┌────────────┬───────────────────────────────────────┬───────────────────────────────────────────────────┐ -│ Name │ Path │ Description │ -├────────────┼───────────────────────────────────────┼───────────────────────────────────────────────────┤ -│ Borders │ examples/Borders/Borders.csproj │ Demonstrates the different kind of borders. │ -│ Calendars │ examples/Calendars/Calendars.csproj │ Demonstrates how to render calendars. │ -│ Colors │ examples/Colors/Colors.csproj │ Demonstrates how to use colors in the console. │ -│ Columns │ examples/Columns/Columns.csproj │ Demonstrates how to render data into columns. │ -│ Emojis │ examples/Emojis/Emojis.csproj │ Demonstrates how to render emojis. │ -│ Exceptions │ examples/Exceptions/Exceptions.csproj │ Demonstrates how to render formatted exceptions. │ -│ Grids │ examples/Grids/Grids.csproj │ Demonstrates how to render grids in a console. │ -│ Info │ examples/Info/Info.csproj │ Displays the capabilities of the current console. │ -│ Links │ examples/Links/Links.csproj │ Demonstrates how to render links in a console. │ -│ Panels │ examples/Panels/Panels.csproj │ Demonstrates how to render items in panels. │ -│ Tables │ examples/Tables/Tables.csproj │ Demonstrates how to render tables in a console. │ -└────────────┴───────────────────────────────────────┴───────────────────────────────────────────────────┘ +╭────────────┬───────────────────────────────────────┬──────────────────────────────────────────────────────╮ +│ Name │ Path │ Description │ +├────────────┼───────────────────────────────────────┼──────────────────────────────────────────────────────┤ +│ Borders │ examples/Borders/Borders.csproj │ Demonstrates the different kind of borders. │ +│ Calendars │ examples/Calendars/Calendars.csproj │ Demonstrates how to render calendars. │ +│ Colors │ examples/Colors/Colors.csproj │ Demonstrates how to use colors in the console. │ +│ Columns │ examples/Columns/Columns.csproj │ Demonstrates how to render data into columns. │ +│ Emojis │ examples/Emojis/Emojis.csproj │ Demonstrates how to render emojis. │ +│ Exceptions │ examples/Exceptions/Exceptions.csproj │ Demonstrates how to render formatted exceptions. │ +│ Grids │ examples/Grids/Grids.csproj │ Demonstrates how to render grids in a console. │ +│ Info │ examples/Info/Info.csproj │ Displays the capabilities of the current console. │ +│ Links │ examples/Links/Links.csproj │ Demonstrates how to render links in a console. │ +│ Panels │ examples/Panels/Panels.csproj │ Demonstrates how to render items in panels. │ +│ Rules │ examples/Rules/Rules.csproj │ Demonstrates how to render horizontal rules (lines). │ +│ Tables │ examples/Tables/Tables.csproj │ Demonstrates how to render tables in a console. │ +╰────────────┴───────────────────────────────────────┴──────────────────────────────────────────────────────╯ ``` そして、例を実行します diff --git a/README.md b/README.md index 06fe9a8..2402f61 100644 --- a/README.md +++ b/README.md @@ -108,21 +108,22 @@ Now you can list available examples in this repository: ``` > dotnet example -┌────────────┬───────────────────────────────────────┬───────────────────────────────────────────────────┐ -│ Name │ Path │ Description │ -├────────────┼───────────────────────────────────────┼───────────────────────────────────────────────────┤ -│ Borders │ examples/Borders/Borders.csproj │ Demonstrates the different kind of borders. │ -│ Calendars │ examples/Calendars/Calendars.csproj │ Demonstrates how to render calendars. │ -│ Colors │ examples/Colors/Colors.csproj │ Demonstrates how to use colors in the console. │ -│ Columns │ examples/Columns/Columns.csproj │ Demonstrates how to render data into columns. │ -│ Emojis │ examples/Emojis/Emojis.csproj │ Demonstrates how to render emojis. │ -│ Exceptions │ examples/Exceptions/Exceptions.csproj │ Demonstrates how to render formatted exceptions. │ -│ Grids │ examples/Grids/Grids.csproj │ Demonstrates how to render grids in a console. │ -│ Info │ examples/Info/Info.csproj │ Displays the capabilities of the current console. │ -│ Links │ examples/Links/Links.csproj │ Demonstrates how to render links in a console. │ -│ Panels │ examples/Panels/Panels.csproj │ Demonstrates how to render items in panels. │ -│ Tables │ examples/Tables/Tables.csproj │ Demonstrates how to render tables in a console. │ -└────────────┴───────────────────────────────────────┴───────────────────────────────────────────────────┘ +╭────────────┬───────────────────────────────────────┬──────────────────────────────────────────────────────╮ +│ Name │ Path │ Description │ +├────────────┼───────────────────────────────────────┼──────────────────────────────────────────────────────┤ +│ Borders │ examples/Borders/Borders.csproj │ Demonstrates the different kind of borders. │ +│ Calendars │ examples/Calendars/Calendars.csproj │ Demonstrates how to render calendars. │ +│ Colors │ examples/Colors/Colors.csproj │ Demonstrates how to use colors in the console. │ +│ Columns │ examples/Columns/Columns.csproj │ Demonstrates how to render data into columns. │ +│ Emojis │ examples/Emojis/Emojis.csproj │ Demonstrates how to render emojis. │ +│ Exceptions │ examples/Exceptions/Exceptions.csproj │ Demonstrates how to render formatted exceptions. │ +│ Grids │ examples/Grids/Grids.csproj │ Demonstrates how to render grids in a console. │ +│ Info │ examples/Info/Info.csproj │ Displays the capabilities of the current console. │ +│ Links │ examples/Links/Links.csproj │ Demonstrates how to render links in a console. │ +│ Panels │ examples/Panels/Panels.csproj │ Demonstrates how to render items in panels. │ +│ Rules │ examples/Rules/Rules.csproj │ Demonstrates how to render horizontal rules (lines). │ +│ Tables │ examples/Tables/Tables.csproj │ Demonstrates how to render tables in a console. │ +╰────────────┴───────────────────────────────────────┴──────────────────────────────────────────────────────╯ ``` And to run an example: diff --git a/examples/Borders/Program.cs b/examples/Borders/Program.cs index 4cba1c9..9b6d02b 100644 --- a/examples/Borders/Program.cs +++ b/examples/Borders/Program.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Spectre.Console; using Spectre.Console.Rendering; @@ -7,6 +8,8 @@ namespace BordersExample { public static void Main() { + Debugger.Launch(); + // Render panel borders AnsiConsole.WriteLine(); AnsiConsole.MarkupLine("[white bold underline]PANEL BORDERS[/]"); diff --git a/examples/Colors/Program.cs b/examples/Colors/Program.cs index 0effd49..ba0c52c 100644 --- a/examples/Colors/Program.cs +++ b/examples/Colors/Program.cs @@ -1,3 +1,4 @@ +using System; using Spectre.Console; namespace ColorExample @@ -24,7 +25,7 @@ namespace ColorExample AnsiConsole.ResetColors(); AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine("[bold underline]3-bit Colors[/]"); + AnsiConsole.Render(new Rule("[yellow bold underline]3-bit Colors[/]").SetStyle("grey").LeftAligned()); AnsiConsole.WriteLine(); for (var i = 0; i < 8; i++) @@ -47,7 +48,7 @@ namespace ColorExample AnsiConsole.ResetColors(); AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine("[bold underline]4-bit Colors[/]"); + AnsiConsole.Render(new Rule("[yellow bold underline]4-bit Colors[/]").SetStyle("grey").LeftAligned()); AnsiConsole.WriteLine(); for (var i = 0; i < 16; i++) @@ -70,7 +71,7 @@ namespace ColorExample AnsiConsole.ResetColors(); AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine("[bold underline]8-bit Colors[/]"); + AnsiConsole.Render(new Rule("[yellow bold underline]8-bit Colors[/]").SetStyle("grey").LeftAligned()); AnsiConsole.WriteLine(); for (var i = 0; i < 16; i++) @@ -97,7 +98,7 @@ namespace ColorExample AnsiConsole.ResetColors(); AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine("[bold underline]24-bit Colors[/]"); + AnsiConsole.Render(new Rule("[yellow bold underline]24-bit Colors[/]").SetStyle("grey").LeftAligned()); AnsiConsole.WriteLine(); var index = 0; diff --git a/examples/Columns/Program.cs b/examples/Columns/Program.cs index ebc8098..70d28cf 100644 --- a/examples/Columns/Program.cs +++ b/examples/Columns/Program.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics; using System.Net.Http; using System.Threading.Tasks; using Newtonsoft.Json.Linq; diff --git a/examples/Exceptions/Program.cs b/examples/Exceptions/Program.cs index 1a644cb..6a9acc5 100644 --- a/examples/Exceptions/Program.cs +++ b/examples/Exceptions/Program.cs @@ -15,17 +15,17 @@ namespace Exceptions catch (Exception ex) { AnsiConsole.WriteLine(); - AnsiConsole.Render(new Panel("[u]Default[/]").Expand()); + AnsiConsole.Render(new Rule("Default").LeftAligned()); AnsiConsole.WriteLine(); AnsiConsole.WriteException(ex); AnsiConsole.WriteLine(); - AnsiConsole.Render(new Panel("[u]Compact[/]").Expand()); + AnsiConsole.Render(new Rule("Compact").LeftAligned()); AnsiConsole.WriteLine(); AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks); AnsiConsole.WriteLine(); - AnsiConsole.Render(new Panel("[u]Custom colors[/]").Expand()); + AnsiConsole.Render(new Rule("Compact + Custom colors").LeftAligned()); AnsiConsole.WriteLine(); AnsiConsole.WriteException(ex, new ExceptionSettings { diff --git a/examples/Rules/Program.cs b/examples/Rules/Program.cs new file mode 100644 index 0000000..b30498e --- /dev/null +++ b/examples/Rules/Program.cs @@ -0,0 +1,28 @@ +using Spectre.Console; + +namespace EmojiExample +{ + public static class Program + { + public static void Main(string[] args) + { + // No title + Render(new Rule().SetStyle("yellow")); + + // Left aligned title + Render(new Rule("[white]Left aligned[/]").LeftAligned().SetStyle("red")); + + // Centered title + Render(new Rule("[silver]Centered[/]").Centered().SetStyle("green")); + + // Right aligned title + Render(new Rule("[grey]Right aligned[/]").RightAligned().SetStyle("blue")); + } + + private static void Render(Rule rule) + { + AnsiConsole.Render(new Panel(rule).Expand().SetBorderStyle(Style.Parse("grey"))); + AnsiConsole.WriteLine(); + } + } +} diff --git a/examples/Rules/Rules.csproj b/examples/Rules/Rules.csproj new file mode 100644 index 0000000..b07b72c --- /dev/null +++ b/examples/Rules/Rules.csproj @@ -0,0 +1,15 @@ + + + + Exe + netcoreapp3.1 + false + Rules + Demonstrates how to render horizontal rules (lines). + + + + + + + diff --git a/src/Spectre.Console.Tests/Unit/RuleTests.cs b/src/Spectre.Console.Tests/Unit/RuleTests.cs new file mode 100644 index 0000000..3ebb653 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/RuleTests.cs @@ -0,0 +1,125 @@ +using Shouldly; +using Xunit; + +namespace Spectre.Console.Tests.Unit +{ + public sealed class RuleTests + { + [Fact] + public void Should_Render_Default_Rule_Without_Title() + { + // Given + var console = new PlainConsole(width: 40); + + // When + console.Render(new Rule()); + + // Then + console.Lines.Count.ShouldBe(1); + console.Lines[0].ShouldBe("────────────────────────────────────────"); + } + + [Fact] + public void Should_Render_Default_Rule_With_Title_Centered_By_Default() + { + // Given + var console = new PlainConsole(width: 40); + + // When + console.Render(new Rule("Hello World")); + + // Then + console.Lines.Count.ShouldBe(1); + console.Lines[0].ShouldBe("───────────── Hello World ──────────────"); + } + + [Fact] + public void Should_Render_Default_Rule_With_Title_Left_Aligned() + { + // Given + var console = new PlainConsole(width: 40); + + // When + console.Render(new Rule("Hello World") + { + Alignment = Justify.Left, + }); + + // Then + console.Lines.Count.ShouldBe(1); + console.Lines[0].ShouldBe("── Hello World ─────────────────────────"); + } + + [Fact] + public void Should_Render_Default_Rule_With_Title_Right_Aligned() + { + // Given + var console = new PlainConsole(width: 40); + + // When + console.Render(new Rule("Hello World") + { + Alignment = Justify.Right, + }); + + // Then + console.Lines.Count.ShouldBe(1); + console.Lines[0].ShouldBe("───────────────────────── Hello World ──"); + } + + [Fact] + public void Should_Convert_Line_Breaks_In_Title_To_Spaces() + { + // Given + var console = new PlainConsole(width: 40); + + // When + console.Render(new Rule("Hello\nWorld\r\n!")); + + // Then + console.Lines.Count.ShouldBe(1); + console.Lines[0].ShouldBe("──────────── Hello World ! ─────────────"); + } + + [Fact] + public void Should_Truncate_Title() + { + // Given + var console = new PlainConsole(width: 40); + + // When + console.Render(new Rule(" Hello World ")); + + // Then + console.Lines.Count.ShouldBe(1); + console.Lines[0].ShouldBe("───────────── Hello World ──────────────"); + } + + [Theory] + [InlineData(0, "Hello World Hello World Hello World Hello World Hello World", "")] + [InlineData(1, "Hello World Hello World Hello World Hello World Hello World", "─")] + [InlineData(2, "Hello World Hello World Hello World Hello World Hello World", "──")] + [InlineData(3, "Hello World Hello World Hello World Hello World Hello World", "───")] + [InlineData(4, "Hello World Hello World Hello World Hello World Hello World", "────")] + [InlineData(5, "Hello World Hello World Hello World Hello World Hello World", "─────")] + [InlineData(6, "Hello World Hello World Hello World Hello World Hello World", "──────")] + [InlineData(7, "Hello World Hello World Hello World Hello World Hello World", "───────")] + [InlineData(8, "Hello World Hello World Hello World Hello World Hello World", "── H… ──")] + [InlineData(8, "A", "── A ───")] + [InlineData(8, "AB", "── AB ──")] + [InlineData(8, "ABC", "── A… ──")] + [InlineData(40, "Hello World Hello World Hello World Hello World Hello World", "──── Hello World Hello World Hello… ────")] + public void Should_Truncate_Too_Long_Title(int width, string input, string expected) + { + // Given + var console = new PlainConsole(width); + + // When + console.Render(new Rule(input)); + + // Then + console.Lines.Count.ShouldBe(1); + console.Lines[0].ShouldBe(expected); + } + } +} diff --git a/src/Spectre.Console.sln b/src/Spectre.Console.sln index 50994c1..3109e9c 100644 --- a/src/Spectre.Console.sln +++ b/src/Spectre.Console.sln @@ -44,7 +44,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{C3E2CB ..\.github\workflows\publish.yaml = ..\.github\workflows\publish.yaml EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Calendars", "..\examples\Calendars\Calendars.csproj", "{57691C7D-683D-46E6-AA4F-57A8C5F65D25}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Calendars", "..\examples\Calendars\Calendars.csproj", "{57691C7D-683D-46E6-AA4F-57A8C5F65D25}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rules", "..\examples\Rules\Rules.csproj", "{8622A261-02C6-40CA-9797-E3F01ED87D6B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -212,6 +214,18 @@ Global {57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Release|x64.Build.0 = Release|Any CPU {57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Release|x86.ActiveCfg = Release|Any CPU {57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Release|x86.Build.0 = Release|Any CPU + {8622A261-02C6-40CA-9797-E3F01ED87D6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8622A261-02C6-40CA-9797-E3F01ED87D6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8622A261-02C6-40CA-9797-E3F01ED87D6B}.Debug|x64.ActiveCfg = Debug|Any CPU + {8622A261-02C6-40CA-9797-E3F01ED87D6B}.Debug|x64.Build.0 = Debug|Any CPU + {8622A261-02C6-40CA-9797-E3F01ED87D6B}.Debug|x86.ActiveCfg = Debug|Any CPU + {8622A261-02C6-40CA-9797-E3F01ED87D6B}.Debug|x86.Build.0 = Debug|Any CPU + {8622A261-02C6-40CA-9797-E3F01ED87D6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8622A261-02C6-40CA-9797-E3F01ED87D6B}.Release|Any CPU.Build.0 = Release|Any CPU + {8622A261-02C6-40CA-9797-E3F01ED87D6B}.Release|x64.ActiveCfg = Release|Any CPU + {8622A261-02C6-40CA-9797-E3F01ED87D6B}.Release|x64.Build.0 = Release|Any CPU + {8622A261-02C6-40CA-9797-E3F01ED87D6B}.Release|x86.ActiveCfg = Release|Any CPU + {8622A261-02C6-40CA-9797-E3F01ED87D6B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -229,6 +243,7 @@ Global {90C081A7-7C1D-4A4A-82B6-8FF473C3EA32} = {F0575243-121F-4DEE-9F6B-246E26DC0844} {C3E2CB5C-1517-4C75-B59A-93D4E22BEC8D} = {20595AD4-8D75-4AF8-B6BC-9C38C160423F} {57691C7D-683D-46E6-AA4F-57A8C5F65D25} = {F0575243-121F-4DEE-9F6B-246E26DC0844} + {8622A261-02C6-40CA-9797-E3F01ED87D6B} = {F0575243-121F-4DEE-9F6B-246E26DC0844} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C} diff --git a/src/Spectre.Console/Extensions/RuleExtensions.cs b/src/Spectre.Console/Extensions/RuleExtensions.cs new file mode 100644 index 0000000..6f74fbe --- /dev/null +++ b/src/Spectre.Console/Extensions/RuleExtensions.cs @@ -0,0 +1,75 @@ +using System; + +namespace Spectre.Console +{ + /// + /// Contains extension methods for . + /// + public static class RuleExtensions + { + /// + /// Sets the rule title. + /// + /// The rule. + /// The title. + /// The same instance so that multiple calls can be chained. + public static Rule SetTitle(this Rule rule, string title) + { + if (rule is null) + { + throw new ArgumentNullException(nameof(rule)); + } + + if (title is null) + { + throw new ArgumentNullException(nameof(title)); + } + + rule.Title = title; + return rule; + } + + /// + /// Sets the rule style. + /// + /// The rule. + /// The rule style string. + /// The same instance so that multiple calls can be chained. + public static Rule SetStyle(this Rule rule, string style) + { + if (rule is null) + { + throw new ArgumentNullException(nameof(rule)); + } + + if (style is null) + { + throw new ArgumentNullException(nameof(style)); + } + + return SetStyle(rule, Style.Parse(style)); + } + + /// + /// Sets the rule style. + /// + /// The rule. + /// The rule style. + /// The same instance so that multiple calls can be chained. + public static Rule SetStyle(this Rule rule, Style style) + { + if (rule is null) + { + throw new ArgumentNullException(nameof(rule)); + } + + if (style is null) + { + throw new ArgumentNullException(nameof(style)); + } + + rule.Style = style; + return rule; + } + } +} diff --git a/src/Spectre.Console/Rendering/RenderContext.cs b/src/Spectre.Console/Rendering/RenderContext.cs index 33b119b..644b443 100644 --- a/src/Spectre.Console/Rendering/RenderContext.cs +++ b/src/Spectre.Console/Rendering/RenderContext.cs @@ -27,6 +27,12 @@ namespace Spectre.Console.Rendering /// public Justify? Justification { get; } + /// + /// Gets a value indicating whether the context want items to render without + /// line breaks and return a single line where applicable. + /// + internal bool SingleLine { get; } + /// /// Initializes a new instance of the class. /// @@ -34,21 +40,42 @@ namespace Spectre.Console.Rendering /// A value indicating whether or not this a legacy console (i.e. cmd.exe). /// The justification to use when rendering. public RenderContext(Encoding encoding, bool legacyConsole, Justify? justification = null) + : this(encoding, legacyConsole, justification, false) + { + } + + private RenderContext(Encoding encoding, bool legacyConsole, Justify? justification = null, bool singleLine = false) { Encoding = encoding ?? throw new System.ArgumentNullException(nameof(encoding)); LegacyConsole = legacyConsole; Justification = justification; Unicode = Encoding == Encoding.UTF8 || Encoding == Encoding.Unicode; + SingleLine = singleLine; } /// /// Creates a new context with the specified justification. /// /// The justification. - /// A new instance with the specified justification. + /// A new instance. public RenderContext WithJustification(Justify? justification) { return new RenderContext(Encoding, LegacyConsole, justification); } + + /// + /// Creates a new context that tell 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. + /// + /// + /// Use with care since this has the potential to mess things up. + /// Only use this kind of context with items that you know about. + /// + /// A new instance. + internal RenderContext WithSingleLine() + { + return new RenderContext(Encoding, LegacyConsole, Justification, true); + } } } diff --git a/src/Spectre.Console/Rendering/Segment.cs b/src/Spectre.Console/Rendering/Segment.cs index 0bcd925..3256b7f 100644 --- a/src/Spectre.Console/Rendering/Segment.cs +++ b/src/Spectre.Console/Rendering/Segment.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; +using System.Text; using Spectre.Console.Internal; namespace Spectre.Console.Rendering @@ -125,48 +127,11 @@ namespace Spectre.Console.Rendering /// The render context. /// The segments to measure. /// The number of cells that the segments occupies in the console. - public static int CellLength(RenderContext context, List segments) + public static int CellLength(RenderContext context, IEnumerable segments) { return segments.Sum(segment => segment.CellLength(context)); } - /// - /// Truncates the segments to the specified width. - /// - /// The render context. - /// The segments to truncate. - /// The maximum width that the segments may occupy. - /// A list of segments that has been truncated. - public static List Truncate(RenderContext context, IEnumerable segments, int maxWidth) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (segments is null) - { - throw new ArgumentNullException(nameof(segments)); - } - - var result = new List(); - - var totalWidth = 0; - foreach (var segment in segments) - { - var segmentWidth = segment.CellLength(context); - if (totalWidth + segmentWidth > maxWidth) - { - break; - } - - result.Add(segment); - totalWidth += segmentWidth; - } - - return result; - } - /// /// Splits the provided segments into lines. /// @@ -387,6 +352,90 @@ namespace Spectre.Console.Rendering return result; } + /// + /// Truncates the segments to the specified width. + /// + /// The render context. + /// The segments to truncate. + /// The maximum width that the segments may occupy. + /// A list of segments that has been truncated. + public static List Truncate(RenderContext context, IEnumerable segments, int maxWidth) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (segments is null) + { + throw new ArgumentNullException(nameof(segments)); + } + + var result = new List(); + + var totalWidth = 0; + foreach (var segment in segments) + { + var segmentWidth = segment.CellLength(context); + if (totalWidth + segmentWidth > maxWidth) + { + break; + } + + result.Add(segment); + totalWidth += segmentWidth; + } + + if (result.Count == 0 && segments.Any()) + { + var segment = Truncate(context, segments.First(), maxWidth); + if (segment != null) + { + result.Add(segment); + } + } + + return result; + } + + /// + /// Truncates the segment to the specified width. + /// + /// The render context. + /// The segment to truncate. + /// The maximum width that the segment may occupy. + /// A new truncated segment, or null. + public static Segment? Truncate(RenderContext context, Segment segment, int maxWidth) + { + if (segment is null) + { + return null; + } + + if (segment.CellLength(context) <= maxWidth) + { + return segment; + } + + var builder = new StringBuilder(); + foreach (var character in segment.Text) + { + if (Cell.GetCellLength(context, builder.ToString()) >= maxWidth) + { + break; + } + + builder.Append(character); + } + + if (builder.Length == 0) + { + return null; + } + + return new Segment(builder.ToString(), segment.Style); + } + internal static Segment TruncateWithEllipsis(string text, Style style, RenderContext context, int maxWidth) { return SplitOverflow( @@ -396,6 +445,46 @@ namespace Spectre.Console.Rendering maxWidth)[0]; } + internal static List TruncateWithEllipsis(IEnumerable segments, RenderContext context, int maxWidth) + { + if (CellLength(context, segments) <= maxWidth) + { + return new List(segments); + } + + segments = TrimEnd(Truncate(context, segments, maxWidth - 1)); + if (!segments.Any()) + { + return new List(1); + } + + var result = new List(segments); + result.Add(new Segment("…", result.Last().Style)); + return result; + } + + internal static List TrimEnd(IEnumerable segments) + { + var stack = new Stack(); + var checkForWhitespace = true; + foreach (var segment in segments.Reverse()) + { + if (checkForWhitespace) + { + if (segment.IsWhiteSpace) + { + continue; + } + + checkForWhitespace = false; + } + + stack.Push(segment); + } + + return stack.ToList(); + } + internal static List> MakeSameHeight(int cellHeight, List> cells) { foreach (var cell in cells) diff --git a/src/Spectre.Console/Widgets/Columns.cs b/src/Spectre.Console/Widgets/Columns.cs index 0356428..ff1e60f 100644 --- a/src/Spectre.Console/Widgets/Columns.cs +++ b/src/Spectre.Console/Widgets/Columns.cs @@ -66,10 +66,15 @@ namespace Spectre.Console var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray(); var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding); + if (columnCount == 0) + { + // Temporary work around for extremely small consoles + return new Measurement(maxWidth, maxWidth); + } - var rows = _items.Count / columnCount; + var rows = _items.Count / Math.Max(columnCount, 1); var greatestWidth = 0; - for (var row = 0; row < rows; row += columnCount) + for (var row = 0; row < rows; row += Math.Max(1, columnCount)) { var widths = itemWidths.Skip(row * columnCount).Take(columnCount).ToList(); var totalWidth = widths.Sum() + (maxPadding * (widths.Count - 1)); @@ -89,6 +94,11 @@ namespace Spectre.Console var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray(); var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding); + if (columnCount == 0) + { + // Temporary work around for extremely small consoles + columnCount = 1; + } var table = new Table(); table.NoBorder(); diff --git a/src/Spectre.Console/Widgets/Paragraph.cs b/src/Spectre.Console/Widgets/Paragraph.cs index f8efbbd..b9bd8e7 100644 --- a/src/Spectre.Console/Widgets/Paragraph.cs +++ b/src/Spectre.Console/Widgets/Paragraph.cs @@ -138,7 +138,9 @@ namespace Spectre.Console return Array.Empty(); } - var lines = SplitLines(context, maxWidth); + var lines = context.SingleLine + ? new List(_lines) + : SplitLines(context, maxWidth); // Justify lines var justification = context.Justification ?? Alignment ?? Justify.Left; @@ -170,6 +172,11 @@ namespace Spectre.Console } } + if (context.SingleLine) + { + return lines.First().Where(segment => !segment.IsLineBreak); + } + return new SegmentLineEnumerator(lines); } diff --git a/src/Spectre.Console/Widgets/Rule.cs b/src/Spectre.Console/Widgets/Rule.cs new file mode 100644 index 0000000..1ac04d3 --- /dev/null +++ b/src/Spectre.Console/Widgets/Rule.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Spectre.Console.Internal; +using Spectre.Console.Rendering; + +namespace Spectre.Console +{ + /// + /// A renderable horizontal rule. + /// + public sealed class Rule : Renderable, IAlignable + { + /// + /// Gets or sets the rule title markup text. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the rule style. + /// + public Style? Style { get; set; } + + /// + /// Gets or sets the rule's title alignment. + /// + public Justify? Alignment { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public Rule() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The rule title markup text. + public Rule(string title) + { + Title = title ?? throw new ArgumentNullException(nameof(title)); + } + + /// + protected override IEnumerable Render(RenderContext context, int maxWidth) + { + if (Title == null || maxWidth <= 6) + { + return GetLineWithoutTitle(maxWidth); + } + + // Get the title and make sure it fits. + var title = GetTitleSegments(context, Title, maxWidth - 6); + if (Segment.CellLength(context, title) > maxWidth - 6) + { + // Truncate the title + title = Segment.TruncateWithEllipsis(title, context, maxWidth - 6); + if (!title.Any()) + { + // We couldn't fit the title at all. + return GetLineWithoutTitle(maxWidth); + } + } + + var (left, right) = GetLineSegments(context, maxWidth, title); + + var segments = new List(); + segments.Add(left); + segments.AddRange(title); + segments.Add(right); + segments.Add(Segment.LineBreak); + + return segments; + } + + private IEnumerable GetLineWithoutTitle(int maxWidth) + { + var text = new string('─', maxWidth); + return new[] + { + new Segment(text, Style ?? Style.Plain), + Segment.LineBreak, + }; + } + + private (Segment Left, Segment Right) GetLineSegments(RenderContext context, int maxWidth, IEnumerable title) + { + var alignment = Alignment ?? Justify.Center; + + var titleLength = Segment.CellLength(context, title); + + if (alignment == Justify.Left) + { + var left = new Segment(new string('─', 2) + " ", Style ?? Style.Plain); + + var rightLength = maxWidth - titleLength - left.CellLength(context) - 1; + var right = new Segment(" " + new string('─', rightLength), Style ?? Style.Plain); + + return (left, right); + } + else if (alignment == Justify.Center) + { + var leftLength = ((maxWidth - titleLength) / 2) - 1; + var left = new Segment(new string('─', leftLength) + " ", Style ?? Style.Plain); + + var rightLength = maxWidth - titleLength - left.CellLength(context) - 1; + var right = new Segment(" " + new string('─', rightLength), Style ?? Style.Plain); + + return (left, right); + } + else if (alignment == Justify.Right) + { + var right = new Segment(" " + new string('─', 2), Style ?? Style.Plain); + + var leftLength = maxWidth - titleLength - right.CellLength(context) - 1; + var left = new Segment(new string('─', leftLength) + " ", Style ?? Style.Plain); + + return (left, right); + } + + throw new NotSupportedException("Unsupported alignment."); + } + + private IEnumerable GetTitleSegments(RenderContext context, string title, int width) + { + title = title.NormalizeLineEndings().Replace("\n", " ").Trim(); + var markup = new Markup(title, Style); + return ((IRenderable)markup).Render(context.WithSingleLine(), width - 6); + } + } +}