Add support for column alignment and padding

Closes #12
Closes #31
This commit is contained in:
Patrik Svensson 2020-08-08 01:52:54 +02:00 committed by Patrik Svensson
parent fa85216554
commit 2dd0eb9f74
16 changed files with 543 additions and 243 deletions

View File

@ -67,31 +67,38 @@ namespace Sample
Text.New( Text.New(
"[underline]I[/] heard [underline on blue]you[/] like 📦\n\n\n\n" + "[underline]I[/] heard [underline on blue]you[/] like 📦\n\n\n\n" +
"So I put a 📦 in a 📦\nin a 📦 in a 📦\n\n" + "So I put a 📦 in a 📦\nin a 📦 in a 📦\n\n" +
"😅", foreground: Color.White), "😅", foreground: Color.White))
content: Justify.Center, { Alignment = Justify.Center, Border = BorderKind.Rounded })))
border: BorderKind.Rounded))), {
border: BorderKind.Ascii)); Border = BorderKind.Ascii
});
// Reset colors // Reset colors
AnsiConsole.ResetColors(); AnsiConsole.ResetColors();
// Left adjusted panel with text // Left adjusted panel with text
AnsiConsole.Render(new Panel( AnsiConsole.Render(new Panel(
Text.New("Left adjusted\nLeft", Text.New("Left adjusted\nLeft"))
foreground: Color.White), {
fit: true)); Expand = true,
Alignment = Justify.Left,
});
// Centered panel with text // Centered panel with text
AnsiConsole.Render(new Panel( AnsiConsole.Render(new Panel(
Text.New("Centered\nCenter", Text.New("Centered\nCenter"))
foreground: Color.White), {
fit: true, content: Justify.Center)); Expand = true,
Alignment = Justify.Center,
});
// Right adjusted panel with text // Right adjusted panel with text
AnsiConsole.Render(new Panel( AnsiConsole.Render(new Panel(
Text.New("Right adjusted\nRight", Text.New("Right adjusted\nRight"))
foreground: Color.White), {
fit: true, content: Justify.Right)); Expand = true,
Alignment = Justify.Right,
});
// A normal, square table // A normal, square table
var table = new Table(); var table = new Table();
@ -145,7 +152,7 @@ namespace Sample
AnsiConsole.Render(table); AnsiConsole.Render(table);
// Render a table in some panels. // Render a table in some panels.
AnsiConsole.Render(new Panel(new Panel(table, border: BorderKind.Ascii))); AnsiConsole.Render(new Panel(new Panel(table) { Border = BorderKind.Ascii }) { Padding = new Padding(0, 0) });
// Draw another table // Draw another table
table = new Table { Expand = false }; table = new Table { Expand = false };

View File

@ -55,7 +55,7 @@ namespace Spectre.Console.Tests.Unit.Composition
} }
[Fact] [Fact]
public void Should_Render_Grid_With_No_Border_Correctly() public void Should_Render_Grid_Correctly()
{ {
// Given // Given
var console = new PlainConsole(width: 80); var console = new PlainConsole(width: 80);
@ -75,13 +75,58 @@ namespace Spectre.Console.Tests.Unit.Composition
console.Lines[1].ShouldBe("Grault Garply Fred "); console.Lines[1].ShouldBe("Grault Garply Fred ");
} }
[Fact]
public void Should_Render_Grid_Column_Alignment_Correctly()
{
// Given
var console = new PlainConsole(width: 80);
var grid = new Grid();
grid.AddColumn(new GridColumn { Alignment = Justify.Right });
grid.AddColumn(new GridColumn { Alignment = Justify.Center });
grid.AddColumn(new GridColumn { Alignment = Justify.Left });
grid.AddRow("Foo", "Bar", "Baz");
grid.AddRow("Qux", "Corgi", "Waldo");
grid.AddRow("Grault", "Garply", "Fred");
// When
console.Render(grid);
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe(" Foo Bar Baz ");
console.Lines[1].ShouldBe(" Qux Corgi Waldo");
console.Lines[2].ShouldBe("Grault Garply Fred ");
}
[Fact]
public void Should_Render_Grid_Column_Padding_Correctly()
{
// Given
var console = new PlainConsole(width: 80);
var grid = new Grid();
grid.AddColumn(new GridColumn { Padding = new Padding(3, 0) });
grid.AddColumns(2);
grid.AddRow("Foo", "Bar", "Baz");
grid.AddRow("Qux", "Corgi", "Waldo");
grid.AddRow("Grault", "Garply", "Fred");
// When
console.Render(grid);
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe(" Foo Bar Baz ");
console.Lines[1].ShouldBe(" Qux Corgi Waldo");
console.Lines[2].ShouldBe(" Grault Garply Fred ");
}
[Fact] [Fact]
public void Should_Render_Grid() public void Should_Render_Grid()
{ {
var console = new PlainConsole(width: 80); var console = new PlainConsole(width: 80);
var grid = new Grid(); var grid = new Grid();
grid.AddColumn(new GridColumn { NoWrap = true }); grid.AddColumn(new GridColumn { NoWrap = true });
grid.AddColumn(); grid.AddColumn(new GridColumn { Padding = new Padding(2, 0) });
grid.AddRow("[bold]Options[/]", string.Empty); grid.AddRow("[bold]Options[/]", string.Empty);
grid.AddRow(" [blue]-h[/], [blue]--help[/]", "Show command line help."); 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[/]."); grid.AddRow(" [blue]-c[/], [blue]--configuration[/]", "The configuration to run for.\nThe default for most projects is [green]Debug[/].");
@ -94,7 +139,7 @@ namespace Spectre.Console.Tests.Unit.Composition
console.Lines[0].ShouldBe("Options "); console.Lines[0].ShouldBe("Options ");
console.Lines[1].ShouldBe(" -h, --help Show command line help. "); console.Lines[1].ShouldBe(" -h, --help Show command line help. ");
console.Lines[2].ShouldBe(" -c, --configuration The configuration to run for. "); console.Lines[2].ShouldBe(" -c, --configuration The configuration to run for. ");
console.Lines[3].ShouldBe(" The default for most projects is Debug. "); console.Lines[3].ShouldBe(" The default for most projects is Debug.");
} }
} }
} }

