using System; using System.Collections.Generic; using System.Linq; using Spectre.Console.Composition; using Spectre.Console.Internal; namespace Spectre.Console { /// /// Represents a table. /// public sealed partial class Table : IRenderable { private readonly List _columns; private readonly List> _rows; /// /// Gets the number of columns in the table. /// public int ColumnCount => _columns.Count; /// /// Gets the number of rows in the table. /// public int RowCount => _rows.Count; /// /// Gets or sets the kind of border to use. /// public BorderKind Border { get; set; } = BorderKind.Square; /// /// Gets or sets a value indicating whether or not table headers should be shown. /// public bool ShowHeaders { get; set; } = true; /// /// Gets or sets a value indicating whether or not the table should /// fit the available space. If false, the table width will be /// auto calculated. Defaults to false. /// public bool Expand { get; set; } = false; /// /// Gets or sets the width of the table. /// public int? Width { get; set; } = null; /// /// Gets or sets a value indicating whether or not to use /// a "safe" border on legacy consoles that might not be able /// to render non-ASCII characters. Defaults to true. /// public bool SafeBorder { get; set; } = true; internal bool IsGrid { get; set; } = false; /// /// Initializes a new instance of the class. /// public Table() { _columns = new List(); _rows = new List>(); } /// /// Adds a column to the table. /// /// The column to add. public void AddColumn(string column) { if (column is null) { throw new ArgumentNullException(nameof(column)); } _columns.Add(new TableColumn(column)); } /// /// Adds a column to the table. /// /// The column to add. public void AddColumn(TableColumn column) { if (column is null) { throw new ArgumentNullException(nameof(column)); } _columns.Add(column); } /// /// Adds multiple columns to the table. /// /// The columns to add. public void AddColumns(params string[] columns) { if (columns is null) { throw new ArgumentNullException(nameof(columns)); } _columns.AddRange(columns.Select(column => new TableColumn(column))); } /// /// Adds multiple columns to the table. /// /// The columns to add. public void AddColumns(params TableColumn[] columns) { if (columns is null) { throw new ArgumentNullException(nameof(columns)); } _columns.AddRange(columns.Select(column => column)); } /// /// Adds a row to the table. /// /// The row columns to add. 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()); } /// Measurement IRenderable.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); } /// IEnumerable IRenderable.Render(RenderContext context, int width) { if (context is null) { throw new ArgumentNullException(nameof(context)); } var border = Composition.Border.GetBorder(Border, (context.LegacyConsole || !context.Unicode) && SafeBorder); var showBorder = Border != BorderKind.None; var hideBorder = Border == BorderKind.None; var maxWidth = width; 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. width = columnWidths.Sum() + GetExtraWidth(includePadding: true); var rows = new List>(); if (ShowHeaders) { // Add columns to top of rows rows.Add(new List(_columns.Select(c => c.Text))); } // Add rows. rows.AddRange(_rows); // Iterate all rows var result = new List(); 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>(); foreach (var (columnIndex, _, _, (rowWidth, cell)) in columnWidths.Zip(row).Enumerate()) { 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); cells.Add(lines); } // Show top of header? if (firstRow && showBorder) { result.Add(new Segment(border.GetPart(BorderPart.HeaderTopLeft))); foreach (var (columnIndex, _, lastColumn, columnWidth) in columnWidths.Enumerate()) { 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, padding.Right))); // 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 cells = Segment.MakeSameHeight(cellHeight, cells); foreach (var (cellIndex, firstCell, lastCell, cell) in cells.Enumerate()) { if (firstCell && showBorder) { // Show left column edge result.Add(new Segment(border.GetPart(BorderPart.CellLeft))); } // Pad column on left side. if (showBorder || IsGrid) { var leftPadding = _columns[cellIndex].Padding.Left; if (leftPadding > 0) { result.Add(new Segment(new string(' ', leftPadding))); } } // Add content result.AddRange(cell[cellRowIndex]); // Pad cell content right var length = cell[cellRowIndex].Sum(segment => segment.CellLength(context.Encoding)); if (length < columnWidths[cellIndex]) { result.Add(new Segment(new string(' ', columnWidths[cellIndex] - length))); } // Pad column on the right side if (showBorder || (hideBorder && !lastCell) || (IsGrid && !lastCell)) { var rightPadding = _columns[cellIndex].Padding.Right; if (rightPadding > 0) { result.Add(new Segment(new string(' ', rightPadding))); } } if (lastCell && showBorder) { // Add right column edge result.Add(new Segment(border.GetPart(BorderPart.CellRight))); } else if (showBorder || (hideBorder && !lastCell)) { // Add column separator result.Add(new Segment(border.GetPart(BorderPart.CellSeparator))); } } result.Add(Segment.LineBreak()); } // Show header separator? if (firstRow && showBorder && ShowHeaders) { result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomLeft))); foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate()) { 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, padding.Right))); // Right padding if (!lastColumn) { result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomSeparator))); } } result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomRight))); result.Add(Segment.LineBreak()); } // Show bottom of footer? if (lastRow && showBorder) { result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft))); foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate()) { 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, padding.Right))); // Right padding 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 bool ShouldExpand() { return Expand || Width != null; } } }