Add padder widget

This commit adds a padder can be use to pad other IRenderable
objects such as tables, panels, grids, text, etc.
This commit is contained in:
Patrik Svensson 2020-09-14 23:34:02 +02:00 committed by Patrik Svensson
parent 314456ca17
commit 7d6104ace4
13 changed files with 366 additions and 55 deletions

View File

@ -190,9 +190,9 @@ namespace Spectre.Console.Tests.Unit
// Given
var console = new PlainConsole(width: 80);
var grid = new Grid();
grid.AddColumn(new GridColumn { Padding = new Padding(3, 0) });
grid.AddColumn(new GridColumn { Padding = new Padding(0, 0) });
grid.AddColumn(new GridColumn { Padding = new Padding(0, 3) });
grid.AddColumn(new GridColumn { Padding = new Padding(3, 0, 0, 0) });
grid.AddColumn(new GridColumn { Padding = new Padding(0, 0, 0, 0) });
grid.AddColumn(new GridColumn { Padding = new Padding(0, 0, 3, 0) });
grid.AddRow("Foo", "Bar", "Baz");
grid.AddRow("Qux", "Corgi", "Waldo");
grid.AddRow("Grault", "Garply", "Fred");
@ -213,7 +213,7 @@ namespace Spectre.Console.Tests.Unit
var console = new PlainConsole(width: 80);
var grid = new Grid();
grid.AddColumn(new GridColumn { NoWrap = true });
grid.AddColumn(new GridColumn { Padding = new Padding(2, 0) });
grid.AddColumn(new GridColumn { Padding = new Padding(2, 0, 0, 0) });
grid.AddRow("[bold]Options[/]", string.Empty);
grid.AddRow(" [blue]-h[/], [blue]--help[/]", "Show command line help.");
grid.AddRow(" [blue]-c[/], [blue]--configuration[/]", "The configuration to run for.\nThe default for most projects is [green]Debug[/].");

View File

@ -0,0 +1,107 @@
using Shouldly;
using Xunit;
namespace Spectre.Console.Tests.Unit
{
public sealed class PadderTests
{
[Fact]
public void Should_Render_Padded_Object_Correctly()
{
// Given
var console = new PlainConsole(width: 60);
var table = new Table();
table.AddColumn("Foo");
table.AddColumn("Bar");
table.AddRow("Baz", "Qux");
table.AddRow("Corgi", "Waldo");
// When
console.Render(new Padder(table).SetPadding(1, 2, 3, 4));
// Then
console.Lines.Count.ShouldBe(12);
console.Lines[00].ShouldBe(" ");
console.Lines[01].ShouldBe(" ");
console.Lines[02].ShouldBe(" ┌───────┬───────┐ ");
console.Lines[03].ShouldBe(" │ Foo │ Bar │ ");
console.Lines[04].ShouldBe(" ├───────┼───────┤ ");
console.Lines[05].ShouldBe(" │ Baz │ Qux │ ");
console.Lines[06].ShouldBe(" │ Corgi │ Waldo │ ");
console.Lines[07].ShouldBe(" └───────┴───────┘ ");
console.Lines[08].ShouldBe(" ");
console.Lines[09].ShouldBe(" ");
console.Lines[10].ShouldBe(" ");
console.Lines[11].ShouldBe(" ");
}
[Fact]
public void Should_Render_Expanded_Padded_Object_Correctly()
{
// Given
var console = new PlainConsole(width: 60);
var table = new Table();
table.AddColumn("Foo");
table.AddColumn("Bar");
table.AddRow("Baz", "Qux");
table.AddRow("Corgi", "Waldo");
// When
console.Render(new Padder(table)
.SetPadding(1, 2, 3, 4)
.Expand());
// Then
console.Lines.Count.ShouldBe(12);
console.Lines[00].ShouldBe(" ");
console.Lines[01].ShouldBe(" ");
console.Lines[02].ShouldBe(" ┌───────┬───────┐ ");
console.Lines[03].ShouldBe(" │ Foo │ Bar │ ");
console.Lines[04].ShouldBe(" ├───────┼───────┤ ");
console.Lines[05].ShouldBe(" │ Baz │ Qux │ ");
console.Lines[06].ShouldBe(" │ Corgi │ Waldo │ ");
console.Lines[07].ShouldBe(" └───────┴───────┘ ");
console.Lines[08].ShouldBe(" ");
console.Lines[09].ShouldBe(" ");
console.Lines[10].ShouldBe(" ");
console.Lines[11].ShouldBe(" ");
}
[Fact]
public void Should_Render_Padded_Object_Correctly_When_Nested_Within_Other_Object()
{
// Given
var console = new PlainConsole(width: 60);
var table = new Table();
table.AddColumn("Foo");
table.AddColumn("Bar", c => c.PadLeft(0).PadRight(0));
table.AddRow("Baz", "Qux");
table.AddRow(new Text("Corgi"), new Padder(new Panel("Waldo"))
.SetPadding(2, 1, 2, 1));
// When
console.Render(new Padder(table)
.SetPadding(1, 2, 3, 4)
.Expand());
// Then
console.Lines.Count.ShouldBe(16);
console.Lines[00].ShouldBe(" ");
console.Lines[01].ShouldBe(" ");
console.Lines[02].ShouldBe(" ┌───────┬─────────────┐ ");
console.Lines[03].ShouldBe(" │ Foo │Bar │ ");
console.Lines[04].ShouldBe(" ├───────┼─────────────┤ ");
console.Lines[05].ShouldBe(" │ Baz │Qux │ ");
console.Lines[06].ShouldBe(" │ Corgi │ │ ");
console.Lines[07].ShouldBe(" │ │ ┌───────┐ │ ");
console.Lines[08].ShouldBe(" │ │ │ Waldo │ │ ");
console.Lines[09].ShouldBe(" │ │ └───────┘ │ ");
console.Lines[10].ShouldBe(" │ │ │ ");
console.Lines[11].ShouldBe(" └───────┴─────────────┘ ");
console.Lines[12].ShouldBe(" ");
console.Lines[13].ShouldBe(" ");
console.Lines[14].ShouldBe(" ");
console.Lines[15].ShouldBe(" ");
}
}
}