View File

@ -21,6 +21,25 @@ namespace Spectre.Console.Tests.Unit
console.Lines[2].ShouldBe("└─────────────┘"); console.Lines[2].ShouldBe("└─────────────┘");
} }
[Fact]
public void Should_Render_Panel_With_Padding()
{
// Given
var console = new PlainConsole(width: 80);
// When
console.Render(new Panel(Text.New("Hello World"))
{
Padding = new Padding(3, 5),
});
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("┌───────────────────┐");
console.Lines[1].ShouldBe("│ Hello World │");
console.Lines[2].ShouldBe("└───────────────────┘");
}
[Fact] [Fact]
public void Should_Render_Panel_With_Unicode_Correctly() public void Should_Render_Panel_With_Unicode_Correctly()
{ {
@ -62,8 +81,7 @@ namespace Spectre.Console.Tests.Unit
// Given // Given
var console = new PlainConsole(width: 80); var console = new PlainConsole(width: 80);
var text = new Panel( var text = new Panel(
Text.New("I heard [underline on blue]you[/] like 📦\n\n\n\nSo I put a 📦 in a 📦"), Text.New("I heard [underline on blue]you[/] like 📦\n\n\n\nSo I put a 📦 in a 📦"));
content: Justify.Center);
// When // When
console.Render(text); console.Render(text);
@ -80,19 +98,23 @@ namespace Spectre.Console.Tests.Unit
} }
[Fact] [Fact]
public void Should_Fit_Panel_To_Parent_If_Enabled() public void Should_Expand_Panel_If_Enabled()
{ {
// Given // Given
var console = new PlainConsole(width: 25); var console = new PlainConsole(width: 80);
// When // When
console.Render(new Panel(Text.New("Hello World"), fit: true)); console.Render(new Panel(Text.New("Hello World"))
{
Expand = true,
});
// Then // Then
console.Lines.Count.ShouldBe(3); console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("┌───────────────────────┐"); console.Lines[0].Length.ShouldBe(80);
console.Lines[0].ShouldBe("┌──────────────────────────────────────────────────────────────────────────────┐");
console.Lines[1].ShouldBe("│ Hello World │"); console.Lines[1].ShouldBe("│ Hello World │");
console.Lines[2].ShouldBe("└───────────────────────┘"); console.Lines[2].ShouldBe("└──────────────────────────────────────────────────────────────────────────────┘");
} }
[Fact] [Fact]
@ -102,7 +124,12 @@ namespace Spectre.Console.Tests.Unit
var console = new PlainConsole(width: 25); var console = new PlainConsole(width: 25);
// When // When
console.Render(new Panel(Text.New("Hello World"), fit: true, content: Justify.Right)); console.Render(
new Panel(
Text.New("Hello World").WithAlignment(Justify.Right))
{
Expand = true,
});
// Then // Then
console.Lines.Count.ShouldBe(3); console.Lines.Count.ShouldBe(3);
@ -118,7 +145,12 @@ namespace Spectre.Console.Tests.Unit
var console = new PlainConsole(width: 25); var console = new PlainConsole(width: 25);
// When // When
console.Render(new Panel(Text.New("Hello World"), fit: true, content: Justify.Center)); console.Render(
new Panel(
Text.New("Hello World").WithAlignment(Justify.Center))
{
Expand = true,
});
// Then // Then
console.Lines.Count.ShouldBe(3); console.Lines.Count.ShouldBe(3);

View File

