Don't limit tables and grids to markup text

Closes #13
This commit is contained in:
Patrik Svensson 2020-08-24 23:24:23 +02:00 committed by Patrik Svensson
parent effdecb1d4
commit 8a01b93aca
41 changed files with 472 additions and 193 deletions

View File

@ -1,4 +1,3 @@
using System;
using Spectre.Console;
namespace GridExample

View File

@ -1,4 +1,3 @@
using System;
using Spectre.Console;
namespace PanelExample

View File

@ -1,4 +1,3 @@
using System;
using Spectre.Console;
namespace TableExample
@ -12,6 +11,42 @@ namespace TableExample
// A big table
RenderBigTable();
// A complex table
RenderComplexTable();
}
private static void RenderComplexTable()
{
// Create simple table.
var simple = new Table { Border = BorderKind.Rounded };
simple.AddColumn(new TableColumn("[u]Foo[/]").Centered());
simple.AddColumn(new TableColumn("[u]Bar[/]"));
simple.AddColumn(new TableColumn("[u]Baz[/]"));
simple.AddRow("Hello", "[red]World![/]", "");
simple.AddRow("[blue]Bounjour[/]", "[white]le[/]", "[red]monde![/]");
simple.AddRow("[blue]Hej[/]", "[yellow]Världen![/]", "");
// Create other table.
var second = new Table { Border = BorderKind.Square };
second.AddColumn(new TableColumn("[u]Foo[/]"));
second.AddColumn(new TableColumn("[u]Bar[/]"));
second.AddColumn(new TableColumn("[u]Baz[/]"));
second.AddRow("Hello", "[red]World![/]", "");
second.AddRow(simple, new Text("Whaaat"), new Text("Lolz"));
second.AddRow("[blue]Hej[/]", "[yellow]Världen![/]", "");
var table = new Table { Border = BorderKind.Rounded };
table.AddColumn(new TableColumn(new Panel("[u]Foo[/]")));
table.AddColumn(new TableColumn(new Panel("[u]Bar[/]")));
table.AddColumn(new TableColumn(new Panel("[u]Baz[/]")));
// Add some rows
table.AddRow(new Text("Hello").Centered(), new Markup("[red]World![/] 🌍"), Text.Empty);
table.AddRow(second, new Text("Whaaat"), new Text("Lol"));
table.AddRow(new Markup("[blue]Hej[/]").Centered(), new Markup("[yellow]Världen![/]"), Text.Empty);
AnsiConsole.Render(table);
}
private static void RenderSimpleTable()

View File

@ -81,3 +81,6 @@ dotnet_diagnostic.RCS1079.severity = warning
# RCS1057: Add empty line between declarations.
dotnet_diagnostic.RCS1057.severity = none
# IDE0004: Remove Unnecessary Cast
dotnet_diagnostic.IDE0004.severity = warning

View File

@ -1,6 +1,6 @@
using System;
using Shouldly;
using Spectre.Console.Composition;
using Spectre.Console.Rendering;
using Xunit;
namespace Spectre.Console.Tests.Unit

View File

@ -125,8 +125,7 @@ namespace Spectre.Console.Tests.Unit
// When
console.Render(
new Panel(
new Text("Hello World").WithAlignment(Justify.Right))
new Panel(new Text("Hello World").RightAligned())
{
Expand = true,
});
@ -146,8 +145,7 @@ namespace Spectre.Console.Tests.Unit
// When
console.Render(
new Panel(
new Text("Hello World").WithAlignment(Justify.Center))
new Panel(new Text("Hello World").Centered())
{
Expand = true,
});

View File

@ -1,5 +1,5 @@
using Shouldly;
using Spectre.Console.Composition;
using Spectre.Console.Rendering;
using Xunit;
namespace Spectre.Console.Tests.Unit

View File