View File

@ -21,6 +21,25 @@ namespace Spectre.Console.Tests.Unit
console.Lines[2].ShouldBe("└─────────────┘");
}
[Fact]
public void Should_Render_Panel_With_Padding_Set_To_Zero()
{
// Given
var console = new PlainConsole(width: 80);
// When
console.Render(new Panel(new Text("Hello World"))
{
Padding = new Padding(0, 0, 0, 0),
});
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("┌───────────┐");
console.Lines[1].ShouldBe("│Hello World│");
console.Lines[2].ShouldBe("└───────────┘");
}
[Fact]
public void Should_Render_Panel_With_Padding()
{
@ -30,14 +49,17 @@ namespace Spectre.Console.Tests.Unit
// When
console.Render(new Panel(new Text("Hello World"))
{
Padding = new Padding(3, 5),
Padding = new Padding(3, 1, 5, 2),
});
// Then
console.Lines.Count.ShouldBe(3);
console.Lines.Count.ShouldBe(6);
console.Lines[0].ShouldBe("┌───────────────────┐");
console.Lines[1].ShouldBe("│ Hello World │");
console.Lines[2].ShouldBe("└───────────────────┘");
console.Lines[1].ShouldBe("│ │");
console.Lines[2].ShouldBe("│ Hello World │");
console.Lines[3].ShouldBe("│ │");
console.Lines[4].ShouldBe("│ │");
console.Lines[5].ShouldBe("└───────────────────┘");
}
[Fact]
@ -51,7 +73,7 @@ namespace Spectre.Console.Tests.Unit
{
Header = new PanelHeader("Greeting"),
Expand = true,
Padding = new Padding(2, 2),
Padding = new Padding(2, 0, 2, 0),
});
// Then

View File

@ -347,7 +347,7 @@ namespace Spectre.Console.Tests.Unit
var console = new PlainConsole(width: 80);
var table = new Table();
table.AddColumns("Foo", "Bar");
table.AddColumn(new TableColumn("Baz") { Padding = new Padding(3, 2) });
table.AddColumn(new TableColumn("Baz") { Padding = new Padding(3, 0, 2, 0) });
table.AddRow("Qux\nQuuux", "Corgi", "Waldo");
table.AddRow("Grault", "Garply", "Fred");
@ -372,7 +372,7 @@ namespace Spectre.Console.Tests.Unit
var console = new PlainConsole(width: 80);
var table = new Table();
table.AddColumns("Foo", "Bar");
table.AddColumn(new TableColumn("Baz") { Padding = new Padding(3, 2) });
table.AddColumn(new TableColumn("Baz") { Padding = new Padding(3, 0, 2, 0) });
// When
console.Render(table);

View File