@ -32,7 +32,7 @@ namespace Spectre.Console.Tests.Unit.Composition
var table = new Table(); var table = new Table();
// When // When
var result = Record.Exception(() => table.AddColumns(null)); var result = Record.Exception(() => table.AddColumns((string[])null));
// Then // Then
result.ShouldBeOfType<ArgumentNullException>() result.ShouldBeOfType<ArgumentNullException>()
@ -88,31 +88,6 @@ namespace Spectre.Console.Tests.Unit.Composition
} }
} }
[Fact]
public void Should_Measure_Table_Correctly()
{
// Given
var console = new PlainConsole(width: 80);
var table = new Table();
table.AddColumns("Foo", "Bar", "Baz");
table.AddRow("Qux", "Corgi", "Waldo");
table.AddRow("Grault", "Garply", "Fred");
// When
console.Render(new Panel(table));
// Then
console.Lines.Count.ShouldBe(8);
console.Lines[0].ShouldBe("┌─────────────────────────────┐");
console.Lines[1].ShouldBe("│ ┌────────┬────────┬───────┐ │");
console.Lines[2].ShouldBe("│ │ Foo │ Bar │ Baz │ │");
console.Lines[3].ShouldBe("│ ├────────┼────────┼───────┤ │");
console.Lines[4].ShouldBe("│ │ Qux │ Corgi │ Waldo │ │");
console.Lines[5].ShouldBe("│ │ Grault │ Garply │ Fred │ │");
console.Lines[6].ShouldBe("│ └────────┴────────┴───────┘ │");
console.Lines[7].ShouldBe("└─────────────────────────────┘");
}
[Fact] [Fact]
public void Should_Render_Table_Correctly() public void Should_Render_Table_Correctly()
{ {
@ -136,6 +111,64 @@ namespace Spectre.Console.Tests.Unit.Composition
console.Lines[5].ShouldBe("└────────┴────────┴───────┘"); console.Lines[5].ShouldBe("└────────┴────────┴───────┘");
} }
[Fact]
public void Should_Render_Table_Nested_In_Panels_Correctly()
{
// A simple table
var console = new PlainConsole(width: 80);
var table = new Table() { Border = BorderKind.Rounded };
table.AddColumn("Foo");
table.AddColumn("Bar");
table.AddColumn(new TableColumn("Baz") { Alignment = Justify.Right });
table.AddRow("Qux\nQuuuuuux", "[blue]Corgi[/]", "Waldo");
table.AddRow("Grault", "Garply", "Fred");
// Render a table in some panels.
console.Render(new Panel(new Panel(table)
{
Border = BorderKind.Ascii,
}));
// Then
console.Lines.Count.ShouldBe(11);
console.Lines[00].ShouldBe("┌───────────────────────────────────┐");
console.Lines[01].ShouldBe("│ +-------------------------------+ │");
console.Lines[02].ShouldBe("│ | ╭──────────┬────────┬───────╮ | │");
console.Lines[03].ShouldBe("│ | │ Foo │ Bar │ Baz │ | │");
console.Lines[04].ShouldBe("│ | ├──────────┼────────┼───────┤ | │");
console.Lines[05].ShouldBe("│ | │ Qux │ Corgi │ Waldo │ | │");
console.Lines[06].ShouldBe("│ | │ Quuuuuux │ │ │ | │");
console.Lines[07].ShouldBe("│ | │ Grault │ Garply │ Fred │ | │");
console.Lines[08].ShouldBe("│ | ╰──────────┴────────┴───────╯ | │");
console.Lines[09].ShouldBe("│ +-------------------------------+ │");
console.Lines[10].ShouldBe("└───────────────────────────────────┘");
}
[Fact]
public void Should_Render_Table_With_Column_Justification_Correctly()
{
// Given
var console = new PlainConsole(width: 80);
var table = new Table();
table.AddColumn(new TableColumn("Foo") { Alignment = Justify.Left });
table.AddColumn(new TableColumn("Bar") { Alignment = Justify.Right });
table.AddColumn(new TableColumn("Baz") { Alignment = Justify.Center });
table.AddRow("Qux", "Corgi", "Waldo");
table.AddRow("Grault", "Garply", "Lorem ipsum dolor sit amet");
// When
console.Render(table);
// Then
console.Lines.Count.ShouldBe(6);
console.Lines[0].ShouldBe("┌────────┬────────┬────────────────────────────┐");
console.Lines[1].ShouldBe("│ Foo │ Bar │ Baz │");
console.Lines[2].ShouldBe("├────────┼────────┼────────────────────────────┤");
console.Lines[3].ShouldBe("│ Qux │ Corgi │ Waldo │");
console.Lines[4].ShouldBe("│ Grault │ Garply │ Lorem ipsum dolor sit amet │");
console.Lines[5].ShouldBe("└────────┴────────┴────────────────────────────┘");
}
[Fact] [Fact]
public void Should_Expand_Table_To_Available_Space_If_Specified() public void Should_Expand_Table_To_Available_Space_If_Specified()
{ {
@ -249,5 +282,30 @@ namespace Spectre.Console.Tests.Unit.Composition
console.Lines[5].ShouldBe("│ Grault │ Garply │ Fred │"); console.Lines[5].ShouldBe("│ Grault │ Garply │ Fred │");
console.Lines[6].ShouldBe("└────────┴────────┴───────┘"); console.Lines[6].ShouldBe("└────────┴────────┴───────┘");
} }
[Fact]
public void Should_Render_Table_With_Cell_Padding_Correctly()
{
// Given
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.AddRow("Qux\nQuuux", "Corgi", "Waldo");
table.AddRow("Grault", "Garply", "Fred");
// When
console.Render(table);
// Then
console.Lines.Count.ShouldBe(7);
console.Lines[0].ShouldBe("┌────────┬────────┬──────────┐");
console.Lines[1].ShouldBe("│ Foo │ Bar │ Baz │");
console.Lines[2].ShouldBe("├────────┼────────┼──────────┤");
console.Lines[3].ShouldBe("│ Qux │ Corgi │ Waldo │");
console.Lines[4].ShouldBe("│ Quuux │ │ │");
console.Lines[5].ShouldBe("│ Grault │ Garply │ Fred │");
console.Lines[6].ShouldBe("└────────┴────────┴──────────┘");
}
} }
} }

View File

@ -20,6 +20,7 @@ namespace Spectre.Console
{ {
Border = BorderKind.None, Border = BorderKind.None,
ShowHeaders = false, ShowHeaders = false,
IsGrid = true,
}; };
} }
@ -40,7 +41,7 @@ namespace Spectre.Console
/// </summary> /// </summary>
public void AddColumn() public void AddColumn()
{ {
_table.AddColumn(string.Empty); AddColumn(new GridColumn());
} }
/// <summary> /// <summary>
@ -58,11 +59,40 @@ namespace Spectre.Console
{ {
Width = column.Width, Width = column.Width,
NoWrap = column.NoWrap, NoWrap = column.NoWrap,
LeftPadding = 0, Padding = column.Padding,
RightPadding = 1, Alignment = column.Alignment,
}); });
} }
/// <summary>
/// Adds a column to the grid.
/// </summary>
/// <param name="count">The number of columns to add.</param>
public void AddColumns(int count)
{
for (var index = 0; index < count; index++)
{
AddColumn(new GridColumn());
}
}
/// <summary>
/// Adds a column to the grid.
/// </summary>
/// <param name="columns">The columns to add.</param>
public void AddColumns(params GridColumn[] columns)
{
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
foreach (var column in columns)
{
AddColumn(column);
}
}
/// <summary> /// <summary>
/// Adds a new row to the grid. /// Adds a new row to the grid.
/// </summary> /// </summary>

View File

