Add support for tables

This commit is contained in:
Patrik Svensson
2020-08-04 16:05:57 +02:00
committed by Patrik Svensson
parent aa34c145b9
commit a068fc68c3
19 changed files with 837 additions and 33 deletions

View File

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Globalization;
namespace Spectre.Console.Composition
{
/// <summary>
/// Represents a border used by tables.
/// </summary>
public abstract class Border
{
private readonly Dictionary<BorderPart, char> _lookup;
private static readonly Dictionary<BorderKind, Border> _borders = new Dictionary<BorderKind, Border>
{
{ BorderKind.Ascii, new AsciiBorder() },
{ BorderKind.Square, new SquareBorder() },
};
/// <summary>
/// Initializes a new instance of the <see cref="Border"/> class.
/// </summary>
protected Border()
{
_lookup = Initialize();
}
/// <summary>
/// Gets a <see cref="Border"/> represented by the specified <see cref="BorderKind"/>.
/// </summary>
/// <param name="kind">The kind of border to get.</param>
/// <returns>A <see cref="Border"/> instance representing the specified <see cref="BorderKind"/>.</returns>
public static Border GetBorder(BorderKind kind)
{
if (!_borders.TryGetValue(kind, out var border))
{
throw new InvalidOperationException("Unknown border kind");
}
return border;
}
private Dictionary<BorderPart, char> Initialize()
{
var lookup = new Dictionary<BorderPart, char>();
foreach (BorderPart part in Enum.GetValues(typeof(BorderPart)))
{
lookup.Add(part, GetBoxPart(part));
}
return lookup;
}
/// <summary>
/// Gets the string representation of a specific border part.
/// </summary>
/// <param name="part">The part to get a string representation for.</param>
/// <param name="count">The number of repetitions.</param>
/// <returns>A string representation of the specified border part.</returns>
public string GetPart(BorderPart part, int count)
{
return new string(GetBoxPart(part), count);
}
/// <summary>
/// Gets the string representation of a specific border part.
/// </summary>
/// <param name="part">The part to get a string representation for.</param>
/// <returns>A string representation of the specified border part.</returns>
public string GetPart(BorderPart part)
{
return _lookup[part].ToString(CultureInfo.InvariantCulture);
}
/// <summary>
/// Gets the character representing the specified border part.
/// </summary>
/// <param name="part">The part to get the character representation for.</param>
/// <returns>A character representation of the specified border part.</returns>
protected abstract char GetBoxPart(BorderPart part);
}
}

View File

@ -0,0 +1,18 @@
namespace Spectre.Console
{
/// <summary>
/// Represents different kinds of borders.
/// </summary>
public enum BorderKind
{
/// <summary>
/// A square border.
/// </summary>
Square = 0,
/// <summary>
/// An old school ASCII border.
/// </summary>
Ascii = 1,
}
}

View File

@ -0,0 +1,98 @@
namespace Spectre.Console.Composition
{
/// <summary>
/// Represents the different border parts.
/// </summary>
public enum BorderPart
{
/// <summary>
/// The top left part of a header.
/// </summary>
HeaderTopLeft,
/// <summary>
/// The top part of a header.
/// </summary>
HeaderTop,
/// <summary>
/// The top separator part of a header.
/// </summary>
HeaderTopSeparator,
/// <summary>
/// The top right part of a header.
/// </summary>
HeaderTopRight,
/// <summary>
/// The left part of a header.
/// </summary>
HeaderLeft,
/// <summary>
/// A header separator.
/// </summary>
HeaderSeparator,
/// <summary>
/// The right part of a header.
/// </summary>
HeaderRight,
/// <summary>
/// The bottom left part of a header.
/// </summary>
HeaderBottomLeft,
/// <summary>
/// The bottom part of a header.
/// </summary>
HeaderBottom,
/// <summary>
/// The bottom separator part of a header.
/// </summary>
HeaderBottomSeparator,
/// <summary>
/// The bottom right part of a header.
/// </summary>
HeaderBottomRight,
/// <summary>
/// The left part of a cell.
/// </summary>
CellLeft,
/// <summary>
/// A cell separator.
/// </summary>
CellSeparator,
/// <summary>
/// The right part of a cell.
/// </summary>
ColumnRight,
/// <summary>
/// The bottom left part of a footer.
/// </summary>
FooterBottomLeft,
/// <summary>
/// The bottom part of a footer.
/// </summary>
FooterBottom,
/// <summary>
/// The bottom separator part of a footer.
/// </summary>
FooterBottomSeparator,
/// <summary>
/// The bottom right part of a footer.
/// </summary>
FooterBottomRight,
}
}

View File