@ -59,7 +59,21 @@ namespace Spectre.Console.Tests.Unit
public sealed class TheAddRowMethod
{
[Fact]
public void Should_Throw_If_Rows_Are_Null()
public void Should_Throw_If_String_Rows_Are_Null()
{
// Given
var table = new Table();
// When
var result = Record.Exception(() => table.AddRow((string[])null));
// Then
result.ShouldBeOfType<ArgumentNullException>()
.ParamName.ShouldBe("columns");
}
[Fact]
public void Should_Throw_If_Renderable_Rows_Are_Null()
{
// Given
var table = new Table();

View File

@ -1,87 +1,81 @@
using System.Text;
using Shouldly;
using Spectre.Console.Composition;
using Spectre.Console.Rendering;
using Xunit;
namespace Spectre.Console.Tests.Unit
{
public sealed class TextTests
{
public sealed class Measuring
[Fact]
public void Should_Consider_The_Longest_Word_As_Minimum_Width()
{
[Fact]
public void Should_Return_The_Longest_Word_As_Minimum_Width()
{
var text = new Text("Foo Bar Baz\nQux\nLol mobile");
var text = new Text("Foo Bar Baz\nQux\nLol mobile");
var result = text.Measure(new RenderContext(Encoding.Unicode, false), 80);
var result = ((IRenderable)text).Measure(new RenderContext(Encoding.Unicode, false), 80);
result.Min.ShouldBe(6);
}
[Fact]
public void Should_Return_The_Longest_Line_As_Maximum_Width()
{
var text = new Text("Foo Bar Baz\nQux\nLol mobile");
var result = text.Measure(new RenderContext(Encoding.Unicode, false), 80);
result.Max.ShouldBe(11);
}
result.Min.ShouldBe(6);
}
public sealed class Rendering
[Fact]
public void Should_Consider_The_Longest_Line_As_Maximum_Width()
{
[Fact]
public void Should_Render_Unstyled_Text_As_Expected()
{
// Given
var fixture = new PlainConsole(width: 80);
var text = new Text("Hello World");
var text = new Text("Foo Bar Baz\nQux\nLol mobile");
// When
fixture.Render(text);
var result = ((IRenderable)text).Measure(new RenderContext(Encoding.Unicode, false), 80);
// Then
fixture.Output
.NormalizeLineEndings()
.ShouldBe("Hello World");
}
result.Max.ShouldBe(11);
}
[Theory]
[InlineData("Hello\n\nWorld\n\n")]
[InlineData("Hello\r\n\r\nWorld\r\n\r\n")]
public void Should_Write_Line_Breaks(string input)
{
// Given
var fixture = new PlainConsole(width: 5);
var text = new Text(input);
[Fact]
public void Should_Render_Unstyled_Text_As_Expected()
{
// Given
var fixture = new PlainConsole(width: 80);
var text = new Text("Hello World");
// When
fixture.Render(text);
// When
fixture.Render(text);
// Then
fixture.RawOutput.ShouldBe("Hello\n\nWorld\n\n");
}
// Then
fixture.Output
.NormalizeLineEndings()
.ShouldBe("Hello World");
}
[Theory]
[InlineData(5, "Hello World", "Hello\nWorld")]
[InlineData(10, "Hello Sweet Nice World", "Hello \nSweet Nice\nWorld")]
public void Should_Split_Unstyled_Text_To_New_Lines_If_Width_Exceeds_Console_Width(
int width, string input, string expected)
{
// Given
var fixture = new PlainConsole(width);
var text = new Text(input);
[Theory]
[InlineData("Hello\n\nWorld\n\n")]
[InlineData("Hello\r\n\r\nWorld\r\n\r\n")]
public void Should_Write_Line_Breaks(string input)
{
// Given
var fixture = new PlainConsole(width: 5);
var text = new Text(input);
// When
fixture.Render(text);
// When
fixture.Render(text);
// Then
fixture.Output
.NormalizeLineEndings()
.ShouldBe(expected);
}
// Then
fixture.RawOutput.ShouldBe("Hello\n\nWorld\n\n");
}
[Theory]
[InlineData(5, "Hello World", "Hello\nWorld")]
[InlineData(10, "Hello Sweet Nice World", "Hello \nSweet Nice\nWorld")]
public void Should_Split_Unstyled_Text_To_New_Lines_If_Width_Exceeds_Console_Width(
int width, string input, string expected)
{
// Given
var fixture = new PlainConsole(width);
var text = new Text(input);
// When
fixture.Render(text);
// Then
fixture.Output
.NormalizeLineEndings()
.ShouldBe(expected);
}
}
}

View File

@ -1,4 +1,4 @@
using Spectre.Console.Composition;
using Spectre.Console.Rendering;
namespace Spectre.Console
{

View File

@ -1,27 +0,0 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="Text"/>.
/// </summary>
public static class TextExtensions
{
/// <summary>
/// Sets the text alignment.
/// </summary>
/// <param name="text">The <see cref="Text"/> instance.</param>
/// <param name="alignment">The text alignment.</param>
/// <returns>The same <see cref="Text"/> instance.</returns>
public static Text WithAlignment(this Text text, Justify alignment)
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
text.Alignment = alignment;
return text;
}
}
}

View File

@ -1,6 +1,6 @@
using System;
using Spectre.Console.Composition;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console
{

View File

@ -0,0 +1,61 @@
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="IAlignable"/>.
/// </summary>
public static class AlignableExtensions
{
/// <summary>
/// Sets the alignment for an <see cref="IAlignable"/> object.
/// </summary>
/// <typeparam name="T">The alignable type.</typeparam>
/// <param name="alignable">The alignable object.</param>
/// <param name="alignment">The alignment.</param>
/// <returns>The same alignable object.</returns>
public static T WithAlignment<T>(this T alignable, Justify alignment)
where T : IAlignable
{
alignable.Alignment = alignment;
return alignable;
}
/// <summary>
/// Sets the <see cref="IAlignable"/> object to be left aligned.
/// </summary>
/// <typeparam name="T">The alignable type.</typeparam>
/// <param name="alignable">The alignable object.</param>
/// <returns>The same alignable object.</returns>
public static T LeftAligned<T>(this T alignable)
where T : IAlignable
{
alignable.Alignment = Justify.Left;
return alignable;
}
/// <summary>
/// Sets the <see cref="IAlignable"/> object to be centered.
/// </summary>
/// <typeparam name="T">The alignable type.</typeparam>
/// <param name="alignable">The alignable object.</param>
/// <returns>The same alignable object.</returns>
public static T Centered<T>(this T alignable)
where T : IAlignable
{
alignable.Alignment = Justify.Center;
return alignable;
}
/// <summary>
/// Sets the <see cref="IAlignable"/> object to be right aligned.
/// </summary>
/// <typeparam name="T">The alignable type.</typeparam>
/// <param name="alignable">The alignable object.</param>
/// <returns>The same alignable object.</returns>
public static T RightAligned<T>(this T alignable)
where T : IAlignable
{
alignable.Alignment = Justify.Right;
return alignable;
}
}
}

View File

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Spectre.Console.Composition
namespace Spectre.Console.Rendering
{
/// <summary>
/// Represents a border used by tables.

View File

@ -1,4 +1,4 @@
namespace Spectre.Console.Composition
namespace Spectre.Console.Rendering
{
/// <summary>
/// Represents the different border parts.

View File

@ -1,6 +1,6 @@
using System;
namespace Spectre.Console.Composition
namespace Spectre.Console.Rendering
{
/// <summary>
/// Represents an old school ASCII border.

View File

@ -1,4 +1,4 @@
namespace Spectre.Console.Composition
namespace Spectre.Console.Rendering
{
/// <summary>
/// Represents an invisible border.

View File

@ -1,6 +1,6 @@
using System;
namespace Spectre.Console.Composition
namespace Spectre.Console.Rendering
{
/// <summary>
/// Represents a rounded border.

View File

@ -1,6 +1,6 @@
using System;
namespace Spectre.Console.Composition
namespace Spectre.Console.Rendering
{
/// <summary>
/// Represents a square border.

View File

@ -1,15 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Composition;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// Represents a grid.
/// A renderable grid.
/// </summary>
public sealed class Grid : IRenderable
public sealed class Grid : Renderable
{
private readonly Table _table;
@ -28,13 +28,13 @@ namespace Spectre.Console
}
/// <inheritdoc/>
public Measurement Measure(RenderContext context, int maxWidth)
protected override Measurement Measure(RenderContext context, int maxWidth)
{
return ((IRenderable)_table).Measure(context, maxWidth);
}
/// <inheritdoc/>
public IEnumerable<Segment> Render(RenderContext context, int width)
protected override IEnumerable<Segment> Render(RenderContext context, int width)
{
return ((IRenderable)_table).Render(context, width);
}
@ -109,8 +109,8 @@ namespace Spectre.Console
/// </summary>
public void AddEmptyRow()
{
var columns = new string[_table.ColumnCount];
Enumerable.Range(0, _table.ColumnCount).ForEach(index => columns[index] = string.Empty);
var columns = new IRenderable[_table.ColumnCount];
Enumerable.Range(0, _table.ColumnCount).ForEach(index => columns[index] = Text.Empty);
AddRow(columns);
}
@ -118,7 +118,7 @@ namespace Spectre.Console
/// Adds a new row to the grid.
/// </summary>
/// <param name="columns">The columns to add.</param>
public void AddRow(params string[] columns)
public void AddRow(params IRenderable[] columns)
{
if (columns is null)
{

View File

@ -3,7 +3,7 @@ namespace Spectre.Console
/// <summary>
/// Represents a grid column.
/// </summary>
public sealed class GridColumn
public sealed class GridColumn : IAlignable
{
/// <summary>
/// Gets or sets the width of the column.

View File

@ -0,0 +1,31 @@
using System;
using System.Linq;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="Grid"/>.
/// </summary>
public static class GridExtensions
{
/// <summary>
/// Adds a new row to the grid.
/// </summary>
/// <param name="grid">The grid to add the row to.</param>
/// <param name="columns">The columns to add.</param>
public static void AddRow(this Grid grid, params string[] columns)
{
if (grid is null)
{
throw new ArgumentNullException(nameof(grid));
}
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
grid.AddRow(columns.Select(column => new Markup(column)).ToArray());
}
}
}

View File

@ -0,0 +1,13 @@
namespace Spectre.Console
{
/// <summary>
/// Represents something that is alignable.
/// </summary>
public interface IAlignable
{
/// <summary>
/// Gets or sets the alignment.
/// </summary>
Justify? Alignment { get; set; }
}
}

View File

@ -1,6 +1,6 @@
using System.Collections.Generic;
namespace Spectre.Console.Composition
namespace Spectre.Console.Rendering
{
/// <summary>
/// Represents something that can be rendered to the console.

View File

@ -0,0 +1,43 @@
using System.Collections.Generic;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// A renderable piece of markup text.
/// </summary>
public sealed class Markup : Renderable, IAlignable
{
private readonly Text _text;
/// <inheritdoc/>
public Justify? Alignment
{
get => _text.Alignment;
set => _text.Alignment = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="Markup"/> class.
/// </summary>
/// <param name="text">The markup text.</param>
/// <param name="style">The style of the text.</param>
public Markup(string text, Style? style = null)
{
_text = MarkupParser.Parse(text, style);
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
return ((IRenderable)_text).Measure(context, maxWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
return ((IRenderable)_text).Render(context, maxWidth);
}
}
}

View File

@ -1,6 +1,6 @@
using System;
namespace Spectre.Console.Composition
namespace Spectre.Console.Rendering
{
/// <summary>
/// Represents a measurement.

View File

@ -1,13 +1,14 @@
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Composition;
using Spectre.Console.Rendering;
using SpectreBorder = Spectre.Console.Rendering.Border;
namespace Spectre.Console
{
/// <summary>
/// Represents a panel which contains another renderable item.
/// A renderable panel.
/// </summary>
public sealed class Panel : IRenderable
public sealed class Panel : Renderable
{
private const int EdgeWidth = 2;
@ -42,6 +43,15 @@ namespace Spectre.Console
/// </summary>
public Padding Padding { get; set; } = new Padding(1, 1);
/// <summary>
/// Initializes a new instance of the <see cref="Panel"/> class.
/// </summary>
/// <param name="text">The panel content.</param>
public Panel(string text)
: this(new Markup(text))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Panel"/> class.
/// </summary>
@ -52,23 +62,25 @@ namespace Spectre.Console
}
/// <inheritdoc/>
Measurement IRenderable.Measure(RenderContext context, int maxWidth)
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var childWidth = _child.Measure(context, maxWidth);
return new Measurement(childWidth.Min + 2 + Padding.GetHorizontalPadding(), childWidth.Max + 2 + Padding.GetHorizontalPadding());
return new Measurement(
childWidth.Min + 2 + Padding.GetHorizontalPadding(),
childWidth.Max + 2 + Padding.GetHorizontalPadding());
}
/// <inheritdoc/>
IEnumerable<Segment> IRenderable.Render(RenderContext context, int width)
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var border = Composition.Border.GetBorder(Border, (context.LegacyConsole || !context.Unicode) && SafeBorder);
var border = SpectreBorder.GetBorder(Border, (context.LegacyConsole || !context.Unicode) && SafeBorder);
var paddingWidth = Padding.GetHorizontalPadding();
var childWidth = width - EdgeWidth - paddingWidth;
var childWidth = maxWidth - EdgeWidth - paddingWidth;
if (!Expand)
{
var measurement = _child.Measure(context, width - EdgeWidth - paddingWidth);
var measurement = _child.Measure(context, maxWidth - EdgeWidth - paddingWidth);
childWidth = measurement.Max;
}

View File

@ -1,6 +1,6 @@
using System.Text;
namespace Spectre.Console.Composition
namespace Spectre.Console.Rendering
{
/// <summary>
/// Represents a render context.

View File

@ -0,0 +1,41 @@
using System.Collections.Generic;
namespace Spectre.Console.Rendering
{
/// <summary>
/// Base class for a renderable object implementing <see cref="IRenderable"/>.
/// </summary>
public abstract class Renderable : IRenderable
{
/// <inheritdoc/>
Measurement IRenderable.Measure(RenderContext context, int maxWidth)
{
return Measure(context, maxWidth);
}
/// <inheritdoc/>
IEnumerable<Segment> IRenderable.Render(RenderContext context, int maxWidth)
{
return Render(context, maxWidth);
}
/// <summary>
/// Measures the renderable object.
/// </summary>
/// <param name="context">The render context.</param>
/// <param name="maxWidth">The maximum allowed width.</param>
/// <returns>The minimum and maximum width of the object.</returns>
protected virtual Measurement Measure(RenderContext context, int maxWidth)
{
return new Measurement(maxWidth, maxWidth);
}
/// <summary>
/// Renders the object.
/// </summary>
/// <param name="context">The render context.</param>
/// <param name="maxWidth">The maximum allowed width.</param>
/// <returns>A collection of segments.</returns>
protected abstract IEnumerable<Segment> Render(RenderContext context, int maxWidth);
}
}

View File

@ -5,7 +5,7 @@ using System.Linq;
using System.Text;
using Spectre.Console.Internal;
namespace Spectre.Console.Composition
namespace Spectre.Console.Rendering
{
/// <summary>
/// Represents a renderable segment.

View File

@ -3,7 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
namespace Spectre.Console.Composition
namespace Spectre.Console.Rendering
{
/// <summary>
/// Represents a collection of segments.

View File

@ -1,7 +1,7 @@
using System.Collections;
using System.Collections.Generic;
namespace Spectre.Console.Composition
namespace Spectre.Console.Rendering
{
internal sealed class SegmentLineEnumerator : IEnumerable<Segment>
{

View File

@ -1,7 +1,7 @@
using System.Collections;
using System.Collections.Generic;
namespace Spectre.Console.Composition
namespace Spectre.Console.Rendering
{
internal sealed class SegmentLineIterator : IEnumerator<Segment>
{

View File

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Composition;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
@ -100,13 +100,13 @@ namespace Spectre.Console
var maxWidths = new List<int>();
// Include columns in measurement
var measure = ((IRenderable)column.Text).Measure(options, maxWidth);
var measure = column.Text.Measure(options, maxWidth);
minWidths.Add(measure.Min);
maxWidths.Add(measure.Max);
foreach (var row in rows)
{
measure = ((IRenderable)row).Measure(options, maxWidth);
measure = row.Measure(options, maxWidth);
minWidths.Add(measure.Min);
maxWidths.Add(measure.Max);
}

View File

@ -1,18 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Composition;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
using SpectreBorder = Spectre.Console.Rendering.Border;
namespace Spectre.Console
{
/// <summary>
/// Represents a table.
/// A renderable table.
/// </summary>
public sealed partial class Table : IRenderable
public sealed partial class Table : Renderable
{
private readonly List<TableColumn> _columns;
private readonly List<List<Text>> _rows;
private readonly List<List<IRenderable>> _rows;
/// <summary>
/// Gets the number of columns in the table.
@ -67,21 +68,7 @@ namespace Spectre.Console
public Table()
{
_columns = new List<TableColumn>();
_rows = new List<List<Text>>();
}
/// <summary>
/// Adds a column to the table.
/// </summary>
/// <param name="column">The column to add.</param>
public void AddColumn(string column)
{
if (column is null)
{
throw new ArgumentNullException(nameof(column));
}
AddColumn(new TableColumn(column));
_rows = new List<List<IRenderable>>();
}
/// <summary>
@ -103,23 +90,6 @@ namespace Spectre.Console
_columns.Add(column);
}
/// <summary>
/// Adds multiple columns to the table.
/// </summary>
/// <param name="columns">The columns to add.</param>
public void AddColumns(params string[] columns)
{
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
foreach (var column in columns)
{
AddColumn(column);
}
}
/// <summary>
/// Adds multiple columns to the table.
/// </summary>
@ -142,8 +112,8 @@ namespace Spectre.Console
/// </summary>
public void AddEmptyRow()
{
var columns = new string[ColumnCount];
Enumerable.Range(0, ColumnCount).ForEach(index => columns[index] = string.Empty);
var columns = new IRenderable[ColumnCount];
Enumerable.Range(0, ColumnCount).ForEach(index => columns[index] = Text.Empty);
AddRow(columns);
}
@ -151,7 +121,7 @@ namespace Spectre.Console
/// Adds a row to the table.
/// </summary>
/// <param name="columns">The row columns to add.</param>
public void AddRow(params string[] columns)
public void AddRow(params IRenderable[] columns)
{
if (columns is null)
{
@ -168,11 +138,11 @@ namespace Spectre.Console
throw new InvalidOperationException("The number of row columns are greater than the number of table columns.");
}
_rows.Add(columns.Select(column => Text.Markup(column)).ToList());
_rows.Add(columns.ToList());
}
/// <inheritdoc/>
Measurement IRenderable.Measure(RenderContext context, int maxWidth)
protected override Measurement Measure(RenderContext context, int maxWidth)
{
if (context is null)
{
@ -194,20 +164,20 @@ namespace Spectre.Console
}
/// <inheritdoc/>
IEnumerable<Segment> IRenderable.Render(RenderContext context, int width)
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var border = Composition.Border.GetBorder(Border, (context.LegacyConsole || !context.Unicode) && SafeBorder);
var border = SpectreBorder.GetBorder(Border, (context.LegacyConsole || !context.Unicode) && SafeBorder);
var tableWidth = maxWidth;
var showBorder = Border != BorderKind.None;
var hideBorder = Border == BorderKind.None;
var hasRows = _rows.Count > 0;
var maxWidth = width;
if (Width != null)
{
maxWidth = Math.Min(Width.Value, maxWidth);
@ -219,13 +189,13 @@ namespace Spectre.Console
var columnWidths = CalculateColumnWidths(context, maxWidth);
// Update the table width.
width = columnWidths.Sum() + GetExtraWidth(includePadding: true);
tableWidth = columnWidths.Sum() + GetExtraWidth(includePadding: true);
var rows = new List<List<Text>>();
var rows = new List<List<IRenderable>>();
if (ShowHeaders)
{
// Add columns to top of rows
rows.Add(new List<Text>(_columns.Select(c => c.Text)));
rows.Add(new List<IRenderable>(_columns.Select(c => c.Text)));
}
// Add rows.
@ -244,7 +214,7 @@ namespace Spectre.Console
var justification = _columns[columnIndex].Alignment;
var childContext = context.WithJustification(justification);
var lines = Segment.SplitLines(((IRenderable)cell).Render(childContext, rowWidth));
var lines = Segment.SplitLines(cell.Render(childContext, rowWidth));
cellHeight = Math.Max(cellHeight, lines.Count);
cells.Add(lines);
}

View File

@ -1,16 +1,17 @@
using System;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// Represents a table column.
/// </summary>
public sealed class TableColumn
public sealed class TableColumn : IAlignable
{
/// <summary>
/// Gets the text associated with the column.
/// </summary>
public Text Text { get; }
public IRenderable Text { get; }
/// <summary>
/// Gets or sets the width of the column.
@ -39,8 +40,17 @@ namespace Spectre.Console
/// </summary>
/// <param name="text">The table column text.</param>
public TableColumn(string text)
: this(new Markup(text))
{
Text = Text.Markup(text ?? throw new ArgumentNullException(nameof(text)));
}
/// <summary>
/// Initializes a new instance of the <see cref="TableColumn"/> class.
/// </summary>
/// <param name="renderable">The <see cref="IRenderable"/> instance to use as the table column.</param>
public TableColumn(IRenderable renderable)
{
Text = renderable ?? throw new ArgumentNullException(nameof(renderable));
Width = null;
Padding = new Padding(1, 1);
NoWrap = false;

View File

@ -0,0 +1,78 @@
using System;
using System.Linq;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="Table"/>.
/// </summary>
public static class TableExtensions
{
/// <summary>
/// Adds a column to the table.
/// </summary>
/// <param name="table">The table to add the column to.</param>
/// <param name="column">The column to add.</param>
/// <returns>The added <see cref="TableColumn"/> instance.</returns>
public static TableColumn AddColumn(this Table table, string column)
{
if (table is null)
{
throw new ArgumentNullException(nameof(table));
}
if (column is null)
{
throw new ArgumentNullException(nameof(column));
}
var tableColumn = new TableColumn(column);
table.AddColumn(tableColumn);
return tableColumn;
}
/// <summary>
/// Adds multiple columns to the table.
/// </summary>
/// <param name="table">The table to add the columns to.</param>
/// <param name="columns">The columns to add.</param>
public static void AddColumns(this Table table, params string[] columns)
{
if (table is null)
{
throw new ArgumentNullException(nameof(table));
}
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
foreach (var column in columns)
{
AddColumn(table, column);
}
}
/// <summary>
/// Adds a row to the table.
/// </summary>
/// <param name="table">The table to add the row to.</param>
/// <param name="columns">The row columns to add.</param>
public static void AddRow(this Table table, params string[] columns)
{
if (table is null)
{
throw new ArgumentNullException(nameof(table));
}
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
table.AddRow(columns.Select(column => new Markup(column)).ToArray());
}
}
}

View File

@ -3,24 +3,29 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Spectre.Console.Composition;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// Represents a piece of text.
/// A renderable piece of text.
/// </summary>
[DebuggerDisplay("{_text,nq}")]
[SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
public sealed class Text : IRenderable
public sealed class Text : Renderable, IAlignable
{
private readonly List<SegmentLine> _lines;
/// <summary>
/// Gets an empty <see cref="Text"/> instance.
/// </summary>
public static Text Empty { get; } = new Text(string.Empty);
/// <summary>
/// Gets or sets the text alignment.
/// </summary>
public Justify Alignment { get; set; } = Justify.Left;
public Justify? Alignment { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Text"/> class.
@ -60,7 +65,7 @@ namespace Spectre.Console
}
/// <inheritdoc/>
public Measurement Measure(RenderContext context, int maxWidth)
protected override Measurement Measure(RenderContext context, int maxWidth)
{
if (_lines.Count == 0)
{
@ -74,7 +79,7 @@ namespace Spectre.Console
}
/// <inheritdoc/>
public IEnumerable<Segment> Render(RenderContext context, int maxWidth)
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
if (context is null)
{
@ -89,7 +94,7 @@ namespace Spectre.Console
var lines = SplitLines(context, maxWidth);
// Justify lines
var justification = context.Justification ?? Alignment;
var justification = context.Justification ?? Alignment ?? Justify.Left;
foreach (var (_, _, last, line) in lines.Enumerate())
{
var length = line.Sum(l => l.StripLineEndings().CellLength(context.Encoding));