@ -1,4 +1,4 @@
namespace Spectre.Console namespace Spectre.Console
{ {
/// <summary> /// <summary>
/// Represents a grid column. /// Represents a grid column.
@ -9,12 +9,22 @@
/// Gets or sets the width of the column. /// Gets or sets the width of the column.
/// If <c>null</c>, the column will adapt to it's contents. /// If <c>null</c>, the column will adapt to it's contents.
/// </summary> /// </summary>
public int? Width { get; set; } public int? Width { get; set; } = null;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether wrapping of /// Gets or sets a value indicating whether wrapping of
/// text within the column should be prevented. /// text within the column should be prevented.
/// </summary> /// </summary>
public bool NoWrap { get; set; } public bool NoWrap { get; set; } = false;
/// <summary>
/// Gets or sets the padding of the column.
/// </summary>
public Padding Padding { get; set; } = new Padding(0, 1);
/// <summary>
/// Gets or sets the alignment of the column.
/// </summary>
public Justify? Alignment { get; set; } = null;
} }
} }

View File

@ -0,0 +1,86 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Represents a measurement.
/// </summary>
public struct Padding : IEquatable<Padding>
{
/// <summary>
/// Gets the left padding.
/// </summary>
public int Left { get; }
/// <summary>
/// Gets the right padding.
/// </summary>
public int Right { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Padding"/> struct.
/// </summary>
/// <param name="left">The left padding.</param>
/// <param name="right">The right padding.</param>
public Padding(int left, int right)
{
Left = left;
Right = right;
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
return obj is Padding padding && Equals(padding);
}
/// <inheritdoc/>
public override int GetHashCode()
{
unchecked
{
var hash = (int)2166136261;
hash = (hash * 16777619) ^ Left.GetHashCode();
hash = (hash * 16777619) ^ Right.GetHashCode();
return hash;
}
}
/// <inheritdoc/>
public bool Equals(Padding other)
{
return Left == other.Left && Right == other.Right;
}
/// <summary>
/// Checks if two <see cref="Padding"/> instances are equal.
/// </summary>
/// <param name="left">The first <see cref="Padding"/> instance to compare.</param>
/// <param name="right">The second <see cref="Padding"/> instance to compare.</param>
/// <returns><c>true</c> if the two instances are equal, otherwise <c>false</c>.</returns>
public static bool operator ==(Padding left, Padding right)
{
return left.Equals(right);
}
/// <summary>
/// Checks if two <see cref="Padding"/> instances are not equal.
/// </summary>
/// <param name="left">The first <see cref="Padding"/> instance to compare.</param>
/// <param name="right">The second <see cref="Padding"/> instance to compare.</param>
/// <returns><c>true</c> if the two instances are not equal, otherwise <c>false</c>.</returns>
public static bool operator !=(Padding left, Padding right)
{
return !(left == right);
}
/// <summary>
/// Gets the horizontal padding.
/// </summary>
/// <returns>The horizontal padding.</returns>
public int GetHorizontalPadding()
{
return Left + Right;
}
}
}

View File