@ -0,0 +1,37 @@
using System;
namespace Spectre.Console.Composition
{
/// <summary>
/// Represents an old school ASCII border.
/// </summary>
public sealed class AsciiBorder : Border
{
/// <inheritdoc/>
protected override char GetBoxPart(BorderPart part)
{
return part switch
{
BorderPart.HeaderTopLeft => '+',
BorderPart.HeaderTop => '-',
BorderPart.HeaderTopSeparator => '-',
BorderPart.HeaderTopRight => '+',
BorderPart.HeaderLeft => '|',
BorderPart.HeaderSeparator => '|',
BorderPart.HeaderRight => '|',
BorderPart.HeaderBottomLeft => '|',
BorderPart.HeaderBottom => '-',
BorderPart.HeaderBottomSeparator => '+',
BorderPart.HeaderBottomRight => '|',
BorderPart.CellLeft => '|',
BorderPart.CellSeparator => '|',
BorderPart.ColumnRight => '|',
BorderPart.FooterBottomLeft => '+',
BorderPart.FooterBottom => '-',
BorderPart.FooterBottomSeparator => '-',
BorderPart.FooterBottomRight => '+',
_ => throw new InvalidOperationException("Unknown box part."),
};
}
}
}

View File

@ -0,0 +1,37 @@
using System;
namespace Spectre.Console.Composition
{
/// <summary>
/// Represents a square border.
/// </summary>
public sealed class SquareBorder : Border
{
/// <inheritdoc/>
protected override char GetBoxPart(BorderPart part)
{
return part switch
{
BorderPart.HeaderTopLeft => '┌',
BorderPart.HeaderTop => '─',
BorderPart.HeaderTopSeparator => '┬',
BorderPart.HeaderTopRight => '┐',
BorderPart.HeaderLeft => '│',
BorderPart.HeaderSeparator => '│',
BorderPart.HeaderRight => '│',
BorderPart.HeaderBottomLeft => '├',
BorderPart.HeaderBottom => '─',
BorderPart.HeaderBottomSeparator => '┼',
BorderPart.HeaderBottomRight => '┤',
BorderPart.CellLeft => '│',
BorderPart.CellSeparator => '│',
BorderPart.ColumnRight => '│',
BorderPart.FooterBottomLeft => '└',
BorderPart.FooterBottom => '─',
BorderPart.FooterBottomSeparator => '┴',
BorderPart.FooterBottomRight => '┘',
_ => throw new InvalidOperationException("Unknown box part."),
};
}
}
}

View File