@ -12,9 +12,9 @@ namespace Spectre.Console
/// </summary>
/// <typeparam name="T">An object implementing <see cref="IPaddable"/>.</typeparam>
/// <param name="obj">The paddable object instance.</param>
/// <param name="padding">The left padding to apply.</param>
/// <param name="left">The left padding.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T PadLeft<T>(this T obj, int padding)
public static T PadLeft<T>(this T obj, int left)
where T : class, IPaddable
{
if (obj is null)
@ -22,7 +22,25 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(obj));
}
return SetPadding(obj, new Padding(padding, obj.Padding.Right));
return SetPadding(obj, new Padding(left, obj.Padding.Top, obj.Padding.Right, obj.Padding.Bottom));
}
/// <summary>
/// Sets the top padding.
/// </summary>
/// <typeparam name="T">An object implementing <see cref="IPaddable"/>.</typeparam>
/// <param name="obj">The paddable object instance.</param>
/// <param name="top">The top padding.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T PadTop<T>(this T obj, int top)
where T : class, IPaddable
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
return SetPadding(obj, new Padding(obj.Padding.Left, top, obj.Padding.Right, obj.Padding.Bottom));
}
/// <summary>
@ -30,9 +48,9 @@ namespace Spectre.Console
/// </summary>
/// <typeparam name="T">An object implementing <see cref="IPaddable"/>.</typeparam>
/// <param name="obj">The paddable object instance.</param>
/// <param name="padding">The right padding to apply.</param>
/// <param name="right">The right padding.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T PadRight<T>(this T obj, int padding)
public static T PadRight<T>(this T obj, int right)
where T : class, IPaddable
{
if (obj is null)
@ -40,7 +58,25 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(obj));
}
return SetPadding(obj, new Padding(obj.Padding.Left, padding));
return SetPadding(obj, new Padding(obj.Padding.Left, obj.Padding.Top, right, obj.Padding.Bottom));
}
/// <summary>
/// Sets the bottom padding.
/// </summary>
/// <typeparam name="T">An object implementing <see cref="IPaddable"/>.</typeparam>
/// <param name="obj">The paddable object instance.</param>
/// <param name="bottom">The bottom padding.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T PadBottom<T>(this T obj, int bottom)
where T : class, IPaddable
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
return SetPadding(obj, new Padding(obj.Padding.Left, obj.Padding.Top, obj.Padding.Right, bottom));
}
/// <summary>
@ -49,12 +85,14 @@ namespace Spectre.Console
/// <typeparam name="T">An object implementing <see cref="IPaddable"/>.</typeparam>
/// <param name="obj">The paddable object instance.</param>
/// <param name="left">The left padding to apply.</param>
/// <param name="top">The top padding to apply.</param>
/// <param name="right">The right padding to apply.</param>
/// <param name="bottom">The bottom padding to apply.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T SetPadding<T>(this T obj, int left, int right)
public static T SetPadding<T>(this T obj, int left, int top, int right, int bottom)
where T : class, IPaddable
{
return SetPadding(obj, new Padding(left, right));
return SetPadding(obj, new Padding(left, top, right, bottom));
}
/// <summary>

View File

@ -3,7 +3,7 @@ using System;
namespace Spectre.Console
{
/// <summary>
/// Represents a measurement.
/// Represents padding.
/// </summary>
public struct Padding : IEquatable<Padding>
{
@ -12,20 +12,53 @@ namespace Spectre.Console
/// </summary>
public int Left { get; }
/// <summary>
/// Gets the top padding.
/// </summary>
public int Top { get; }
/// <summary>
/// Gets the right padding.
/// </summary>
public int Right { get; }
/// <summary>
/// Gets the bottom padding.
/// </summary>
public int Bottom { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Padding"/> struct.
/// </summary>
/// <param name="size">The padding for all sides.</param>
public Padding(int size)
: this(size, size, size, size)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Padding"/> struct.
/// </summary>
/// <param name="horizontal">The left and right padding.</param>
/// <param name="vertical">The top and bottom padding.</param>
public Padding(int horizontal, int vertical)
: this(horizontal, vertical, horizontal, vertical)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Padding"/> struct.
/// </summary>
/// <param name="left">The left padding.</param>
/// <param name="top">The top padding.</param>
/// <param name="right">The right padding.</param>
public Padding(int left, int right)
/// <param name="bottom">The bottom padding.</param>
public Padding(int left, int top, int right, int bottom)
{
Left = left;
Top = top;
Right = right;
Bottom = bottom;
}
/// <inheritdoc/>
@ -41,7 +74,9 @@ namespace Spectre.Console
{
var hash = (int)2166136261;
hash = (hash * 16777619) ^ Left.GetHashCode();
hash = (hash * 16777619) ^ Top.GetHashCode();
hash = (hash * 16777619) ^ Right.GetHashCode();
hash = (hash * 16777619) ^ Bottom.GetHashCode();
return hash;
}
}
@ -49,7 +84,10 @@ namespace Spectre.Console
/// <inheritdoc/>
public bool Equals(Padding other)
{
return Left == other.Left && Right == other.Right;
return Left == other.Left
&& Top == other.Top
&& Right == other.Right
&& Bottom == other.Bottom;
}
/// <summary>
@ -75,12 +113,21 @@ namespace Spectre.Console
}
/// <summary>
/// Gets the horizontal padding.
/// Gets the padding width.
/// </summary>
/// <returns>The horizontal padding.</returns>
public int GetHorizontalPadding()
/// <returns>The padding width.</returns>
public int GetWidth()
{
return Left + Right;
}
/// <summary>
/// Gets the padding height.
/// </summary>
/// <returns>The padding height.</returns>
public int GetHeight()
{
return Top + Bottom;
}
}
}