@ -10,9 +10,6 @@ namespace Spectre.Console
public sealed class Panel : IRenderable public sealed class Panel : IRenderable
{ {
private readonly IRenderable _child; private readonly IRenderable _child;
private readonly bool _fit;
private readonly Justify _content;
private readonly BorderKind _border;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not to use /// Gets or sets a value indicating whether or not to use
@ -21,113 +18,109 @@ namespace Spectre.Console
/// </summary> /// </summary>
public bool SafeBorder { get; set; } = true; public bool SafeBorder { get; set; } = true;
/// <summary>
/// Gets or sets the kind of border to use.
/// </summary>
public BorderKind Border { get; set; } = BorderKind.Square;
/// <summary>
/// Gets or sets the alignment of the panel contents.
/// </summary>
public Justify? Alignment { get; set; } = null;
/// <summary>
/// Gets or sets a value indicating whether or not the panel should
/// fit the available space. If <c>false</c>, the panel width will be
/// auto calculated. Defaults to <c>false</c>.
/// </summary>
public bool Expand { get; set; } = false;
/// <summary>
/// Gets or sets the padding.
/// </summary>
public Padding Padding { get; set; } = new Padding(1, 1);
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Panel"/> class. /// Initializes a new instance of the <see cref="Panel"/> class.
/// </summary> /// </summary>
/// <param name="child">The child.</param> /// <param name="content">The panel content.</param>
/// <param name="fit">Whether or not to fit the panel to it's parent.</param> public Panel(IRenderable content)
/// <param name="content">The justification of the panel content.</param>
/// <param name="border">The border to use.</param>
public Panel(
IRenderable child,
bool fit = false,
Justify content = Justify.Left,
BorderKind border = BorderKind.Square)
{ {
_child = child ?? throw new System.ArgumentNullException(nameof(child)); _child = content ?? throw new System.ArgumentNullException(nameof(content));
_fit = fit;
_content = content;
_border = border;
} }
/// <inheritdoc/> /// <inheritdoc/>
Measurement IRenderable.Measure(RenderContext context, int maxWidth) Measurement IRenderable.Measure(RenderContext context, int maxWidth)
{ {
var childWidth = _child.Measure(context, maxWidth); var childWidth = _child.Measure(context, maxWidth);
return new Measurement(childWidth.Min + 4, childWidth.Max + 4); return new Measurement(childWidth.Min + 2 + Padding.GetHorizontalPadding(), childWidth.Max + 2 + Padding.GetHorizontalPadding());
} }
/// <inheritdoc/> /// <inheritdoc/>
IEnumerable<Segment> IRenderable.Render(RenderContext context, int width) IEnumerable<Segment> IRenderable.Render(RenderContext context, int width)
{ {
var border = Border.GetBorder(_border, (context.LegacyConsole || !context.Unicode) && SafeBorder); var border = Composition.Border.GetBorder(Border, (context.LegacyConsole || !context.Unicode) && SafeBorder);
var childWidth = width - 4; var edgeWidth = 2;
if (!_fit) var paddingWidth = Padding.GetHorizontalPadding();
var childWidth = width - edgeWidth - paddingWidth;
if (!Expand)
{ {
var measurement = _child.Measure(context, width - 2); var measurement = _child.Measure(context, width - edgeWidth - paddingWidth);
childWidth = measurement.Max; childWidth = measurement.Max;
} }
var result = new List<Segment>(); var panelWidth = childWidth + paddingWidth;
var panelWidth = childWidth + 2;
result.Add(new Segment(border.GetPart(BorderPart.HeaderTopLeft))); // Panel top
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, panelWidth))); var result = new List<Segment>
result.Add(new Segment(border.GetPart(BorderPart.HeaderTopRight))); {
result.Add(new Segment("\n")); new Segment(border.GetPart(BorderPart.HeaderTopLeft)),
new Segment(border.GetPart(BorderPart.HeaderTop, panelWidth)),
new Segment(border.GetPart(BorderPart.HeaderTopRight)),
new Segment("\n"),
};
// Render the child. // Render the child.
var childSegments = _child.Render(context, childWidth); var childContext = context.WithJustification(Alignment);
var childSegments = _child.Render(childContext, childWidth);
// Split the child segments into lines. // Split the child segments into lines.
var lines = Segment.SplitLines(childSegments, childWidth); foreach (var line in Segment.SplitLines(childSegments, panelWidth))
foreach (var line in lines)
{ {
result.Add(new Segment(border.GetPart(BorderPart.CellLeft))); result.Add(new Segment(border.GetPart(BorderPart.CellLeft)));
result.Add(new Segment(" ")); // Left padding
// Left padding
if (Padding.Left > 0)
{
result.Add(new Segment(new string(' ', Padding.Left)));
}
var content = new List<Segment>(); var content = new List<Segment>();
content.AddRange(line);
// Do we need to pad the panel?
var length = line.Sum(segment => segment.CellLength(context.Encoding)); var length = line.Sum(segment => segment.CellLength(context.Encoding));
if (length < childWidth) if (length < childWidth)
{
// Justify right side
if (_content == Justify.Right)
{ {
var diff = childWidth - length; var diff = childWidth - length;
content.Add(new Segment(new string(' ', diff))); content.Add(new Segment(new string(' ', diff)));
} }
else if (_content == Justify.Center)
{
var diff = (childWidth - length) / 2;
content.Add(new Segment(new string(' ', diff)));
}
}
foreach (var segment in line)
{
content.Add(segment.StripLineEndings());
}
// Justify left side
if (length < childWidth)
{
if (_content == Justify.Left)
{
var diff = childWidth - length;
content.Add(new Segment(new string(' ', diff)));
}
else if (_content == Justify.Center)
{
var diff = (childWidth - length) / 2;
content.Add(new Segment(new string(' ', diff)));
var remainder = (childWidth - length) % 2;
if (remainder != 0)
{
content.Add(new Segment(new string(' ', remainder)));
}
}
}
result.AddRange(content); result.AddRange(content);
result.Add(new Segment(" ")); // Right padding
if (Padding.Right > 0)
{
result.Add(new Segment(new string(' ', Padding.Right)));
}
result.Add(new Segment(border.GetPart(BorderPart.CellRight))); result.Add(new Segment(border.GetPart(BorderPart.CellRight)));
result.Add(new Segment("\n")); result.Add(new Segment("\n"));
} }
// Panel bottom
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft))); result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, panelWidth))); result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, panelWidth)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight))); result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight)));

View File

@ -22,16 +22,33 @@ namespace Spectre.Console.Composition
/// </summary> /// </summary>
public bool Unicode { get; } public bool Unicode { get; }
/// <summary>
/// Gets the current justification.
/// </summary>
public Justify? Justification { get; }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RenderContext"/> class. /// Initializes a new instance of the <see cref="RenderContext"/> class.
/// </summary> /// </summary>
/// <param name="encoding">The console's output encoding.</param> /// <param name="encoding">The console's output encoding.</param>
/// <param name="legacyConsole">A value indicating whether or not this a legacy console (i.e. cmd.exe).</param> /// <param name="legacyConsole">A value indicating whether or not this a legacy console (i.e. cmd.exe).</param>
public RenderContext(Encoding encoding, bool legacyConsole) /// <param name="justification">The justification to use when rendering.</param>
public RenderContext(Encoding encoding, bool legacyConsole, Justify? justification = null)
{ {
Encoding = encoding ?? throw new System.ArgumentNullException(nameof(encoding)); Encoding = encoding ?? throw new System.ArgumentNullException(nameof(encoding));
LegacyConsole = legacyConsole; LegacyConsole = legacyConsole;
Justification = justification;
Unicode = Encoding == Encoding.UTF8 || Encoding == Encoding.Unicode; Unicode = Encoding == Encoding.UTF8 || Encoding == Encoding.Unicode;
} }
/// <summary>
/// Creates a new context with the specified justification.
/// </summary>
/// <param name="justification">The justification.</param>
/// <returns>A new <see cref="RenderContext"/> instance with the specified justification.</returns>
public RenderContext WithJustification(Justify? justification)
{
return new RenderContext(Encoding, LegacyConsole, justification);
}
} }
} }

View File

