Add panel header support

Closes #63
This commit is contained in:
Patrik Svensson 2020-09-03 18:48:30 +02:00 committed by Patrik Svensson
parent 9f8ca6d648
commit 7471e9d38c
11 changed files with 301 additions and 27 deletions

View File

@ -4,7 +4,7 @@ namespace ColorExample
{
public static class Program
{
public static void Main(string[] args)
public static void Main()
{
if (AnsiConsole.Capabilities.ColorSystem == ColorSystem.NoColors)
{

View File

@ -3,9 +3,9 @@ using Spectre.Console;
namespace Diagnostic
{
public class Program
public static class Program
{
public static void Main(string[] args)
public static void Main()
{
AnsiConsole.MarkupLine("Color system: [bold]{0}[/]", AnsiConsole.Capabilities.ColorSystem);
AnsiConsole.MarkupLine("Supports ansi? [bold]{0}[/]", AnsiConsole.Capabilities.SupportsAnsi);

View File

@ -2,9 +2,9 @@ using Spectre.Console;
namespace GridExample
{
public sealed class Program
public static class Program
{
static void Main(string[] args)
public static void Main()
{
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("Usage: [grey]dotnet [blue]run[/] [[options]] [[[[--]] <additional arguments>...]]]][/]");

View File

@ -2,9 +2,9 @@ using Spectre.Console;
namespace PanelExample
{
class Program
public static class Program
{
static void Main(string[] args)
public static void Main()
{
var content = new Markup(
"[underline]I[/] heard [underline on blue]you[/] like panels\n\n\n\n" +
@ -14,7 +14,7 @@ namespace PanelExample
new Panel(
new Panel(content)
{
Border = BorderKind.Rounded
Border = BorderKind.Rounded,
}));
// Left adjusted panel with text
@ -22,6 +22,7 @@ namespace PanelExample
new Text("Left adjusted\nLeft").LeftAligned())
{
Expand = true,
Header = new Header("Left", new Style(foreground: Color.Red)).LeftAligned(),
});
// Centered ASCII panel with text
@ -30,6 +31,7 @@ namespace PanelExample
{
Expand = true,
Border = BorderKind.Ascii,
Header = new Header("Center", new Style(foreground: Color.Green)).Centered(),
});
// Right adjusted, rounded panel with text
@ -38,6 +40,7 @@ namespace PanelExample
{
Expand = true,
Border = BorderKind.Rounded,
Header = new Header("Right", new Style(foreground: Color.Blue)).RightAligned(),
});
}
}

View File

@ -5,7 +5,7 @@ namespace TableExample
{
public static class Program
{
public static void Main(string[] args)
public static void Main()
{
// A simple table
RenderSimpleTable();

View File

@ -40,6 +40,108 @@ namespace Spectre.Console.Tests.Unit
console.Lines[2].ShouldBe("└───────────────────┘");
}
[Fact]
public void Should_Render_Panel_With_Header()
{
// Given
var console = new PlainConsole(width: 80);
// When
console.Render(new Panel("Hello World")
{
Header = new Header("Greeting"),
Expand = true,
Padding = new Padding(2, 2),
});
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("┌─Greeting─────────────────────────────────────────────────────────────────────┐");
console.Lines[1].ShouldBe("│ Hello World │");
console.Lines[2].ShouldBe("└──────────────────────────────────────────────────────────────────────────────┘");
}
[Fact]
public void Should_Render_Panel_With_Left_Aligned_Header()
{
// Given
var console = new PlainConsole(width: 80);
// When
console.Render(new Panel("Hello World")
{
Header = new Header("Greeting").LeftAligned(),
Expand = true,
});
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("┌─Greeting─────────────────────────────────────────────────────────────────────┐");
console.Lines[1].ShouldBe("│ Hello World │");
console.Lines[2].ShouldBe("└──────────────────────────────────────────────────────────────────────────────┘");
}
[Fact]
public void Should_Render_Panel_With_Centered_Header()
{
// Given
var console = new PlainConsole(width: 80);
// When
console.Render(new Panel("Hello World")
{
Header = new Header("Greeting").Centered(),
Expand = true,
});
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("┌───────────────────────────────────Greeting───────────────────────────────────┐");
console.Lines[1].ShouldBe("│ Hello World │");
console.Lines[2].ShouldBe("└──────────────────────────────────────────────────────────────────────────────┘");
}
[Fact]
public void Should_Render_Panel_With_Right_Aligned_Header()
{
// Given
var console = new PlainConsole(width: 80);
// When
console.Render(new Panel("Hello World")
{
Header = new Header("Greeting").RightAligned(),
Expand = true,
});
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("┌─────────────────────────────────────────────────────────────────────Greeting─┐");
console.Lines[1].ShouldBe("│ Hello World │");
console.Lines[2].ShouldBe("└──────────────────────────────────────────────────────────────────────────────┘");
}
[Fact]
public void Should_Collapse_Header_If_It_Will_Not_Fit()
{
// Given
var console = new PlainConsole(width: 10);
// When
console.Render(new Panel("Hello World")
{
Header = new Header("Greeting"),
Expand = true,
});
// Then
console.Lines.Count.ShouldBe(4);
console.Lines[0].ShouldBe("┌─Greet…─┐");
console.Lines[1].ShouldBe("│ Hello │");
console.Lines[2].ShouldBe("│ World │");
console.Lines[3].ShouldBe("└────────┘");
}
[Fact]
public void Should_Render_Panel_With_Unicode_Correctly()
{

View File

@ -0,0 +1,60 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Represents a header.
/// </summary>
public sealed class Header : IAlignable
{
/// <summary>
/// Gets the header text.
/// </summary>
public string Text { get; }
/// <summary>
/// Gets or sets the header style.
/// </summary>
public Style? Style { get; set; }
/// <summary>
/// Gets or sets the header alignment.
/// </summary>
public Justify? Alignment { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Header"/> class.
/// </summary>
/// <param name="text">The header text.</param>
/// <param name="style">The header style.</param>
/// <param name="alignment">The header alignment.</param>
public Header(string text, Style? style = null, Justify? alignment = null)
{
Text = text ?? throw new ArgumentNullException(nameof(text));
Style = style;
Alignment = alignment;
}
/// <summary>
/// Sets the header style.
/// </summary>
/// <param name="style">The header style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Header SetStyle(Style? style)
{
Style = style ?? Style.Plain;
return this;
}
/// <summary>
/// Sets the header alignment.
/// </summary>
/// <param name="alignment">The header alignment.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Header SetAlignment(Justify alignment)
{
Alignment = alignment;
return this;
}
}
}

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
@ -35,6 +36,11 @@ namespace Spectre.Console
/// </summary>
public Padding Padding { get; set; } = new Padding(1, 1);
/// <summary>
/// Gets or sets the header.
/// </summary>
public Header? Header { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Panel"/> class.
/// </summary>
@ -77,21 +83,16 @@ namespace Spectre.Console
childWidth = measurement.Max;
}
var panelWidth = childWidth + paddingWidth;
var panelWidth = childWidth + EdgeWidth + paddingWidth;
panelWidth = Math.Min(panelWidth, maxWidth);
var result = new List<Segment>();
// Panel top
var result = new List<Segment>
{
new Segment(border.GetPart(BorderPart.HeaderTopLeft), borderStyle),
new Segment(border.GetPart(BorderPart.HeaderTop, panelWidth), borderStyle),
new Segment(border.GetPart(BorderPart.HeaderTopRight), borderStyle),
Segment.LineBreak,
};
// Render the child.
var childSegments = _child.Render(context, childWidth);
AddTopBorder(result, context, border, borderStyle, panelWidth);
// Split the child segments into lines.
var childSegments = _child.Render(context, childWidth);
foreach (var line in Segment.SplitLines(childSegments, panelWidth))
{
result.Add(new Segment(border.GetPart(BorderPart.CellLeft), borderStyle));
@ -126,12 +127,62 @@ namespace Spectre.Console
}
// Panel bottom
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft), borderStyle));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, panelWidth), borderStyle));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight), borderStyle));
result.Add(Segment.LineBreak);
AddBottomBorder(result, border, borderStyle, panelWidth);
return result;
}
private static void AddBottomBorder(List<Segment> result, SpectreBorder border, Style borderStyle, int panelWidth)
{
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft), borderStyle));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, panelWidth - EdgeWidth), borderStyle));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight), borderStyle));
result.Add(Segment.LineBreak);
}
private void AddTopBorder(List<Segment> segments, RenderContext context, SpectreBorder border, Style borderStyle, int panelWidth)
{
segments.Add(new Segment(border.GetPart(BorderPart.HeaderTopLeft), borderStyle));
if (Header != null)
{
var leftSpacing = 0;
var rightSpacing = 0;
var headerWidth = panelWidth - (EdgeWidth * 2);
var header = Segment.TruncateWithEllipsis(Header.Text, Header.Style ?? borderStyle, context.Encoding, headerWidth);
var excessWidth = headerWidth - header.CellLength(context.Encoding);
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;
}
}
segments.Add(new Segment(border.GetPart(BorderPart.HeaderTop, leftSpacing + 1), borderStyle));
segments.Add(header);
segments.Add(new Segment(border.GetPart(BorderPart.HeaderTop, rightSpacing + 1), borderStyle));
}
else
{
segments.Add(new Segment(border.GetPart(BorderPart.HeaderTop, panelWidth - EdgeWidth), borderStyle));
}
segments.Add(new Segment(border.GetPart(BorderPart.HeaderTopRight), borderStyle));
segments.Add(Segment.LineBreak);
}
}
}