View File

@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
<AdditionalFiles Include="..\stylecop.json" Link="Properties/stylecop.json" />
<None Include="../../resources/gfx/small-logo.png" Pack="true" PackagePath="\" Link="Properties/small-logo.png" />
</ItemGroup>

View File

@ -13,7 +13,7 @@ namespace Spectre.Console
private readonly List<IRenderable> _items;
/// <inheritdoc/>
public Padding Padding { get; set; } = new Padding(0, 1);
public Padding Padding { get; set; } = new Padding(0, 0, 1, 0);
/// <summary>
/// Gets or sets a value indicating whether or not the columns should

View File

@ -21,6 +21,7 @@ namespace Spectre.Console
/// <summary>
/// Gets or sets the padding of the column.
/// Vertical padding (top and bottom) is ignored.
/// </summary>
public Padding Padding
{
@ -48,7 +49,7 @@ namespace Spectre.Console
/// </summary>
public GridColumn()
{
_padding = new Padding(0, 2);
_padding = new Padding(0, 0, 2, 0);
}
}
}

View File

@ -0,0 +1,106 @@
using System.Collections.Generic;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// Represents padding around a <see cref="IRenderable"/> object.
/// </summary>
public sealed class Padder : Renderable, IPaddable, IExpandable
{
private readonly IRenderable _child;
/// <inheritdoc/>
public Padding Padding { get; set; } = new Padding(1, 1, 1, 1);
/// <summary>
/// Gets or sets a value indicating whether or not the padding should
/// fit the available space. If <c>false</c>, the padding width will be
/// auto calculated. Defaults to <c>false</c>.
/// </summary>
public bool Expand { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Padder"/> class.
/// </summary>
/// <param name="child">The thing to pad.</param>
/// <param name="padding">The padding. Defaults to <c>1,1,1,1</c> if null.</param>
public Padder(IRenderable child, Padding? padding = null)
{
_child = child;
Padding = padding ?? Padding;
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var paddingWidth = Padding.GetWidth();
var measurement = _child.Measure(context, maxWidth - paddingWidth);
return new Measurement(
measurement.Min + paddingWidth,
measurement.Max + paddingWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var paddingWidth = Padding.GetWidth();
var childWidth = maxWidth - paddingWidth;
if (!Expand)
{
var measurement = _child.Measure(context, maxWidth - paddingWidth);
childWidth = measurement.Max;
}
var width = childWidth + paddingWidth;
var result = new List<Segment>();
// Top padding
for (var i = 0; i < Padding.Top; i++)
{
result.Add(new Segment(new string(' ', width)));
result.Add(Segment.LineBreak);
}
var child = _child.Render(context, maxWidth - paddingWidth);
foreach (var (_, _, _, line) in Segment.SplitLines(child).Enumerate())
{
// Left padding
if (Padding.Left != 0)
{
result.Add(new Segment(new string(' ', Padding.Left)));
}
result.AddRange(line);
// Right padding
if (Padding.Right != 0)
{
result.Add(new Segment(new string(' ', Padding.Right)));
}
// Missing space on right side?
var lineWidth = line.CellWidth(context.Encoding);
var diff = width - lineWidth - Padding.Left - Padding.Right;
if (diff > 0)
{
result.Add(new Segment(new string(' ', diff)));
}
result.Add(Segment.LineBreak);
}
// Bottom padding
for (var i = 0; i < Padding.Bottom; i++)
{
result.Add(new Segment(new string(' ', width)));
result.Add(Segment.LineBreak);
}
return result;
}
}
}

View File

@ -33,7 +33,7 @@ namespace Spectre.Console
/// <summary>
/// Gets or sets the padding.
/// </summary>
public Padding Padding { get; set; } = new Padding(1, 1);
public Padding Padding { get; set; } = new Padding(1, 0, 1, 0);
/// <summary>
/// Gets or sets the header.
@ -61,10 +61,11 @@ namespace Spectre.Console
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var childWidth = _child.Measure(context, maxWidth);
var child = new Padder(_child, Padding);
var childWidth = ((IRenderable)child).Measure(context, maxWidth);
return new Measurement(
childWidth.Min + EdgeWidth + Padding.GetHorizontalPadding(),
childWidth.Max + EdgeWidth + Padding.GetHorizontalPadding());
childWidth.Min + EdgeWidth,
childWidth.Max + EdgeWidth);
}
/// <inheritdoc/>
@ -73,16 +74,16 @@ namespace Spectre.Console
var border = Border.GetSafeBorder((context.LegacyConsole || !context.Unicode) && UseSafeBorder);
var borderStyle = BorderStyle ?? Style.Plain;
var paddingWidth = Padding.GetHorizontalPadding();
var childWidth = maxWidth - EdgeWidth - paddingWidth;
var child = new Padder(_child, Padding);
var childWidth = maxWidth - EdgeWidth;
if (!Expand)
{
var measurement = _child.Measure(context, maxWidth - EdgeWidth - paddingWidth);
var measurement = ((IRenderable)child).Measure(context, maxWidth - EdgeWidth);
childWidth = measurement.Max;
}
var panelWidth = childWidth + EdgeWidth + paddingWidth;
var panelWidth = childWidth + EdgeWidth;
panelWidth = Math.Min(panelWidth, maxWidth);
var result = new List<Segment>();
@ -91,17 +92,11 @@ namespace Spectre.Console
AddTopBorder(result, context, border, borderStyle, panelWidth);
// Split the child segments into lines.
var childSegments = _child.Render(context, childWidth);
var childSegments = ((IRenderable)child).Render(context, childWidth);
foreach (var line in Segment.SplitLines(childSegments, panelWidth))
{
result.Add(new Segment(border.GetPart(BorderPart.CellLeft), borderStyle));
// Left padding
if (Padding.Left > 0)
{
result.Add(new Segment(new string(' ', Padding.Left)));
}
var content = new List<Segment>();
content.AddRange(line);
@ -115,12 +110,6 @@ namespace Spectre.Console
result.AddRange(content);
// Right padding
if (Padding.Right > 0)
{
result.Add(new Segment(new string(' ', Padding.Right)));
}
result.Add(new Segment(border.GetPart(BorderPart.CellRight), borderStyle));
result.Add(Segment.LineBreak);
}