@ -21,47 +21,6 @@ namespace Spectre.Console
var tableWidth = widths.Sum(); var tableWidth = widths.Sum();
if (ShouldExpand())
{
var ratios = _columns.Select(c => c.Ratio ?? 0).ToList();
if (ratios.Any(r => r != 0))
{
var fixedWidths = new List<int>();
foreach (var (range, column) in width_ranges.Zip(_columns, (a, b) => (a, b)))
{
fixedWidths.Add(column.IsFlexible() ? 0 : range.Max);
}
var flexMinimum = new List<int>();
foreach (var column in _columns)
{
if (column.IsFlexible())
{
flexMinimum.Add(column.Width ?? 1 + column.GetPadding());
}
else
{
flexMinimum.Add(0);
}
}
var flexibleWidth = maxWidth - fixedWidths.Sum();
var flexWidths = Ratio.Distribute(flexibleWidth, ratios, flexMinimum);
var flexWidthsIterator = flexWidths.GetEnumerator();
foreach (var (index, _, _, column) in _columns.Enumerate())
{
if (column.IsFlexible())
{
flexWidthsIterator.MoveNext();
widths[index] = fixedWidths[index] + flexWidthsIterator.Current;
}
}
}
}
tableWidth = widths.Sum();
if (tableWidth > maxWidth) if (tableWidth > maxWidth)
{ {
var wrappable = _columns.Select(c => !c.NoWrap).ToList(); var wrappable = _columns.Select(c => !c.NoWrap).ToList();
@ -72,7 +31,7 @@ namespace Spectre.Console
if (tableWidth > maxWidth) if (tableWidth > maxWidth)
{ {
var excessWidth = tableWidth - maxWidth; var excessWidth = tableWidth - maxWidth;
widths = Ratio.Reduce(excessWidth, widths.Select(w => 1).ToList(), widths, widths); widths = Ratio.Reduce(excessWidth, widths.Select(_ => 1).ToList(), widths, widths);
tableWidth = widths.Sum(); tableWidth = widths.Sum();
} }
} }
@ -96,26 +55,17 @@ namespace Spectre.Console
if (wrappable.AnyTrue()) if (wrappable.AnyTrue())
{ {
while (totalWidth > 0 && excessWidth > 0) while (totalWidth != 0 && excessWidth > 0)
{ {
var maxColumn = widths.Zip(wrappable, (first, second) => (width: first, isWrappable: second)) var maxColumn = widths.Zip(wrappable, (first, second) => (width: first, allowWrap: second))
.Where(x => x.isWrappable) .Where(x => x.allowWrap)
.Max(x => x.width); .Max(x => x.width);
var secondMaxColumn = widths.Zip(wrappable, (width, isWrappable) => isWrappable && width != maxColumn ? maxColumn : 0).Max(); var secondMaxColumn = widths.Zip(wrappable, (width, allowWrap) => allowWrap && width != maxColumn ? width : 0).Max();
var columnDifference = maxColumn - secondMaxColumn; var columnDifference = maxColumn - secondMaxColumn;
var ratios = widths.Zip(wrappable, (width, allowWrap) => var ratios = widths.Zip(wrappable, (width, allowWrap) => width == maxColumn && allowWrap ? 1 : 0).ToList();
{ if (!ratios.Any(x => x != 0) || columnDifference == 0)
if (width == maxColumn && allowWrap)
{
return 1;
}
return 0;
}).ToList();
if (!ratios.Any(x => x > 0) || columnDifference == 0)
{ {
break; break;
} }
@ -133,10 +83,11 @@ namespace Spectre.Console
private (int Min, int Max) MeasureColumn(TableColumn column, RenderContext options, int maxWidth) private (int Min, int Max) MeasureColumn(TableColumn column, RenderContext options, int maxWidth)
{ {
var padding = column.Padding.GetHorizontalPadding();
// Predetermined width? // Predetermined width?
if (column.Width != null) if (column.Width != null)
{ {
var padding = column.GetPadding();
return (column.Width.Value + padding, column.Width.Value + padding); return (column.Width.Value + padding, column.Width.Value + padding);
} }
@ -152,7 +103,7 @@ namespace Spectre.Console
maxWidths.Add(measure.Max); maxWidths.Add(measure.Max);
} }
return (minWidths.Count > 0 ? minWidths.Max() : 1, return (minWidths.Count > 0 ? minWidths.Max() : padding,
maxWidths.Count > 0 ? maxWidths.Max() : maxWidth); maxWidths.Count > 0 ? maxWidths.Max() : maxWidth);
} }
@ -160,7 +111,7 @@ namespace Spectre.Console
{ {
var edges = 2; var edges = 2;
var separators = _columns.Count - 1; var separators = _columns.Count - 1;
var padding = includePadding ? _columns.Select(x => x.GetPadding()).Sum() : 0; var padding = includePadding ? _columns.Select(x => x.Padding.GetHorizontalPadding()).Sum() : 0;
return separators + edges + padding; return separators + edges + padding;
} }
} }

View File

