diff --git a/examples/Borders/Program.cs b/examples/Borders/Program.cs index dd8412d..2854107 100644 --- a/examples/Borders/Program.cs +++ b/examples/Borders/Program.cs @@ -21,7 +21,7 @@ namespace BordersExample static IRenderable CreatePanel(string name, BoxBorder border) { return new Panel($"This is a panel with\nthe [yellow]{name}[/] border.") - .Header($" {name} ", Style.Parse("blue"), Justify.Center) + .Header($" [blue]{name}[/] ", Justify.Center) .Border(border) .BorderStyle(Style.Parse("grey")); } @@ -53,7 +53,7 @@ namespace BordersExample table.AddRow("Cell", "Cell"); return new Panel(table) - .Header($" {name} ", Style.Parse("blue"), Justify.Center) + .Header($" [blue]{name}[/] ", Justify.Center) .NoBorder(); } diff --git a/examples/Cursor/Program.cs b/examples/Cursor/Program.cs index ebfdfbf..e2645c2 100644 --- a/examples/Cursor/Program.cs +++ b/examples/Cursor/Program.cs @@ -1,4 +1,3 @@ -using System; using Spectre.Console; namespace Cursor diff --git a/examples/Panels/Program.cs b/examples/Panels/Program.cs index de8e2fa..d43c110 100644 --- a/examples/Panels/Program.cs +++ b/examples/Panels/Program.cs @@ -20,16 +20,14 @@ namespace PanelExample new Panel(new Text("Left adjusted\nLeft").LeftAligned()) .Expand() .SquareBorder() - .Header("Left") - .HeaderStyle("red")); + .Header("[red]Left[/]")); // Centered ASCII panel with text AnsiConsole.Render( new Panel(new Text("Centered\nCenter").Centered()) .Expand() .AsciiBorder() - .Header("Center") - .HeaderStyle("green") + .Header("[green]Center[/]") .HeaderAlignment(Justify.Center)); // Right adjusted, rounded panel with text @@ -37,8 +35,7 @@ namespace PanelExample new Panel(new Text("Right adjusted\nRight").RightAligned()) .Expand() .RoundedBorder() - .Header("Right") - .HeaderStyle("blue") + .Header("[blue]Right[/]") .HeaderAlignment(Justify.Right)); } } diff --git a/examples/Rules/Program.cs b/examples/Rules/Program.cs index 438a717..e65ac97 100644 --- a/examples/Rules/Program.cs +++ b/examples/Rules/Program.cs @@ -10,18 +10,21 @@ namespace EmojiExample WrapInPanel( new Rule() .RuleStyle(Style.Parse("yellow")) + .AsciiBorder() .LeftAligned()); // Left aligned title WrapInPanel( new Rule("[blue]Left aligned[/]") .RuleStyle(Style.Parse("red")) + .DoubleBorder() .LeftAligned()); // Centered title WrapInPanel( new Rule("[green]Centered[/]") .RuleStyle(Style.Parse("green")) + .HeavyBorder() .Centered()); // Right aligned title diff --git a/src/Spectre.Console.Tests/Unit/PanelTests.cs b/src/Spectre.Console.Tests/Unit/PanelTests.cs index 354229a..1faa679 100644 --- a/src/Spectre.Console.Tests/Unit/PanelTests.cs +++ b/src/Spectre.Console.Tests/Unit/PanelTests.cs @@ -316,14 +316,14 @@ namespace Spectre.Console.Tests.Unit var panel = new Panel(grid) .Expand().RoundedBorder() .BorderStyle(new Style().Foreground(Color.Grey)) - .Header("Short paths ", new Style().Foreground(Color.Grey)); + .Header("[grey]Short paths[/]"); // When console.Render(panel); // Then console.Lines.Count.ShouldBe(4); - console.Lines[0].ShouldBe("╭─Short paths ─────────────────────────────────────────────────────────────────────╮"); + console.Lines[0].ShouldBe("╭─Short paths──────────────────────────────────────────────────────────────────────╮"); console.Lines[1].ShouldBe("│ at System.Runtime.CompilerServices.TaskAwaiter. │"); console.Lines[2].ShouldBe("│ HandleNonSuccessAndDebuggerNotification(Task task) │"); console.Lines[3].ShouldBe("╰──────────────────────────────────────────────────────────────────────────────────╯"); diff --git a/src/Spectre.Console.Tests/Unit/RuleTests.cs b/src/Spectre.Console.Tests/Unit/RuleTests.cs index 3ebb653..a39113b 100644 --- a/src/Spectre.Console.Tests/Unit/RuleTests.cs +++ b/src/Spectre.Console.Tests/Unit/RuleTests.cs @@ -19,6 +19,34 @@ namespace Spectre.Console.Tests.Unit console.Lines[0].ShouldBe("────────────────────────────────────────"); } + [Fact] + public void Should_Render_Default_Rule_With_Specified_Box() + { + // Given + var console = new PlainConsole(width: 40); + + // When + console.Render(new Rule().DoubleBorder()); + + // Then + console.Lines.Count.ShouldBe(1); + console.Lines[0].ShouldBe("════════════════════════════════════════"); + } + + [Fact] + public void Should_Render_With_Specified_Box() + { + // Given + var console = new PlainConsole(width: 40); + + // When + console.Render(new Rule("Hello World").DoubleBorder()); + + // Then + console.Lines.Count.ShouldBe(1); + console.Lines[0].ShouldBe("═════════════ Hello World ══════════════"); + } + [Fact] public void Should_Render_Default_Rule_With_Title_Centered_By_Default() { diff --git a/src/Spectre.Console/Extensions/Obsolete/ObsoletePanelExtensions.cs b/src/Spectre.Console/Extensions/Obsolete/ObsoletePanelExtensions.cs index f5d2dd4..0af65b2 100644 --- a/src/Spectre.Console/Extensions/Obsolete/ObsoletePanelExtensions.cs +++ b/src/Spectre.Console/Extensions/Obsolete/ObsoletePanelExtensions.cs @@ -30,10 +30,8 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(text)); } - style ??= panel.Header?.Style; alignment ??= panel.Header?.Alignment; - - return SetHeader(panel, new PanelHeader(text, style, alignment)); + return SetHeader(panel, new PanelHeader(text, alignment)); } /// @@ -54,5 +52,18 @@ namespace Spectre.Console panel.Header = header; return panel; } + + /// + /// Sets the panel header style. + /// + /// The panel. + /// The header style. + /// The same instance so that multiple calls can be chained. + [Obsolete("Use markup in header instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static Panel HeaderStyle(this Panel panel, Style style) + { + return panel; + } } } diff --git a/src/Spectre.Console/Extensions/PanelExtensions.cs b/src/Spectre.Console/Extensions/PanelExtensions.cs index a817b5c..e1901dc 100644 --- a/src/Spectre.Console/Extensions/PanelExtensions.cs +++ b/src/Spectre.Console/Extensions/PanelExtensions.cs @@ -12,10 +12,9 @@ namespace Spectre.Console /// /// The panel. /// The header text. - /// The header style. /// The header alignment. /// The same instance so that multiple calls can be chained. - public static Panel Header(this Panel panel, string text, Style? style = null, Justify? alignment = null) + public static Panel Header(this Panel panel, string text, Justify? alignment = null) { if (panel is null) { @@ -27,42 +26,8 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(text)); } - style ??= panel.Header?.Style; alignment ??= panel.Header?.Alignment; - - return Header(panel, new PanelHeader(text, style, alignment)); - } - - /// - /// Sets the panel header style. - /// - /// The panel. - /// The header style. - /// The same instance so that multiple calls can be chained. - public static Panel HeaderStyle(this Panel panel, Style style) - { - if (panel is null) - { - throw new ArgumentNullException(nameof(panel)); - } - - if (style is null) - { - throw new ArgumentNullException(nameof(style)); - } - - if (panel.Header != null) - { - // Update existing style - panel.Header.Style = style; - } - else - { - // Create header - Header(panel, string.Empty, style, null); - } - - return panel; + return Header(panel, new PanelHeader(text, alignment)); } /// @@ -86,7 +51,7 @@ namespace Spectre.Console else { // Create header - Header(panel, string.Empty, null, alignment); + Header(panel, string.Empty, alignment); } return panel; diff --git a/src/Spectre.Console/IHasBoxBorder.cs b/src/Spectre.Console/IHasBoxBorder.cs index bb5f97b..70daa98 100644 --- a/src/Spectre.Console/IHasBoxBorder.cs +++ b/src/Spectre.Console/IHasBoxBorder.cs @@ -3,7 +3,7 @@ namespace Spectre.Console /// /// Represents something that has a box border. /// - public interface IHasBoxBorder : IHasBorder + public interface IHasBoxBorder { /// /// Gets or sets the box. diff --git a/src/Spectre.Console/Widgets/Panel.cs b/src/Spectre.Console/Widgets/Panel.cs index e429176..483d1b6 100644 --- a/src/Spectre.Console/Widgets/Panel.cs +++ b/src/Spectre.Console/Widgets/Panel.cs @@ -9,7 +9,7 @@ namespace Spectre.Console /// /// A renderable panel. /// - public sealed class Panel : Renderable, IHasBoxBorder, IExpandable, IPaddable + public sealed class Panel : Renderable, IHasBoxBorder, IHasBorder, IExpandable, IPaddable { private const int EdgeWidth = 2; @@ -123,62 +123,35 @@ namespace Spectre.Console } // Panel bottom - AddBottomBorder(result, border, borderStyle, panelWidth); - - return result; - } - - private static void AddBottomBorder(List result, BoxBorder border, Style borderStyle, int panelWidth) - { result.Add(new Segment(border.GetPart(BoxBorderPart.BottomLeft), borderStyle)); result.Add(new Segment(border.GetPart(BoxBorderPart.Bottom).Repeat(panelWidth - EdgeWidth), borderStyle)); result.Add(new Segment(border.GetPart(BoxBorderPart.BottomRight), borderStyle)); result.Add(Segment.LineBreak); + + return result; } - private void AddTopBorder(List segments, RenderContext context, BoxBorder border, Style borderStyle, int panelWidth) + private void AddTopBorder(List result, RenderContext context, BoxBorder border, Style borderStyle, int panelWidth) { - segments.Add(new Segment(border.GetPart(BoxBorderPart.TopLeft), borderStyle)); - - if (Header != null) + var rule = new Rule { - var leftSpacing = 0; - var rightSpacing = 0; + Style = borderStyle, + Border = border, + TitlePadding = 1, + TitleSpacing = 0, + Title = Header?.Text, + Alignment = Header?.Alignment ?? Justify.Left, + }; - var headerWidth = panelWidth - (EdgeWidth * 2); - var header = Segment.TruncateWithEllipsis(Header.Text, Header.Style ?? borderStyle, context, headerWidth); + // Top left border + result.Add(new Segment(border.GetPart(BoxBorderPart.TopLeft), borderStyle)); - var excessWidth = headerWidth - header.CellCount(context); - if (excessWidth > 0) - { - switch (Header.Alignment ?? Justify.Left) - { - case Justify.Left: - leftSpacing = 0; - rightSpacing = excessWidth; - break; - case Justify.Right: - leftSpacing = excessWidth; - rightSpacing = 0; - break; - case Justify.Center: - leftSpacing = excessWidth / 2; - rightSpacing = (excessWidth / 2) + (excessWidth % 2); - break; - } - } + // Top border (and header text if specified) + result.AddRange(((IRenderable)rule).Render(context, panelWidth - 2).Where(x => !x.IsLineBreak)); - segments.Add(new Segment(border.GetPart(BoxBorderPart.Top).Repeat(leftSpacing + 1), borderStyle)); - segments.Add(header); - segments.Add(new Segment(border.GetPart(BoxBorderPart.Top).Repeat(rightSpacing + 1), borderStyle)); - } - else - { - segments.Add(new Segment(border.GetPart(BoxBorderPart.Top).Repeat(panelWidth - EdgeWidth), borderStyle)); - } - - segments.Add(new Segment(border.GetPart(BoxBorderPart.TopRight), borderStyle)); - segments.Add(Segment.LineBreak); + // Top right border + result.Add(new Segment(border.GetPart(BoxBorderPart.TopRight), borderStyle)); + result.Add(Segment.LineBreak); } } } diff --git a/src/Spectre.Console/Widgets/PanelHeader.cs b/src/Spectre.Console/Widgets/PanelHeader.cs index 0c327f8..4c609bb 100644 --- a/src/Spectre.Console/Widgets/PanelHeader.cs +++ b/src/Spectre.Console/Widgets/PanelHeader.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; namespace Spectre.Console { @@ -12,11 +13,6 @@ namespace Spectre.Console /// public string Text { get; } - /// - /// Gets or sets the panel header style. - /// - public Style? Style { get; set; } - /// /// Gets or sets the panel header alignment. /// @@ -26,12 +22,10 @@ namespace Spectre.Console /// Initializes a new instance of the class. /// /// The panel header text. - /// The panel header style. /// The panel header alignment. - public PanelHeader(string text, Style? style = null, Justify? alignment = null) + public PanelHeader(string text, Justify? alignment = null) { Text = text ?? throw new ArgumentNullException(nameof(text)); - Style = style; Alignment = alignment; } @@ -40,9 +34,10 @@ namespace Spectre.Console /// /// The panel header style. /// The same instance so that multiple calls can be chained. + [Obsolete("Use markup instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] public PanelHeader SetStyle(Style? style) { - Style = style ?? Style.Plain; return this; } @@ -51,14 +46,10 @@ namespace Spectre.Console /// /// The panel header style. /// The same instance so that multiple calls can be chained. + [Obsolete("Use markup instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] public PanelHeader SetStyle(string style) { - if (style is null) - { - throw new ArgumentNullException(nameof(style)); - } - - Style = Style.Parse(style); return this; } diff --git a/src/Spectre.Console/Widgets/Rule.cs b/src/Spectre.Console/Widgets/Rule.cs index 168cbb0..ead2acc 100644 --- a/src/Spectre.Console/Widgets/Rule.cs +++ b/src/Spectre.Console/Widgets/Rule.cs @@ -9,7 +9,7 @@ namespace Spectre.Console /// /// A renderable horizontal rule. /// - public sealed class Rule : Renderable, IAlignable + public sealed class Rule : Renderable, IAlignable, IHasBoxBorder { /// /// Gets or sets the rule title markup text. @@ -26,6 +26,12 @@ namespace Spectre.Console /// public Justify? Alignment { get; set; } + /// + public BoxBorder Border { get; set; } = BoxBorder.Square; + + internal int TitlePadding { get; set; } = 2; + internal int TitleSpacing { get; set; } = 1; + /// /// Initializes a new instance of the class. /// @@ -45,21 +51,23 @@ namespace Spectre.Console /// protected override IEnumerable Render(RenderContext context, int maxWidth) { - if (Title == null || maxWidth <= 6) + var extraLength = (2 * TitlePadding) + (2 * TitleSpacing); + + if (Title == null || maxWidth <= extraLength) { - return GetLineWithoutTitle(maxWidth); + return GetLineWithoutTitle(context, maxWidth); } // Get the title and make sure it fits. - var title = GetTitleSegments(context, Title, maxWidth - 6); - if (Segment.CellCount(context, title) > maxWidth - 6) + var title = GetTitleSegments(context, Title, maxWidth - extraLength); + if (Segment.CellCount(context, title) > maxWidth - extraLength) { // Truncate the title - title = Segment.TruncateWithEllipsis(title, context, maxWidth - 6); + title = Segment.TruncateWithEllipsis(title, context, maxWidth - extraLength); if (!title.Any()) { // We couldn't fit the title at all. - return GetLineWithoutTitle(maxWidth); + return GetLineWithoutTitle(context, maxWidth); } } @@ -74,9 +82,11 @@ namespace Spectre.Console return segments; } - private IEnumerable GetLineWithoutTitle(int maxWidth) + private IEnumerable GetLineWithoutTitle(RenderContext context, int maxWidth) { - var text = new string('─', maxWidth); + var border = Border.GetSafeBorder(context.LegacyConsole || !context.Unicode); + var text = border.GetPart(BoxBorderPart.Top).Repeat(maxWidth); + return new[] { new Segment(text, Style ?? Style.Plain), @@ -84,49 +94,51 @@ namespace Spectre.Console }; } - private (Segment Left, Segment Right) GetLineSegments(RenderContext context, int maxWidth, IEnumerable title) + private IEnumerable GetTitleSegments(RenderContext context, string title, int width) { - var alignment = Alignment ?? Justify.Center; + title = title.NormalizeLineEndings().Replace("\n", " ").Trim(); + var markup = new Markup(title, Style); + return ((IRenderable)markup).Render(context.WithSingleLine(), width); + } + private (Segment Left, Segment Right) GetLineSegments(RenderContext context, int width, IEnumerable title) + { var titleLength = Segment.CellCount(context, title); + var border = Border.GetSafeBorder(context.LegacyConsole || !context.Unicode); + var borderPart = border.GetPart(BoxBorderPart.Top); + + var alignment = Alignment ?? Justify.Center; if (alignment == Justify.Left) { - var left = new Segment(new string('─', 2) + " ", Style ?? Style.Plain); + var left = new Segment(borderPart.Repeat(TitlePadding) + new string(' ', TitleSpacing), Style ?? Style.Plain); - var rightLength = maxWidth - titleLength - left.CellCount(context) - 1; - var right = new Segment(" " + new string('─', rightLength), Style ?? Style.Plain); + var rightLength = width - titleLength - left.CellCount(context) - TitleSpacing; + var right = new Segment(new string(' ', TitleSpacing) + borderPart.Repeat(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 leftLength = ((width - titleLength) / 2) - TitleSpacing; + var left = new Segment(borderPart.Repeat(leftLength) + new string(' ', TitleSpacing), Style ?? Style.Plain); - var rightLength = maxWidth - titleLength - left.CellCount(context) - 1; - var right = new Segment(" " + new string('─', rightLength), Style ?? Style.Plain); + var rightLength = width - titleLength - left.CellCount(context) - TitleSpacing; + var right = new Segment(new string(' ', TitleSpacing) + borderPart.Repeat(rightLength), Style ?? Style.Plain); return (left, right); } else if (alignment == Justify.Right) { - var right = new Segment(" " + new string('─', 2), Style ?? Style.Plain); + var right = new Segment(new string(' ', TitleSpacing) + borderPart.Repeat(TitlePadding), Style ?? Style.Plain); - var leftLength = maxWidth - titleLength - right.CellCount(context) - 1; - var left = new Segment(new string('─', leftLength) + " ", Style ?? Style.Plain); + var leftLength = width - titleLength - right.CellCount(context) - TitleSpacing; + var left = new Segment(borderPart.Repeat(leftLength) + new string(' ', TitleSpacing), 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); - } } }