View File

@ -0,0 +1,50 @@
using System;
namespace Spectre.Console.Rendering
{
/// <summary>
/// Contains extension methods for <see cref="Panel"/>.
/// </summary>
public static class PanelExtensions
{
/// <summary>
/// Sets the panel header.
/// </summary>
/// <param name="panel">The panel.</param>
/// <param name="text">The header text.</param>
/// <param name="style">The header style.</param>
/// <param name="alignment">The header alignment.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Panel SetHeader(this Panel panel, string text, Style? style = null, Justify? alignment = null)
{
if (panel is null)
{
throw new ArgumentNullException(nameof(panel));
}
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
return SetHeader(panel, new Header(text, style, alignment));
}
/// <summary>
/// Sets the panel header.
/// </summary>
/// <param name="panel">The panel.</param>
/// <param name="header">The header to use.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Panel SetHeader(this Panel panel, Header header)
{
if (panel is null)
{
throw new ArgumentNullException(nameof(panel));
}
panel.Header = header;
return panel;
}
}
}

View File

@ -122,7 +122,7 @@ namespace Spectre.Console
var min = _lines.Max(line => line.Max(segment => segment.CellLength(context.Encoding)));
var max = _lines.Max(x => x.CellWidth(context.Encoding));
return new Measurement(min, max);
return new Measurement(min, Math.Min(max, maxWidth));
}
/// <inheritdoc/>

View File

@ -305,13 +305,21 @@ namespace Spectre.Console.Rendering
}
else if (overflow == Overflow.Ellipsis)
{
result.Add(new Segment(segment.Text.Substring(0, width - 1), segment.Style));
result.Add(new Segment("…", segment.Style));
result.Add(new Segment(segment.Text.Substring(0, width - 1) + "…", segment.Style));
}
return result;
}
internal static Segment TruncateWithEllipsis(string text, Style style, Encoding encoding, int maxWidth)
{
return SplitOverflow(
new Segment(text, style),
Overflow.Ellipsis,
encoding,
maxWidth).First();
}
internal static List<List<SegmentLine>> MakeSameHeight(int cellHeight, List<List<SegmentLine>> cells)
{
foreach (var cell in cells)