@ -53,6 +53,8 @@ namespace Spectre.Console
/// </summary> /// </summary>
public bool SafeBorder { get; set; } = true; public bool SafeBorder { get; set; } = true;
internal bool IsGrid { get; set; } = false;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Table"/> class. /// Initializes a new instance of the <see cref="Table"/> class.
/// </summary> /// </summary>
@ -104,6 +106,20 @@ namespace Spectre.Console
_columns.AddRange(columns.Select(column => new TableColumn(column))); _columns.AddRange(columns.Select(column => new TableColumn(column)));
} }
/// <summary>
/// Adds multiple columns to the table.
/// </summary>
/// <param name="columns">The columns to add.</param>
public void AddColumns(params TableColumn[] columns)
{
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
_columns.AddRange(columns.Select(column => column));
}
/// <summary> /// <summary>
/// Adds a row to the table. /// Adds a row to the table.
/// </summary> /// </summary>
@ -175,7 +191,7 @@ namespace Spectre.Console
var columnWidths = CalculateColumnWidths(context, maxWidth); var columnWidths = CalculateColumnWidths(context, maxWidth);
// Update the table width. // Update the table width.
width = columnWidths.Sum() + GetExtraWidth(includePadding: false); width = columnWidths.Sum() + GetExtraWidth(includePadding: true);
var rows = new List<List<Text>>(); var rows = new List<List<Text>>();
if (ShowHeaders) if (ShowHeaders)
@ -195,9 +211,12 @@ namespace Spectre.Console
// Get the list of cells for the row and calculate the cell height // Get the list of cells for the row and calculate the cell height
var cells = new List<List<SegmentLine>>(); var cells = new List<List<SegmentLine>>();
foreach (var (rowWidth, cell) in columnWidths.Zip(row, (f, s) => (f, s))) foreach (var (columnIndex, _, _, (rowWidth, cell)) in columnWidths.Zip(row).Enumerate())
{ {
var lines = Segment.SplitLines(((IRenderable)cell).Render(context, rowWidth)); var justification = _columns[columnIndex].Alignment;
var childContext = context.WithJustification(justification);
var lines = Segment.SplitLines(((IRenderable)cell).Render(childContext, rowWidth));
cellHeight = Math.Max(cellHeight, lines.Count); cellHeight = Math.Max(cellHeight, lines.Count);
cells.Add(lines); cells.Add(lines);
} }
@ -208,9 +227,11 @@ namespace Spectre.Console
result.Add(new Segment(border.GetPart(BorderPart.HeaderTopLeft))); result.Add(new Segment(border.GetPart(BorderPart.HeaderTopLeft)));
foreach (var (columnIndex, _, lastColumn, columnWidth) in columnWidths.Enumerate()) foreach (var (columnIndex, _, lastColumn, columnWidth) in columnWidths.Enumerate())
{ {
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop))); // Left padding var padding = _columns[columnIndex].Padding;
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, padding.Left))); // Left padding
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, columnWidth))); result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, columnWidth)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop))); // Right padding result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, padding.Right))); // Right padding
if (!lastColumn) if (!lastColumn)
{ {
@ -237,9 +258,9 @@ namespace Spectre.Console
} }
// Pad column on left side. // Pad column on left side.
if (showBorder) if (showBorder || IsGrid)
{ {
var leftPadding = _columns[cellIndex].LeftPadding; var leftPadding = _columns[cellIndex].Padding.Left;
if (leftPadding > 0) if (leftPadding > 0)
{ {
result.Add(new Segment(new string(' ', leftPadding))); result.Add(new Segment(new string(' ', leftPadding)));
@ -257,9 +278,9 @@ namespace Spectre.Console
} }
// Pad column on the right side // Pad column on the right side
if (showBorder || (hideBorder && !lastCell)) if (showBorder || (hideBorder && !lastCell) || (IsGrid && !lastCell))
{ {
var rightPadding = _columns[cellIndex].RightPadding; var rightPadding = _columns[cellIndex].Padding.Right;
if (rightPadding > 0) if (rightPadding > 0)
{ {
result.Add(new Segment(new string(' ', rightPadding))); result.Add(new Segment(new string(' ', rightPadding)));
@ -281,15 +302,17 @@ namespace Spectre.Console
result.Add(Segment.LineBreak()); result.Add(Segment.LineBreak());
} }
// Show bottom of header? // Show header separator?
if (firstRow && showBorder && ShowHeaders) if (firstRow && showBorder && ShowHeaders)
{ {
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomLeft))); result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomLeft)));
foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate()) foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
{ {
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom))); // Left padding var padding = _columns[columnIndex].Padding;
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom, padding.Left))); // Left padding
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom, columnWidth))); result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom, columnWidth)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom))); // Right padding result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom, padding.Right))); // Right padding
if (!lastColumn) if (!lastColumn)
{ {
@ -307,9 +330,11 @@ namespace Spectre.Console
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft))); result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft)));
foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate()) foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
{ {
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom))); var padding = _columns[columnIndex].Padding;
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, padding.Left))); // Left padding
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, columnWidth))); result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, columnWidth)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom))); result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, padding.Right))); // Right padding
if (!lastColumn) if (!lastColumn)
{ {

View File

@ -19,20 +19,9 @@ namespace Spectre.Console
public int? Width { get; set; } public int? Width { get; set; }
/// <summary> /// <summary>
/// Gets or sets the left padding. /// Gets or sets the padding of the column.
/// </summary> /// </summary>
public int LeftPadding { get; set; } public Padding Padding { get; set; }
/// <summary>
/// Gets or sets the right padding.
/// </summary>
public int RightPadding { get; set; }
/// <summary>
/// Gets or sets the ratio to use when calculating column width.
/// If <c>null</c>, the column will adapt to it's contents.
/// </summary>
public int? Ratio { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether wrapping of /// Gets or sets a value indicating whether wrapping of
@ -40,6 +29,11 @@ namespace Spectre.Console
/// </summary> /// </summary>
public bool NoWrap { get; set; } public bool NoWrap { get; set; }
/// <summary>
/// Gets or sets the alignment of the column.
/// </summary>
public Justify? Alignment { get; set; }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TableColumn"/> class. /// Initializes a new instance of the <see cref="TableColumn"/> class.
/// </summary> /// </summary>
@ -48,20 +42,9 @@ namespace Spectre.Console
{ {
Text = Text.New(text ?? throw new ArgumentNullException(nameof(text))); Text = Text.New(text ?? throw new ArgumentNullException(nameof(text)));
Width = null; Width = null;
LeftPadding = 1; Padding = new Padding(1, 1);
RightPadding = 1;
Ratio = null;
NoWrap = false; NoWrap = false;
} Alignment = null;
internal int GetPadding()
{
return LeftPadding + RightPadding;
}
internal bool IsFlexible()
{
return Width == null;
} }
} }
} }

View File

