mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-16 00:42:51 +08:00
Clean up table rendering a bit
This commit is contained in:
parent
c6210f75ca
commit
179e243214
@ -0,0 +1,18 @@
|
||||
<ProjectConfiguration>
|
||||
<Settings>
|
||||
<IgnoredTests>
|
||||
<NamedTestSelector>
|
||||
<TestName>Spectre.Console.Tests.Unit.Cli.CommandAppTests+Parsing+UnknownCommand.Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Root_Command_Is_Unknown_And_Distance_Is_Small</TestName>
|
||||
</NamedTestSelector>
|
||||
<NamedTestSelector>
|
||||
<TestName>Spectre.Console.Tests.Unit.Cli.CommandAppTests+Parsing+UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small</TestName>
|
||||
</NamedTestSelector>
|
||||
<NamedTestSelector>
|
||||
<TestName>Spectre.Console.Tests.Unit.Cli.CommandAppTests+Parsing+UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Root_Command_After_Argument_Is_Unknown_And_Distance_Is_Small</TestName>
|
||||
</NamedTestSelector>
|
||||
<NamedTestSelector>
|
||||
<TestName>Spectre.Console.Tests.Unit.Cli.CommandAppTests+Parsing+UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Root_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small</TestName>
|
||||
</NamedTestSelector>
|
||||
</IgnoredTests>
|
||||
</Settings>
|
||||
</ProjectConfiguration>
|
@ -6,6 +6,23 @@ namespace Spectre.Console.Internal
|
||||
{
|
||||
internal static class EnumerableExtensions
|
||||
{
|
||||
public static int IndexOf<T>(this IEnumerable<T> source, T item)
|
||||
where T : class
|
||||
{
|
||||
var index = 0;
|
||||
foreach (var candidate in source)
|
||||
{
|
||||
if (candidate == item)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static int GetCount<T>(this IEnumerable<T> source)
|
||||
{
|
||||
if (source is IList<T> list)
|
||||
|
@ -1,501 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Internal;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// A renderable table.
|
||||
/// </summary>
|
||||
public sealed class Table : Renderable, IHasTableBorder, IExpandable, IAlignable
|
||||
{
|
||||
private const int EdgeCount = 2;
|
||||
|
||||
private readonly List<TableColumn> _columns;
|
||||
private readonly List<TableRow> _rows;
|
||||
|
||||
private static readonly Style _defaultHeadingStyle = new Style(Color.Silver);
|
||||
private static readonly Style _defaultCaptionStyle = new Style(Color.Grey);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the table columns.
|
||||
/// </summary>
|
||||
public IReadOnlyList<TableColumn> Columns => _columns;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the table rows.
|
||||
/// </summary>
|
||||
public IReadOnlyList<TableRow> Rows => _rows;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TableBorder Border { get; set; } = TableBorder.Square;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Style? BorderStyle { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool UseSafeBorder { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not table headers should be shown.
|
||||
/// </summary>
|
||||
public bool ShowHeaders { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not table footers should be shown.
|
||||
/// </summary>
|
||||
public bool ShowFooters { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not the table should
|
||||
/// fit the available space. If <c>false</c>, the table width will be
|
||||
/// auto calculated. Defaults to <c>false</c>.
|
||||
/// </summary>
|
||||
public bool Expand { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width of the table.
|
||||
/// </summary>
|
||||
public int? Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the table title.
|
||||
/// </summary>
|
||||
public TableTitle? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the table footnote.
|
||||
/// </summary>
|
||||
public TableTitle? Caption { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Justify? Alignment { get; set; }
|
||||
|
||||
// Whether this is a grid or not.
|
||||
internal bool IsGrid { get; set; }
|
||||
|
||||
// Whether or not the most right cell should be padded.
|
||||
// This is almost always the case, unless we're rendering
|
||||
// a grid without explicit padding in the last cell.
|
||||
internal bool PadRightCell { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Table"/> class.
|
||||
/// </summary>
|
||||
public Table()
|
||||
{
|
||||
_columns = new List<TableColumn>();
|
||||
_rows = new List<TableRow>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a column to the table.
|
||||
/// </summary>
|
||||
/// <param name="column">The column to add.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public Table AddColumn(TableColumn column)
|
||||
{
|
||||
if (column is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(column));
|
||||
}
|
||||
|
||||
if (_rows.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot add new columns to table with existing rows.");
|
||||
}
|
||||
|
||||
_columns.Add(column);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a row to the table.
|
||||
/// </summary>
|
||||
/// <param name="columns">The row columns to add.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public Table AddRow(IEnumerable<IRenderable> columns)
|
||||
{
|
||||
if (columns is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(columns));
|
||||
}
|
||||
|
||||
var rowColumnCount = columns.GetCount();
|
||||
if (rowColumnCount > _columns.Count)
|
||||
{
|
||||
throw new InvalidOperationException("The number of row columns are greater than the number of table columns.");
|
||||
}
|
||||
|
||||
_rows.Add(new TableRow(columns));
|
||||
|
||||
// Need to add missing columns?
|
||||
if (rowColumnCount < _columns.Count)
|
||||
{
|
||||
var diff = _columns.Count - rowColumnCount;
|
||||
Enumerable.Range(0, diff).ForEach(_ => _rows.Last().Add(Text.Empty));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Measurement Measure(RenderContext context, int maxWidth)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (Width != null)
|
||||
{
|
||||
maxWidth = Math.Min(Width.Value, maxWidth);
|
||||
}
|
||||
|
||||
maxWidth -= GetExtraWidth(includePadding: true);
|
||||
|
||||
var measurements = _columns.Select(column => MeasureColumn(column, context, maxWidth)).ToList();
|
||||
var min = measurements.Sum(x => x.Min) + GetExtraWidth(includePadding: true);
|
||||
var max = Width ?? measurements.Sum(x => x.Max) + GetExtraWidth(includePadding: true);
|
||||
|
||||
return new Measurement(min, max);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var border = Border.GetSafeBorder((context.LegacyConsole || !context.Unicode) && UseSafeBorder);
|
||||
var borderStyle = BorderStyle ?? Style.Plain;
|
||||
|
||||
var tableWidth = maxWidth;
|
||||
var actualMaxWidth = maxWidth;
|
||||
|
||||
var showBorder = Border.Visible;
|
||||
var hideBorder = !Border.Visible;
|
||||
var hasRows = _rows.Count > 0;
|
||||
var hasFooters = _columns.Any(c => c.Footer != null);
|
||||
|
||||
if (Width != null)
|
||||
{
|
||||
maxWidth = Math.Min(Width.Value, maxWidth);
|
||||
}
|
||||
|
||||
maxWidth -= GetExtraWidth(includePadding: true);
|
||||
|
||||
// Calculate the column and table widths
|
||||
var columnWidths = CalculateColumnWidths(context, maxWidth);
|
||||
|
||||
// Update the table width.
|
||||
tableWidth = columnWidths.Sum() + GetExtraWidth(includePadding: true);
|
||||
if (tableWidth <= 0 || tableWidth > actualMaxWidth || columnWidths.Any(c => c <= 0))
|
||||
{
|
||||
return new List<Segment>(new[] { new Segment("…", BorderStyle ?? Style.Plain) });
|
||||
}
|
||||
|
||||
var rows = new List<TableRow>();
|
||||
if (ShowHeaders)
|
||||
{
|
||||
// Add columns to top of rows
|
||||
rows.Add(new TableRow(new List<IRenderable>(_columns.Select(c => c.Header))));
|
||||
}
|
||||
|
||||
// Add rows.
|
||||
rows.AddRange(_rows);
|
||||
|
||||
if (hasFooters)
|
||||
{
|
||||
rows.Add(new TableRow(new List<IRenderable>(_columns.Select(c => c.Footer ?? Text.Empty))));
|
||||
}
|
||||
|
||||
var result = new List<Segment>();
|
||||
result.AddRange(RenderAnnotation(context, Title, actualMaxWidth, tableWidth, _defaultHeadingStyle));
|
||||
|
||||
// Iterate all rows
|
||||
foreach (var (index, firstRow, lastRow, row) in rows.Enumerate())
|
||||
{
|
||||
var cellHeight = 1;
|
||||
|
||||
// Get the list of cells for the row and calculate the cell height
|
||||
var cells = new List<List<SegmentLine>>();
|
||||
foreach (var (columnIndex, _, _, (rowWidth, cell)) in columnWidths.Zip(row).Enumerate())
|
||||
{
|
||||
var justification = _columns[columnIndex].Alignment;
|
||||
var childContext = context.WithJustification(justification);
|
||||
|
||||
var lines = Segment.SplitLines(context, cell.Render(childContext, rowWidth));
|
||||
cellHeight = Math.Max(cellHeight, lines.Count);
|
||||
cells.Add(lines);
|
||||
}
|
||||
|
||||
// Show top of header?
|
||||
if (firstRow && showBorder)
|
||||
{
|
||||
var separator = Aligner.Align(context, border.GetColumnRow(TablePart.Top, columnWidths, _columns), Alignment, actualMaxWidth);
|
||||
result.Add(new Segment(separator, borderStyle));
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
|
||||
// Show footer separator?
|
||||
if (ShowFooters && lastRow && showBorder && hasFooters)
|
||||
{
|
||||
var textBorder = border.GetColumnRow(TablePart.FooterSeparator, columnWidths, _columns);
|
||||
if (!string.IsNullOrEmpty(textBorder))
|
||||
{
|
||||
var separator = Aligner.Align(context, textBorder, Alignment, actualMaxWidth);
|
||||
result.Add(new Segment(separator, borderStyle));
|
||||
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))
|
||||
{
|
||||
var rowResult = new List<Segment>();
|
||||
|
||||
foreach (var (cellIndex, firstCell, lastCell, cell) in cells.Enumerate())
|
||||
{
|
||||
if (firstCell && showBorder)
|
||||
{
|
||||
// Show left column edge
|
||||
var part = firstRow && ShowHeaders ? TableBorderPart.HeaderLeft : TableBorderPart.CellLeft;
|
||||
rowResult.Add(new Segment(border.GetPart(part), borderStyle));
|
||||
}
|
||||
|
||||
// Pad column on left side.
|
||||
if (showBorder || IsGrid)
|
||||
{
|
||||
var leftPadding = _columns[cellIndex].Padding.GetLeftSafe();
|
||||
if (leftPadding > 0)
|
||||
{
|
||||
rowResult.Add(new Segment(new string(' ', leftPadding)));
|
||||
}
|
||||
}
|
||||
|
||||
// Add content
|
||||
rowResult.AddRange(cell[cellRowIndex]);
|
||||
|
||||
// Pad cell content right
|
||||
var length = cell[cellRowIndex].Sum(segment => segment.CellCount(context));
|
||||
if (length < columnWidths[cellIndex])
|
||||
{
|
||||
rowResult.Add(new Segment(new string(' ', columnWidths[cellIndex] - length)));
|
||||
}
|
||||
|
||||
// Pad column on the right side
|
||||
if (showBorder || (hideBorder && !lastCell) || (hideBorder && lastCell && IsGrid && PadRightCell))
|
||||
{
|
||||
var rightPadding = _columns[cellIndex].Padding.GetRightSafe();
|
||||
if (rightPadding > 0)
|
||||
{
|
||||
rowResult.Add(new Segment(new string(' ', rightPadding)));
|
||||
}
|
||||
}
|
||||
|
||||
if (lastCell && showBorder)
|
||||
{
|
||||
// Add right column edge
|
||||
var part = firstRow && ShowHeaders ? TableBorderPart.HeaderRight : TableBorderPart.CellRight;
|
||||
rowResult.Add(new Segment(border.GetPart(part), borderStyle));
|
||||
}
|
||||
else if (showBorder)
|
||||
{
|
||||
// Add column separator
|
||||
var part = firstRow && ShowHeaders ? TableBorderPart.HeaderSeparator : TableBorderPart.CellSeparator;
|
||||
rowResult.Add(new Segment(border.GetPart(part), borderStyle));
|
||||
}
|
||||
}
|
||||
|
||||
// Align the row result.
|
||||
Aligner.Align(context, rowResult, Alignment, actualMaxWidth);
|
||||
|
||||
// Is the row larger than the allowed max width?
|
||||
if (Segment.CellCount(context, rowResult) > actualMaxWidth)
|
||||
{
|
||||
result.AddRange(Segment.Truncate(context, rowResult, actualMaxWidth));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.AddRange(rowResult);
|
||||
}
|
||||
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
|
||||
// Show header separator?
|
||||
if (firstRow && showBorder && ShowHeaders && hasRows)
|
||||
{
|
||||
var separator = Aligner.Align(context, border.GetColumnRow(TablePart.HeaderSeparator, columnWidths, _columns), Alignment, actualMaxWidth);
|
||||
result.Add(new Segment(separator, borderStyle));
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
|
||||
// Show bottom of footer?
|
||||
if (lastRow && showBorder)
|
||||
{
|
||||
var separator = Aligner.Align(context, border.GetColumnRow(TablePart.Bottom, columnWidths, _columns), Alignment, actualMaxWidth);
|
||||
result.Add(new Segment(separator, borderStyle));
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
}
|
||||
|
||||
result.AddRange(RenderAnnotation(context, Caption, actualMaxWidth, tableWidth, _defaultCaptionStyle));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Calculate the widths of each column, including padding, not including borders.
|
||||
// Ported from Rich by Will McGugan, licensed under MIT.
|
||||
// https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/table.py#L394
|
||||
private List<int> CalculateColumnWidths(RenderContext options, int maxWidth)
|
||||
{
|
||||
var width_ranges = _columns.Select(column => MeasureColumn(column, options, maxWidth)).ToArray();
|
||||
var widths = width_ranges.Select(range => range.Max).ToList();
|
||||
|
||||
var tableWidth = widths.Sum();
|
||||
if (tableWidth > maxWidth)
|
||||
{
|
||||
var wrappable = _columns.Select(c => !c.NoWrap).ToList();
|
||||
widths = CollapseWidths(widths, wrappable, maxWidth);
|
||||
tableWidth = widths.Sum();
|
||||
|
||||
// last resort, reduce columns evenly
|
||||
if (tableWidth > maxWidth)
|
||||
{
|
||||
var excessWidth = tableWidth - maxWidth;
|
||||
widths = Ratio.Reduce(excessWidth, widths.Select(_ => 1).ToList(), widths, widths);
|
||||
tableWidth = widths.Sum();
|
||||
}
|
||||
}
|
||||
|
||||
if (tableWidth < maxWidth && ShouldExpand())
|
||||
{
|
||||
var padWidths = Ratio.Distribute(maxWidth - tableWidth, widths);
|
||||
widths = widths.Zip(padWidths, (a, b) => (a, b)).Select(f => f.a + f.b).ToList();
|
||||
}
|
||||
|
||||
return widths;
|
||||
}
|
||||
|
||||
// Reduce widths so that the total is less or equal to the max width.
|
||||
// Ported from Rich by Will McGugan, licensed under MIT.
|
||||
// https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/table.py#L442
|
||||
private static List<int> CollapseWidths(List<int> widths, List<bool> wrappable, int maxWidth)
|
||||
{
|
||||
var totalWidth = widths.Sum();
|
||||
var excessWidth = totalWidth - maxWidth;
|
||||
|
||||
if (wrappable.AnyTrue())
|
||||
{
|
||||
while (totalWidth != 0 && excessWidth > 0)
|
||||
{
|
||||
var maxColumn = widths.Zip(wrappable, (first, second) => (width: first, allowWrap: second))
|
||||
.Where(x => x.allowWrap)
|
||||
.Max(x => x.width);
|
||||
|
||||
var secondMaxColumn = widths.Zip(wrappable, (width, allowWrap) => allowWrap && width != maxColumn ? width : 1).Max();
|
||||
var columnDifference = maxColumn - secondMaxColumn;
|
||||
|
||||
var ratios = widths.Zip(wrappable, (width, allowWrap) => width == maxColumn && allowWrap ? 1 : 0).ToList();
|
||||
if (!ratios.Any(x => x != 0) || columnDifference == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var maxReduce = widths.Select(_ => Math.Min(excessWidth, columnDifference)).ToList();
|
||||
widths = Ratio.Reduce(excessWidth, ratios, maxReduce, widths);
|
||||
|
||||
totalWidth = widths.Sum();
|
||||
excessWidth = totalWidth - maxWidth;
|
||||
}
|
||||
}
|
||||
|
||||
return widths;
|
||||
}
|
||||
|
||||
private IEnumerable<Segment> RenderAnnotation(
|
||||
RenderContext context, TableTitle? header,
|
||||
int maxWidth, int tableWidth, Style defaultStyle)
|
||||
{
|
||||
if (header == null)
|
||||
{
|
||||
return Array.Empty<Segment>();
|
||||
}
|
||||
|
||||
var paragraph = new Markup(header.Text.Capitalize(), header.Style ?? defaultStyle)
|
||||
.Alignment(Justify.Center)
|
||||
.Overflow(Overflow.Ellipsis);
|
||||
|
||||
var items = new List<Segment>();
|
||||
items.AddRange(((IRenderable)paragraph).Render(context, tableWidth));
|
||||
|
||||
// Align over the whole buffer area
|
||||
Aligner.Align(context, items, Alignment, maxWidth);
|
||||
|
||||
items.Add(Segment.LineBreak);
|
||||
return items;
|
||||
}
|
||||
|
||||
private (int Min, int Max) MeasureColumn(TableColumn column, RenderContext options, int maxWidth)
|
||||
{
|
||||
// Predetermined width?
|
||||
if (column.Width != null)
|
||||
{
|
||||
return (column.Width.Value, column.Width.Value);
|
||||
}
|
||||
|
||||
var columnIndex = _columns.IndexOf(column);
|
||||
var rows = _rows.Select(row => row[columnIndex]);
|
||||
|
||||
var minWidths = new List<int>();
|
||||
var maxWidths = new List<int>();
|
||||
|
||||
// Include columns (both header and footer) in measurement
|
||||
var headerMeasure = column.Header.Measure(options, maxWidth);
|
||||
var footerMeasure = column.Footer?.Measure(options, maxWidth) ?? headerMeasure;
|
||||
minWidths.Add(Math.Min(headerMeasure.Min, footerMeasure.Min));
|
||||
maxWidths.Add(Math.Max(headerMeasure.Max, footerMeasure.Max));
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var rowMeasure = row.Measure(options, maxWidth);
|
||||
minWidths.Add(rowMeasure.Min);
|
||||
maxWidths.Add(rowMeasure.Max);
|
||||
}
|
||||
|
||||
var padding = column.Padding?.GetWidth() ?? 0;
|
||||
|
||||
return (minWidths.Count > 0 ? minWidths.Max() : padding,
|
||||
maxWidths.Count > 0 ? maxWidths.Max() : maxWidth);
|
||||
}
|
||||
|
||||
private int GetExtraWidth(bool includePadding)
|
||||
{
|
||||
var hideBorder = !Border.Visible;
|
||||
var separators = hideBorder ? 0 : _columns.Count - 1;
|
||||
var edges = hideBorder ? 0 : EdgeCount;
|
||||
var padding = includePadding ? _columns.Select(x => x.Padding?.GetWidth() ?? 0).Sum() : 0;
|
||||
|
||||
if (!PadRightCell)
|
||||
{
|
||||
padding -= _columns.Last().Padding.GetRightSafe();
|
||||
}
|
||||
|
||||
return separators + edges + padding;
|
||||
}
|
||||
|
||||
private bool ShouldExpand()
|
||||
{
|
||||
return Expand || Width != null;
|
||||
}
|
||||
}
|
||||
}
|
205
src/Spectre.Console/Widgets/Table/Table.cs
Normal file
205
src/Spectre.Console/Widgets/Table/Table.cs
Normal file
@ -0,0 +1,205 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Internal;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// A renderable table.
|
||||
/// </summary>
|
||||
public sealed class Table : Renderable, IHasTableBorder, IExpandable, IAlignable
|
||||
{
|
||||
private readonly List<TableColumn> _columns;
|
||||
private readonly List<TableRow> _rows;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the table columns.
|
||||
/// </summary>
|
||||
public IReadOnlyList<TableColumn> Columns => _columns;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the table rows.
|
||||
/// </summary>
|
||||
public IReadOnlyList<TableRow> Rows => _rows;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TableBorder Border { get; set; } = TableBorder.Square;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Style? BorderStyle { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool UseSafeBorder { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not table headers should be shown.
|
||||
/// </summary>
|
||||
public bool ShowHeaders { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not table footers should be shown.
|
||||
/// </summary>
|
||||
public bool ShowFooters { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not the table should
|
||||
/// fit the available space. If <c>false</c>, the table width will be
|
||||
/// auto calculated. Defaults to <c>false</c>.
|
||||
/// </summary>
|
||||
public bool Expand { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width of the table.
|
||||
/// </summary>
|
||||
public int? Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the table title.
|
||||
/// </summary>
|
||||
public TableTitle? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the table footnote.
|
||||
/// </summary>
|
||||
public TableTitle? Caption { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Justify? Alignment { get; set; }
|
||||
|
||||
// Whether this is a grid or not.
|
||||
internal bool IsGrid { get; set; }
|
||||
|
||||
// Whether or not the most right cell should be padded.
|
||||
// This is almost always the case, unless we're rendering
|
||||
// a grid without explicit padding in the last cell.
|
||||
internal bool PadRightCell { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Table"/> class.
|
||||
/// </summary>
|
||||
public Table()
|
||||
{
|
||||
_columns = new List<TableColumn>();
|
||||
_rows = new List<TableRow>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a column to the table.
|
||||
/// </summary>
|
||||
/// <param name="column">The column to add.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public Table AddColumn(TableColumn column)
|
||||
{
|
||||
if (column is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(column));
|
||||
}
|
||||
|
||||
if (_rows.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot add new columns to table with existing rows.");
|
||||
}
|
||||
|
||||
_columns.Add(column);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a row to the table.
|
||||
/// </summary>
|
||||
/// <param name="columns">The row columns to add.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public Table AddRow(IEnumerable<IRenderable> columns)
|
||||
{
|
||||
if (columns is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(columns));
|
||||
}
|
||||
|
||||
var rowColumnCount = columns.GetCount();
|
||||
if (rowColumnCount > _columns.Count)
|
||||
{
|
||||
throw new InvalidOperationException("The number of row columns are greater than the number of table columns.");
|
||||
}
|
||||
|
||||
_rows.Add(new TableRow(columns));
|
||||
|
||||
// Need to add missing columns?
|
||||
if (rowColumnCount < _columns.Count)
|
||||
{
|
||||
var diff = _columns.Count - rowColumnCount;
|
||||
Enumerable.Range(0, diff).ForEach(_ => _rows.Last().Add(Text.Empty));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Measurement Measure(RenderContext context, int maxWidth)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var measurer = new TableMeasurer(this, context);
|
||||
|
||||
// Calculate the total cell width
|
||||
var totalCellWidth = measurer.CalculateTotalCellWidth(maxWidth);
|
||||
|
||||
// Calculate the minimum and maximum table width
|
||||
var measurements = _columns.Select(column => measurer.MeasureColumn(column, totalCellWidth));
|
||||
var minTableWidth = measurements.Sum(x => x.Min) + measurer.GetNonColumnWidth();
|
||||
var maxTableWidth = Width ?? measurements.Sum(x => x.Max) + measurer.GetNonColumnWidth();
|
||||
return new Measurement(minTableWidth, maxTableWidth);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var measurer = new TableMeasurer(this, context);
|
||||
|
||||
// Calculate the column and table width
|
||||
var totalCellWidth = measurer.CalculateTotalCellWidth(maxWidth);
|
||||
var columnWidths = measurer.CalculateColumnWidths(totalCellWidth);
|
||||
var tableWidth = columnWidths.Sum() + measurer.GetNonColumnWidth();
|
||||
|
||||
// Get the rows to render
|
||||
var rows = GetRenderableRows();
|
||||
|
||||
// Render the table
|
||||
return TableRenderer.Render(
|
||||
new TableRendererContext(this, context, rows, tableWidth, maxWidth),
|
||||
columnWidths);
|
||||
}
|
||||
|
||||
private List<TableRow> GetRenderableRows()
|
||||
{
|
||||
var rows = new List<TableRow>();
|
||||
|
||||
// Show headers?
|
||||
if (ShowHeaders)
|
||||
{
|
||||
rows.Add(TableRow.Header(_columns.Select(c => c.Header)));
|
||||
}
|
||||
|
||||
// Add rows
|
||||
rows.AddRange(_rows);
|
||||
|
||||
// Show footers?
|
||||
if (ShowFooters && _columns.Any(c => c.Footer != null))
|
||||
{
|
||||
rows.Add(TableRow.Footer(_columns.Select(c => c.Footer ?? Text.Empty)));
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
}
|
22
src/Spectre.Console/Widgets/Table/TableAccessor.cs
Normal file
22
src/Spectre.Console/Widgets/Table/TableAccessor.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
internal abstract class TableAccessor
|
||||
{
|
||||
private readonly Table _table;
|
||||
|
||||
public RenderContext Options { get; }
|
||||
public IReadOnlyList<TableColumn> Columns => _table.Columns;
|
||||
public virtual IReadOnlyList<TableRow> Rows => _table.Rows;
|
||||
public bool Expand => _table.Expand || _table.Width != null;
|
||||
|
||||
protected TableAccessor(Table table, RenderContext options)
|
||||
{
|
||||
_table = table ?? throw new ArgumentNullException(nameof(table));
|
||||
Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
}
|
||||
}
|
161
src/Spectre.Console/Widgets/Table/TableMeasurer.cs
Normal file
161
src/Spectre.Console/Widgets/Table/TableMeasurer.cs
Normal file
@ -0,0 +1,161 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Internal;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
internal sealed class TableMeasurer : TableAccessor
|
||||
{
|
||||
private const int EdgeCount = 2;
|
||||
|
||||
private readonly int? _explicitWidth;
|
||||
private readonly TableBorder _border;
|
||||
private readonly bool _padRightCell;
|
||||
|
||||
public TableMeasurer(Table table, RenderContext options)
|
||||
: base(table, options)
|
||||
{
|
||||
_explicitWidth = table.Width;
|
||||
_border = table.Border;
|
||||
_padRightCell = table.PadRightCell;
|
||||
}
|
||||
|
||||
public int CalculateTotalCellWidth(int maxWidth)
|
||||
{
|
||||
var totalCellWidth = maxWidth;
|
||||
if (_explicitWidth != null)
|
||||
{
|
||||
totalCellWidth = Math.Min(_explicitWidth.Value, maxWidth);
|
||||
}
|
||||
|
||||
return totalCellWidth - GetNonColumnWidth();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the width of everything that's not a cell.
|
||||
/// That means separators, edges and padding.
|
||||
/// </summary>
|
||||
/// <returns>The width of everything that's not a cell.</returns>
|
||||
public int GetNonColumnWidth()
|
||||
{
|
||||
var hideBorder = !_border.Visible;
|
||||
var separators = hideBorder ? 0 : Columns.Count - 1;
|
||||
var edges = hideBorder ? 0 : EdgeCount;
|
||||
var padding = Columns.Select(x => x.Padding?.GetWidth() ?? 0).Sum();
|
||||
|
||||
if (!_padRightCell)
|
||||
{
|
||||
padding -= Columns.Last().Padding.GetRightSafe();
|
||||
}
|
||||
|
||||
return separators + edges + padding;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the width of all columns minus any padding.
|
||||
/// </summary>
|
||||
/// <param name="maxWidth">The maximum width that the columns may occupy.</param>
|
||||
/// <returns>A list of column widths.</returns>
|
||||
public List<int> CalculateColumnWidths(int maxWidth)
|
||||
{
|
||||
var width_ranges = Columns.Select(column => MeasureColumn(column, maxWidth)).ToArray();
|
||||
var widths = width_ranges.Select(range => range.Max).ToList();
|
||||
|
||||
var tableWidth = widths.Sum();
|
||||
if (tableWidth > maxWidth)
|
||||
{
|
||||
var wrappable = Columns.Select(c => !c.NoWrap).ToList();
|
||||
widths = CollapseWidths(widths, wrappable, maxWidth);
|
||||
tableWidth = widths.Sum();
|
||||
|
||||
// last resort, reduce columns evenly
|
||||
if (tableWidth > maxWidth)
|
||||
{
|
||||
var excessWidth = tableWidth - maxWidth;
|
||||
widths = Ratio.Reduce(excessWidth, widths.Select(_ => 1).ToList(), widths, widths);
|
||||
tableWidth = widths.Sum();
|
||||
}
|
||||
}
|
||||
|
||||
if (tableWidth < maxWidth && Expand)
|
||||
{
|
||||
var padWidths = Ratio.Distribute(maxWidth - tableWidth, widths);
|
||||
widths = widths.Zip(padWidths, (a, b) => (a, b)).Select(f => f.a + f.b).ToList();
|
||||
}
|
||||
|
||||
return widths;
|
||||
}
|
||||
|
||||
public Measurement MeasureColumn(TableColumn column, int maxWidth)
|
||||
{
|
||||
// Predetermined width?
|
||||
if (column.Width != null)
|
||||
{
|
||||
return new Measurement(column.Width.Value, column.Width.Value);
|
||||
}
|
||||
|
||||
var columnIndex = Columns.IndexOf(column);
|
||||
var rows = Rows.Select(row => row[columnIndex]);
|
||||
|
||||
var minWidths = new List<int>();
|
||||
var maxWidths = new List<int>();
|
||||
|
||||
// Include columns (both header and footer) in measurement
|
||||
var headerMeasure = column.Header.Measure(Options, maxWidth);
|
||||
var footerMeasure = column.Footer?.Measure(Options, maxWidth) ?? headerMeasure;
|
||||
minWidths.Add(Math.Min(headerMeasure.Min, footerMeasure.Min));
|
||||
maxWidths.Add(Math.Max(headerMeasure.Max, footerMeasure.Max));
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var rowMeasure = row.Measure(Options, maxWidth);
|
||||
minWidths.Add(rowMeasure.Min);
|
||||
maxWidths.Add(rowMeasure.Max);
|
||||
}
|
||||
|
||||
var padding = column.Padding?.GetWidth() ?? 0;
|
||||
|
||||
return new Measurement(
|
||||
minWidths.Count > 0 ? minWidths.Max() : padding,
|
||||
maxWidths.Count > 0 ? maxWidths.Max() : maxWidth);
|
||||
}
|
||||
|
||||
// Reduce widths so that the total is less or equal to the max width.
|
||||
// Ported from Rich by Will McGugan, licensed under MIT.
|
||||
// https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/table.py#L442
|
||||
private static List<int> CollapseWidths(List<int> widths, List<bool> wrappable, int maxWidth)
|
||||
{
|
||||
var totalWidth = widths.Sum();
|
||||
var excessWidth = totalWidth - maxWidth;
|
||||
|
||||
if (wrappable.AnyTrue())
|
||||
{
|
||||
while (totalWidth != 0 && excessWidth > 0)
|
||||
{
|
||||
var maxColumn = widths.Zip(wrappable, (first, second) => (width: first, allowWrap: second))
|
||||
.Where(x => x.allowWrap)
|
||||
.Max(x => x.width);
|
||||
|
||||
var secondMaxColumn = widths.Zip(wrappable, (width, allowWrap) => allowWrap && width != maxColumn ? width : 1).Max();
|
||||
var columnDifference = maxColumn - secondMaxColumn;
|
||||
|
||||
var ratios = widths.Zip(wrappable, (width, allowWrap) => width == maxColumn && allowWrap ? 1 : 0).ToList();
|
||||
if (!ratios.Any(x => x != 0) || columnDifference == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var maxReduce = widths.Select(_ => Math.Min(excessWidth, columnDifference)).ToList();
|
||||
widths = Ratio.Reduce(excessWidth, ratios, maxReduce, widths);
|
||||
|
||||
totalWidth = widths.Sum();
|
||||
excessWidth = totalWidth - maxWidth;
|
||||
}
|
||||
}
|
||||
|
||||
return widths;
|
||||
}
|
||||
}
|
||||
}
|
182
src/Spectre.Console/Widgets/Table/TableRenderer.cs
Normal file
182
src/Spectre.Console/Widgets/Table/TableRenderer.cs
Normal file
@ -0,0 +1,182 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Internal;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
internal static class TableRenderer
|
||||
{
|
||||
private static readonly Style _defaultHeadingStyle = new Style(Color.Silver);
|
||||
private static readonly Style _defaultCaptionStyle = new Style(Color.Grey);
|
||||
|
||||
public static List<Segment> Render(TableRendererContext context, List<int> columnWidths)
|
||||
{
|
||||
// Can't render the table?
|
||||
if (context.TableWidth <= 0 || context.TableWidth > context.MaxWidth || columnWidths.Any(c => c <= 0))
|
||||
{
|
||||
return new List<Segment>(new[] { new Segment("…", context.BorderStyle ?? Style.Plain) });
|
||||
}
|
||||
|
||||
var result = new List<Segment>();
|
||||
result.AddRange(RenderAnnotation(context, context.Title, _defaultHeadingStyle));
|
||||
|
||||
// Iterate all rows
|
||||
foreach (var (index, isFirstRow, isLastRow, row) in context.Rows.Enumerate())
|
||||
{
|
||||
var cellHeight = 1;
|
||||
|
||||
// Get the list of cells for the row and calculate the cell height
|
||||
var cells = new List<List<SegmentLine>>();
|
||||
foreach (var (columnIndex, _, _, (rowWidth, cell)) in columnWidths.Zip(row).Enumerate())
|
||||
{
|
||||
var justification = context.Columns[columnIndex].Alignment;
|
||||
var childContext = context.Options.WithJustification(justification);
|
||||
|
||||
var lines = Segment.SplitLines(context.Options, cell.Render(childContext, rowWidth));
|
||||
cellHeight = Math.Max(cellHeight, lines.Count);
|
||||
cells.Add(lines);
|
||||
}
|
||||
|
||||
// Show top of header?
|
||||
if (isFirstRow && context.ShowBorder)
|
||||
{
|
||||
var separator = Aligner.Align(context.Options, context.Border.GetColumnRow(TablePart.Top, columnWidths, context.Columns), context.Alignment, context.MaxWidth);
|
||||
result.Add(new Segment(separator, context.BorderStyle));
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
|
||||
// Show footer separator?
|
||||
if (context.ShowFooters && isLastRow && context.ShowBorder && context.HasFooters)
|
||||
{
|
||||
var textBorder = context.Border.GetColumnRow(TablePart.FooterSeparator, columnWidths, context.Columns);
|
||||
if (!string.IsNullOrEmpty(textBorder))
|
||||
{
|
||||
var separator = Aligner.Align(context.Options, textBorder, context.Alignment, context.MaxWidth);
|
||||
result.Add(new Segment(separator, context.BorderStyle));
|
||||
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))
|
||||
{
|
||||
var rowResult = new List<Segment>();
|
||||
|
||||
foreach (var (cellIndex, isFirstCell, isLastCell, cell) in cells.Enumerate())
|
||||
{
|
||||
if (isFirstCell && context.ShowBorder)
|
||||
{
|
||||
// Show left column edge
|
||||
var part = isFirstRow && context.ShowHeaders ? TableBorderPart.HeaderLeft : TableBorderPart.CellLeft;
|
||||
rowResult.Add(new Segment(context.Border.GetPart(part), context.BorderStyle));
|
||||
}
|
||||
|
||||
// Pad column on left side.
|
||||
if (context.ShowBorder || context.IsGrid)
|
||||
{
|
||||
var leftPadding = context.Columns[cellIndex].Padding.GetLeftSafe();
|
||||
if (leftPadding > 0)
|
||||
{
|
||||
rowResult.Add(new Segment(new string(' ', leftPadding)));
|
||||
}
|
||||
}
|
||||
|
||||
// Add content
|
||||
rowResult.AddRange(cell[cellRowIndex]);
|
||||
|
||||
// Pad cell content right
|
||||
var length = cell[cellRowIndex].Sum(segment => segment.CellCount(context.Options));
|
||||
if (length < columnWidths[cellIndex])
|
||||
{
|
||||
rowResult.Add(new Segment(new string(' ', columnWidths[cellIndex] - length)));
|
||||
}
|
||||
|
||||
// Pad column on the right side
|
||||
if (context.ShowBorder || (context.HideBorder && !isLastCell) || (context.HideBorder && isLastCell && context.IsGrid && context.PadRightCell))
|
||||
{
|
||||
var rightPadding = context.Columns[cellIndex].Padding.GetRightSafe();
|
||||
if (rightPadding > 0)
|
||||
{
|
||||
rowResult.Add(new Segment(new string(' ', rightPadding)));
|
||||
}
|
||||
}
|
||||
|
||||
if (isLastCell && context.ShowBorder)
|
||||
{
|
||||
// Add right column edge
|
||||
var part = isFirstRow && context.ShowHeaders ? TableBorderPart.HeaderRight : TableBorderPart.CellRight;
|
||||
rowResult.Add(new Segment(context.Border.GetPart(part), context.BorderStyle));
|
||||
}
|
||||
else if (context.ShowBorder)
|
||||
{
|
||||
// Add column separator
|
||||
var part = isFirstRow && context.ShowHeaders ? TableBorderPart.HeaderSeparator : TableBorderPart.CellSeparator;
|
||||
rowResult.Add(new Segment(context.Border.GetPart(part), context.BorderStyle));
|
||||
}
|
||||
}
|
||||
|
||||
// Align the row result.
|
||||
Aligner.Align(context.Options, rowResult, context.Alignment, context.MaxWidth);
|
||||
|
||||
// Is the row larger than the allowed max width?
|
||||
if (Segment.CellCount(context.Options, rowResult) > context.MaxWidth)
|
||||
{
|
||||
result.AddRange(Segment.Truncate(context.Options, rowResult, context.MaxWidth));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.AddRange(rowResult);
|
||||
}
|
||||
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
|
||||
// Show header separator?
|
||||
if (isFirstRow && context.ShowBorder && context.ShowHeaders && context.HasRows)
|
||||
{
|
||||
var separator = Aligner.Align(context.Options, context.Border.GetColumnRow(TablePart.HeaderSeparator, columnWidths, context.Columns), context.Alignment, context.MaxWidth);
|
||||
result.Add(new Segment(separator, context.BorderStyle));
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
|
||||
// Show bottom of footer?
|
||||
if (isLastRow && context.ShowBorder)
|
||||
{
|
||||
var separator = Aligner.Align(context.Options, context.Border.GetColumnRow(TablePart.Bottom, columnWidths, context.Columns), context.Alignment, context.MaxWidth);
|
||||
result.Add(new Segment(separator, context.BorderStyle));
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
}
|
||||
|
||||
result.AddRange(RenderAnnotation(context, context.Caption, _defaultCaptionStyle));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IEnumerable<Segment> RenderAnnotation(TableRendererContext context, TableTitle? header, Style defaultStyle)
|
||||
{
|
||||
if (header == null)
|
||||
{
|
||||
return Array.Empty<Segment>();
|
||||
}
|
||||
|
||||
var paragraph = new Markup(header.Text.Capitalize(), header.Style ?? defaultStyle)
|
||||
.Alignment(Justify.Center)
|
||||
.Overflow(Overflow.Ellipsis);
|
||||
|
||||
// Render the paragraphs
|
||||
var segments = new List<Segment>();
|
||||
segments.AddRange(((IRenderable)paragraph).Render(context.Options, context.TableWidth));
|
||||
|
||||
// Align over the whole buffer area
|
||||
Aligner.Align(context.Options, segments, context.Alignment, context.MaxWidth);
|
||||
|
||||
segments.Add(Segment.LineBreak);
|
||||
return segments;
|
||||
}
|
||||
}
|
||||
}
|
57
src/Spectre.Console/Widgets/Table/TableRendererContext.cs
Normal file
57
src/Spectre.Console/Widgets/Table/TableRendererContext.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
internal sealed class TableRendererContext : TableAccessor
|
||||
{
|
||||
private readonly Table _table;
|
||||
private readonly List<TableRow> _rows;
|
||||
|
||||
public override IReadOnlyList<TableRow> Rows => _rows;
|
||||
|
||||
public TableBorder Border { get; }
|
||||
public Style BorderStyle { get; }
|
||||
public bool ShowBorder { get; }
|
||||
public bool HasRows { get; }
|
||||
public bool HasFooters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the max width of the destination area.
|
||||
/// The table might take up less than this.
|
||||
/// </summary>
|
||||
public int MaxWidth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the width of the table.
|
||||
/// </summary>
|
||||
public int TableWidth { get; }
|
||||
|
||||
public bool HideBorder => !ShowBorder;
|
||||
public bool ShowHeaders => _table.ShowHeaders;
|
||||
public bool ShowFooters => _table.ShowFooters;
|
||||
public bool IsGrid => _table.IsGrid;
|
||||
public bool PadRightCell => _table.PadRightCell;
|
||||
public TableTitle? Title => _table.Title;
|
||||
public TableTitle? Caption => _table.Caption;
|
||||
public Justify? Alignment => _table.Alignment;
|
||||
|
||||
public TableRendererContext(Table table, RenderContext options, IEnumerable<TableRow> rows, int tableWidth, int maxWidth)
|
||||
: base(table, options)
|
||||
{
|
||||
_table = table ?? throw new ArgumentNullException(nameof(table));
|
||||
_rows = new List<TableRow>(rows ?? Enumerable.Empty<TableRow>());
|
||||
|
||||
ShowBorder = _table.Border.Visible;
|
||||
HasRows = Rows.Any(row => !row.IsHeader && !row.IsFooter);
|
||||
HasFooters = Rows.Any(column => column.IsFooter);
|
||||
Border = table.Border.GetSafeBorder((options.LegacyConsole || !options.Unicode) && table.UseSafeBorder);
|
||||
BorderStyle = table.BorderStyle ?? Style.Plain;
|
||||
|
||||
TableWidth = tableWidth;
|
||||
MaxWidth = maxWidth;
|
||||
}
|
||||
}
|
||||
}
|
@ -12,6 +12,9 @@ namespace Spectre.Console
|
||||
{
|
||||
private readonly List<IRenderable> _items;
|
||||
|
||||
internal bool IsHeader { get; }
|
||||
internal bool IsFooter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a row item at the specified table column index.
|
||||
/// </summary>
|
||||
@ -27,8 +30,26 @@ namespace Spectre.Console
|
||||
/// </summary>
|
||||
/// <param name="items">The row items.</param>
|
||||
public TableRow(IEnumerable<IRenderable> items)
|
||||
: this(items, false, false)
|
||||
{
|
||||
}
|
||||
|
||||
private TableRow(IEnumerable<IRenderable> items, bool isHeader, bool isFooter)
|
||||
{
|
||||
_items = new List<IRenderable>(items ?? Array.Empty<IRenderable>());
|
||||
|
||||
IsHeader = isHeader;
|
||||
IsFooter = isFooter;
|
||||
}
|
||||
|
||||
internal static TableRow Header(IEnumerable<IRenderable> items)
|
||||
{
|
||||
return new TableRow(items, true, false);
|
||||
}
|
||||
|
||||
internal static TableRow Footer(IEnumerable<IRenderable> items)
|
||||
{
|
||||
return new TableRow(items, false, true);
|
||||
}
|
||||
|
||||
internal void Add(IRenderable item)
|
Loading…
x
Reference in New Issue
Block a user