View File

@ -221,12 +221,12 @@ namespace Spectre.Console
result.Add(Segment.LineBreak);
}
// Make cells the same shape
cells = Segment.MakeSameHeight(cellHeight, cells);
// Iterate through each cell row
foreach (var cellRowIndex in Enumerable.Range(0, cellHeight))
{
// Make cells the same shape
cells = Segment.MakeSameHeight(cellHeight, cells);
foreach (var (cellIndex, firstCell, lastCell, cell) in cells.Enumerate())
{
if (firstCell && showBorder)
@ -403,7 +403,7 @@ namespace Spectre.Console
private (int Min, int Max) MeasureColumn(TableColumn column, RenderContext options, int maxWidth)
{
var padding = column.Padding.GetHorizontalPadding();
var padding = column.Padding.GetWidth();
// Predetermined width?
if (column.Width != null)
@ -438,7 +438,7 @@ namespace Spectre.Console
var hideBorder = !Border.Visible;
var separators = hideBorder ? 0 : _columns.Count - 1;
var edges = hideBorder ? 0 : EdgeCount;
var padding = includePadding ? _columns.Select(x => x.Padding.GetHorizontalPadding()).Sum() : 0;
var padding = includePadding ? _columns.Select(x => x.Padding.GetWidth()).Sum() : 0;
if (!PadRightCell)
{

View File

@ -21,6 +21,7 @@ namespace Spectre.Console
/// <summary>
/// Gets or sets the padding of the column.
/// Vertical padding (top and bottom) is ignored.
/// </summary>
public Padding Padding { get; set; }
@ -52,7 +53,7 @@ namespace Spectre.Console
{
Text = renderable ?? throw new ArgumentNullException(nameof(renderable));
Width = null;
Padding = new Padding(1, 1);
Padding = new Padding(1, 0, 1, 0);
NoWrap = false;
Alignment = null;
}