@ -18,6 +18,11 @@ namespace Spectre.Console
private readonly List<Span> _spans; private readonly List<Span> _spans;
private string _text; private string _text;
/// <summary>
/// Gets or sets the text alignment.
/// </summary>
public Justify Alignment { get; set; } = Justify.Left;
private sealed class Span private sealed class Span
{ {
public int Start { get; } public int Start { get; }
@ -38,7 +43,7 @@ namespace Spectre.Console
/// <param name="text">The text.</param> /// <param name="text">The text.</param>
internal Text(string text) internal Text(string text)
{ {
_text = text ?? throw new ArgumentNullException(nameof(text)); _text = text?.NormalizeLineEndings() ?? throw new ArgumentNullException(nameof(text));
_spans = new List<Span>(); _spans = new List<Span>();
} }
@ -51,12 +56,26 @@ namespace Spectre.Console
/// <param name="decoration">The text decoration.</param> /// <param name="decoration">The text decoration.</param>
/// <returns>A <see cref="Text"/> instance.</returns> /// <returns>A <see cref="Text"/> instance.</returns>
public static Text New( public static Text New(
string text, Color? foreground = null, Color? background = null, Decoration? decoration = null) string text,
Color? foreground = null,
Color? background = null,
Decoration? decoration = null)
{ {
var result = MarkupParser.Parse(text, new Style(foreground, background, decoration)); var result = MarkupParser.Parse(text, new Style(foreground, background, decoration));
return result; return result;
} }
/// <summary>
/// Sets the text alignment.
/// </summary>
/// <param name="alignment">The text alignment.</param>
/// <returns>The same <see cref="Text"/> instance.</returns>
public Text WithAlignment(Justify alignment)
{
Alignment = alignment;
return this;
}
/// <summary> /// <summary>
/// Appends some text with the specified color and decorations. /// Appends some text with the specified color and decorations.
/// </summary> /// </summary>
@ -99,14 +118,17 @@ namespace Spectre.Console
/// <inheritdoc/> /// <inheritdoc/>
Measurement IRenderable.Measure(RenderContext context, int maxWidth) Measurement IRenderable.Measure(RenderContext context, int maxWidth)
{ {
var lines = Segment.SplitLines(((IRenderable)this).Render(context, maxWidth)); if (string.IsNullOrEmpty(_text))
if (lines.Count == 0)
{ {
return new Measurement(0, maxWidth); return new Measurement(1, 1);
} }
var max = lines.Max(line => line.Length); // TODO: Write some kind of tokenizer for this
var min = lines.SelectMany(line => line.Select(segment => segment.Text.Length)).Max(); var min = Segment.SplitLines(((IRenderable)this).Render(context, maxWidth))
.SelectMany(line => line.Select(segment => segment.Text.Length))
.Max();
var max = _text.SplitLines().Max(x => Cell.GetCellLength(context.Encoding, x));
return new Measurement(min, max); return new Measurement(min, max);
} }
@ -122,13 +144,49 @@ namespace Spectre.Console
var result = new List<Segment>(); var result = new List<Segment>();
var segments = SplitLineBreaks(CreateSegments()); var segments = SplitLineBreaks(CreateSegments());
var justification = context.Justification ?? Alignment;
foreach (var (_, _, last, line) in Segment.SplitLines(segments, width).Enumerate()) foreach (var (_, _, last, line) in Segment.SplitLines(segments, width).Enumerate())
{ {
var length = line.Sum(l => l.StripLineEndings().CellLength(context.Encoding));
if (length < width)
{
// Justify right side
if (justification == Justify.Right)
{
var diff = width - length;
result.Add(new Segment(new string(' ', diff)));
}
else if (justification == Justify.Center)
{
var diff = (width - length) / 2;
result.Add(new Segment(new string(' ', diff)));
}
}
// Render the line.
foreach (var segment in line) foreach (var segment in line)
{ {
result.Add(segment.StripLineEndings()); result.Add(segment.StripLineEndings());
} }
// Justify left side
if (length < width)
{
if (justification == Justify.Center)
{
var diff = (width - length) / 2;
result.Add(new Segment(new string(' ', diff)));
var remainder = (width - length) % 2;
if (remainder != 0)
{
result.Add(new Segment(new string(' ', remainder)));
}
}
}
if (!last) if (!last)
{ {
result.Add(Segment.LineBreak()); result.Add(Segment.LineBreak());

View File

@ -17,6 +17,11 @@ namespace Spectre.Console.Internal
public static string NormalizeLineEndings(this string text, bool native = false) public static string NormalizeLineEndings(this string text, bool native = false)
{ {
if (text == null)
{
return null;
}
var normalized = text?.Replace("\r\n", "\n") var normalized = text?.Replace("\r\n", "\n")
?.Replace("\r", string.Empty); ?.Replace("\r", string.Empty);

View File

@ -14,7 +14,7 @@ namespace Spectre.Console.Internal
{ {
ratios = ratios.Zip(maximums, (a, b) => (ratio: a, max: b)).Select(a => a.max > 0 ? a.ratio : 0).ToList(); ratios = ratios.Zip(maximums, (a, b) => (ratio: a, max: b)).Select(a => a.max > 0 ? a.ratio : 0).ToList();
var totalRatio = ratios.Sum(); var totalRatio = ratios.Sum();
if (totalRatio == 0) if (totalRatio <= 0)
{ {
return values; return values;
} }
@ -24,9 +24,9 @@ namespace Spectre.Console.Internal
foreach (var (ratio, maximum, value) in ratios.Zip(maximums, values)) foreach (var (ratio, maximum, value) in ratios.Zip(maximums, values))
{ {
if (ratio > 0 && totalRatio > 0) if (ratio != 0 && totalRatio > 0)
{ {
var distributed = (int)Math.Min(maximum, Math.Round(ratio * totalRemaining / (double)totalRatio)); var distributed = (int)Math.Min(maximum, Math.Round((double)(ratio * totalRemaining / totalRatio)));
result.Add(value - distributed); result.Add(value - distributed);
totalRemaining -= distributed; totalRemaining -= distributed;
totalRatio -= ratio; totalRatio -= ratio;