mirror of
				https://github.com/nsnail/spectre.console.git
				synced 2025-11-01 01:25:27 +08:00 
			
		
		
		
	Clean up table rendering a bit
This commit is contained in:
		 Patrik Svensson
					Patrik Svensson
				
			
				
					committed by
					
						 Patrik Svensson
						Patrik Svensson
					
				
			
			
				
	
			
			
			 Patrik Svensson
						Patrik Svensson
					
				
			
						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) | ||||
		Reference in New Issue
	
	Block a user