@ -0,0 +1,262 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Spectre.Console.Composition;
using Spectre.Console.Internal;
namespace Spectre.Console
{
/// <summary>
/// Represents a table.
/// </summary>
public sealed class Table : IRenderable
{
private readonly List<Text> _columns;
private readonly List<List<Text>> _rows;
private readonly Border _border;
/// <summary>
/// Initializes a new instance of the <see cref="Table"/> class.
/// </summary>
/// <param name="border">The border to use.</param>
public Table(BorderKind border = BorderKind.Square)
{
_columns = new List<Text>();
_rows = new List<List<Text>>();
_border = Border.GetBorder(border);
}
/// <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));
}
_columns.Add(Text.New(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));
}
_columns.AddRange(columns.Select(column => Text.New(column)));
}
/// <summary>
/// Adds a row to the table.
/// </summary>
/// <param name="columns">The row columns to add.</param>
public void AddRow(params string[] columns)
{
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
if (columns.Length < _columns.Count)
{
throw new InvalidOperationException("The number of row columns are less than the number of table columns.");
}
if (columns.Length > _columns.Count)
{
throw new InvalidOperationException("The number of row columns are greater than the number of table columns.");
}
_rows.Add(columns.Select(column => Text.New(column)).ToList());
}
/// <inheritdoc/>
public int Measure(Encoding encoding, int maxWidth)
{
// Calculate the max width for each column.
var maxColumnWidth = (maxWidth - (2 + (_columns.Count * 2) + (_columns.Count - 1))) / _columns.Count;
var columnWidths = _columns.Select(c => c.Measure(encoding, maxColumnWidth)).ToArray();
for (var rowIndex = 0; rowIndex < _rows.Count; rowIndex++)
{
for (var columnIndex = 0; columnIndex < _rows[rowIndex].Count; columnIndex++)
{
var columnWidth = _rows[rowIndex][columnIndex].Measure(encoding, maxColumnWidth);
if (columnWidth > columnWidths[columnIndex])
{
columnWidths[columnIndex] = columnWidth;
}
}
}
// We now know the max width of each column, so let's recalculate the width.
return columnWidths.Sum() + 2 + (_columns.Count * 2) + (_columns.Count - 1);
}
/// <inheritdoc/>
public IEnumerable<Segment> Render(Encoding encoding, int width)
{
// Calculate the max width for each column.
var maxColumnWidth = (width - (2 + (_columns.Count * 2) + (_columns.Count - 1))) / _columns.Count;
var columnWidths = _columns.Select(c => c.Measure(encoding, maxColumnWidth)).ToArray();
for (var rowIndex = 0; rowIndex < _rows.Count; rowIndex++)
{
for (var columnIndex = 0; columnIndex < _rows[rowIndex].Count; columnIndex++)
{
var columnWidth = _rows[rowIndex][columnIndex].Measure(encoding, maxColumnWidth);
if (columnWidth > columnWidths[columnIndex])
{
columnWidths[columnIndex] = columnWidth;
}
}
}
// We now know the max width of each column, so let's recalculate the width.
width = columnWidths.Sum() + 2 + (_columns.Count * 2) + (_columns.Count - 1);
// Create the rows.
var rows = new List<List<Text>>();
rows.Add(new List<Text>(_columns));
rows.AddRange(_rows);
// Iterate all rows.
var result = new List<Segment>();
foreach (var (index, firstRow, lastRow, row) in rows.Enumerate())
{
var cellHeight = 1;
// Get the list of cells for the row.
var cells = new List<List<SegmentLine>>();
foreach (var (rowWidth, cell) in columnWidths.Zip(row, (f, s) => (f, s)))
{
var lines = Segment.SplitLines(cell.Render(encoding, rowWidth));
cellHeight = Math.Max(cellHeight, lines.Count);
cells.Add(lines);
}
if (firstRow)
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopLeft)));
foreach (var (columnIndex, _, lastColumn, columnWidth) in columnWidths.Enumerate())
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop))); // Left padding
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop, columnWidth)));
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop))); // Right padding
if (!lastColumn)
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopSeparator)));
}
}
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopRight)));
result.Add(Segment.LineBreak());
}
// Iterate through each cell row
foreach (var cellRowIndex in Enumerable.Range(0, cellHeight))
{
// Make cells the same shape
MakeSameHeight(cellHeight, cells);
var w00t = cells.Enumerate().ToArray();
foreach (var (cellIndex, firstCell, lastCell, cell) in w00t)
{
if (firstCell)
{
result.Add(new Segment(_border.GetPart(BorderPart.CellLeft)));
}
result.Add(new Segment(" "));
result.AddRange(cell[cellRowIndex]);
// Pad cell right
var length = cell[cellRowIndex].Sum(segment => segment.CellLength(encoding));
if (length < columnWidths[cellIndex])
{
result.Add(new Segment(new string(' ', columnWidths[cellIndex] - length)));
}
result.Add(new Segment(" "));
if (lastCell)
{
// Separator
result.Add(new Segment(_border.GetPart(BorderPart.ColumnRight)));
}
else
{
// Separator
result.Add(new Segment(_border.GetPart(BorderPart.CellSeparator)));
}
}
result.Add(Segment.LineBreak());
}
if (firstRow)
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomLeft)));
foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom))); // Left padding
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom, columnWidth)));
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom))); // Right padding
if (!lastColumn)
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomSeparator)));
}
}
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomRight)));
result.Add(Segment.LineBreak());
}
if (lastRow)
{
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomLeft)));
foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
{
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom)));
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom, columnWidth)));
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom)));
if (!lastColumn)
{
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomSeparator)));
}
}
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomRight)));
result.Add(Segment.LineBreak());
}
}
return result;
}
private static void MakeSameHeight(int cellHeight, List<List<SegmentLine>> cells)
{
foreach (var cell in cells)
{
if (cell.Count < cellHeight)
{
while (cell.Count != cellHeight)
{
cell.Add(new SegmentLine());
}
}
}
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
@ -12,6 +13,7 @@ namespace Spectre.Console
/// Represents text with color and decorations.
/// </summary>
[SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
[DebuggerDisplay("{_text,nq}")]
public sealed class Text : IRenderable
{
private readonly List<Span> _spans;
@ -98,15 +100,24 @@ namespace Spectre.Console
/// <inheritdoc/>
public int Measure(Encoding encoding, int maxWidth)
{
var lines = _text.SplitLines();
return lines.Max(x => x.CellLength(encoding));
var lines = Segment.SplitLines(Render(encoding, maxWidth));
if (lines.Count == 0)
{
return 0;
}
return lines.Max(x => x.Length);
}
/// <inheritdoc/>
public IEnumerable<Segment> Render(Encoding encoding, int width)
{
var result = new List<Segment>();
if (string.IsNullOrWhiteSpace(_text))
{
return Array.Empty<Segment>();
}
var result = new List<Segment>();
var segments = SplitLineBreaks(CreateSegments());
foreach (var (_, _, last, line) in Segment.SplitLines(segments, width).Enumerate())