Add better algorithm for calculating column widths

Closes #14
This commit is contained in:
Patrik Svensson
2020-08-05 16:25:09 +02:00
committed by Patrik Svensson
parent 0b4321115a
commit 9637066927
18 changed files with 751 additions and 150 deletions

View File

@ -17,23 +17,27 @@ namespace Spectre.Console
/// </summary>
public Grid()
{
_table = new Table(BorderKind.None, showHeaders: false);
_table = new Table
{
Border = BorderKind.None,
ShowHeaders = false,
};
}
/// <inheritdoc/>
public int Measure(Encoding encoding, int maxWidth)
public Measurement Measure(Encoding encoding, int maxWidth)
{
return _table.Measure(encoding, maxWidth);
return ((IRenderable)_table).Measure(encoding, maxWidth);
}
/// <inheritdoc/>
public IEnumerable<Segment> Render(Encoding encoding, int width)
{
return _table.Render(encoding, width);
return ((IRenderable)_table).Render(encoding, width);
}
/// <summary>
/// Adds a single column to the grid.
/// Adds a column to the grid.
/// </summary>
public void AddColumn()
{
@ -41,15 +45,23 @@ namespace Spectre.Console
}
/// <summary>
/// Adds the specified number of columns to the grid.
/// Adds a column to the grid.
/// </summary>
/// <param name="count">The number of columns.</param>
public void AddColumns(int count)
/// <param name="column">The column to add.</param>
public void AddColumn(GridColumn column)
{
for (var i = 0; i < count; i++)
if (column is null)
{
_table.AddColumn(string.Empty);
throw new ArgumentNullException(nameof(column));
}
_table.AddColumn(new TableColumn(string.Empty)
{
Width = column.Width,
NoWrap = column.NoWrap,
LeftPadding = 0,
RightPadding = 1,
});
}
/// <summary>

View File

@ -0,0 +1,20 @@
namespace Spectre.Console
{
/// <summary>
/// Represents a grid column.
/// </summary>
public sealed class GridColumn
{
/// <summary>
/// Gets or sets the width of the column.
/// If <c>null</c>, the column will adapt to it's contents.
/// </summary>
public int? Width { get; set; }
/// <summary>
/// Gets or sets a value indicating whether wrapping of
/// text within the column should be prevented.
/// </summary>
public bool NoWrap { get; set; }
}
}

View File

@ -13,8 +13,8 @@ namespace Spectre.Console.Composition
/// </summary>
/// <param name="encoding">The encoding to use.</param>
/// <param name="maxWidth">The maximum allowed width.</param>
/// <returns>The width of the object.</returns>
int Measure(Encoding encoding, int maxWidth);
/// <returns>The minimum and maximum width of the object.</returns>
Measurement Measure(Encoding encoding, int maxWidth);
/// <summary>
/// Renders the object.

View File

@ -0,0 +1,77 @@
using System;
namespace Spectre.Console.Composition
{
/// <summary>
/// Represents a measurement.
/// </summary>
public struct Measurement : IEquatable<Measurement>
{
/// <summary>
/// Gets the minimum width.
/// </summary>
public int Min { get; }
/// <summary>
/// Gets the maximum width.
/// </summary>
public int Max { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Measurement"/> struct.
/// </summary>
/// <param name="min">The minimum width.</param>
/// <param name="max">The maximum width.</param>
public Measurement(int min, int max)
{
Min = min;
Max = max;
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
return obj is Measurement measurement && Equals(measurement);
}
/// <inheritdoc/>
public override int GetHashCode()
{
unchecked
{
var hash = (int)2166136261;
hash = (hash * 16777619) ^ Min.GetHashCode();
hash = (hash * 16777619) ^ Max.GetHashCode();
return hash;
}
}
/// <inheritdoc/>
public bool Equals(Measurement other)
{
return Min == other.Min && Max == other.Max;
}
/// <summary>
/// Checks if two <see cref="Measurement"/> instances are equal.
/// </summary>
/// <param name="left">The first measurement instance to compare.</param>
/// <param name="right">The second measurement instance to compare.</param>
/// <returns><c>true</c> if the two measurements are equal, otherwise <c>false</c>.</returns>
public static bool operator ==(Measurement left, Measurement right)
{
return left.Equals(right);
}
/// <summary>
/// Checks if two <see cref="Measurement"/> instances are not equal.
/// </summary>
/// <param name="left">The first measurement instance to compare.</param>
/// <param name="right">The second measurement instance to compare.</param>
/// <returns><c>true</c> if the two measurements are not equal, otherwise <c>false</c>.</returns>
public static bool operator !=(Measurement left, Measurement right)
{
return !(left == right);
}
}
}

View File

@ -35,19 +35,20 @@ namespace Spectre.Console
}
/// <inheritdoc/>
public int Measure(Encoding encoding, int maxWidth)
Measurement IRenderable.Measure(Encoding encoding, int maxWidth)
{
var childWidth = _child.Measure(encoding, maxWidth);
return childWidth + 4;
return new Measurement(childWidth.Min + 4, childWidth.Max + 4);
}
/// <inheritdoc/>
public IEnumerable<Segment> Render(Encoding encoding, int width)
IEnumerable<Segment> IRenderable.Render(Encoding encoding, int width)
{
var childWidth = width - 4;
if (!_fit)
{
childWidth = _child.Measure(encoding, width - 2);
var measurement = _child.Measure(encoding, width - 2);
childWidth = measurement.Max;
}
var result = new List<Segment>();

View File

@ -211,5 +211,21 @@ namespace Spectre.Console.Composition
return lines;
}
internal static List<List<SegmentLine>> MakeSameHeight(int cellHeight, List<List<SegmentLine>> cells)
{
foreach (var cell in cells)
{
if (cell.Count < cellHeight)
{
while (cell.Count != cellHeight)
{
cell.Add(new SegmentLine());
}
}
}
return cells;
}
}
}

View File

@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Spectre.Console.Composition;
using Spectre.Console.Internal;
namespace Spectre.Console
{
/// <summary>
/// Represents a table.
/// </summary>
public sealed partial class Table
{
// 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(Encoding encoding, int maxWidth)
{
var width_ranges = _columns.Select(column => MeasureColumn(column, encoding, maxWidth));
var widths = width_ranges.Select(range => range.Max).ToList();
var tableWidth = widths.Sum();
if (ShouldExpand())
{
var ratios = _columns.Select(c => c.Ratio ?? 0).ToList();
if (ratios.Any(r => r != 0))
{
var fixedWidths = new List<int>();
foreach (var (range, column) in width_ranges.Zip(_columns, (a, b) => (a, b)))
{
fixedWidths.Add(column.IsFlexible() ? 0 : range.Max);
}
var flexMinimum = new List<int>();
foreach (var column in _columns)
{
if (column.IsFlexible())
{
flexMinimum.Add(column.Width ?? 1 + column.GetPadding());
}
else
{
flexMinimum.Add(0);
}
}
var flexibleWidth = maxWidth - fixedWidths.Sum();
var flexWidths = Ratio.Distribute(flexibleWidth, ratios, flexMinimum);
var flexWidthsIterator = flexWidths.GetEnumerator();
foreach (var (index, _, _, column) in _columns.Enumerate())
{
if (column.IsFlexible())
{
flexWidthsIterator.MoveNext();
widths[index] = fixedWidths[index] + flexWidthsIterator.Current;
}
}
}
}
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(w => 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, isWrappable: second))
.Where(x => x.isWrappable)
.Max(x => x.width);
var secondMaxColumn = widths.Zip(wrappable, (width, isWrappable) => isWrappable && width != maxColumn ? maxColumn : 0).Max();
var columnDifference = maxColumn - secondMaxColumn;
var ratios = widths.Zip(wrappable, (width, allowWrap) =>
{
if (width == maxColumn && allowWrap)
{
return 1;
}
return 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 (int Min, int Max) MeasureColumn(TableColumn column, Encoding encoding, int maxWidth)
{
// Predetermined width?
if (column.Width != null)
{
var padding = column.GetPadding();
return (column.Width.Value + padding, column.Width.Value + padding);
}
var columnIndex = _columns.IndexOf(column);
var rows = _rows.Select(row => row[columnIndex]);
var minWidths = new List<int>();
var maxWidths = new List<int>();
foreach (var row in rows)
{
var measure = ((IRenderable)row).Measure(encoding, maxWidth);
minWidths.Add(measure.Min);
maxWidths.Add(measure.Max);
}
return (minWidths.Count > 0 ? minWidths.Max() : 1,
maxWidths.Count > 0 ? maxWidths.Max() : maxWidth);
}
private int GetExtraWidth(bool includePadding)
{
var edges = 2;
var separators = _columns.Count - 1;
var padding = includePadding ? _columns.Select(x => x.GetPadding()).Sum() : 0;
return separators + edges + padding;
}
}
}

View File

@ -10,31 +10,50 @@ namespace Spectre.Console
/// <summary>
/// Represents a table.
/// </summary>
public sealed class Table : IRenderable
public sealed partial class Table : IRenderable
{
private readonly List<Text> _columns;
private readonly List<TableColumn> _columns;
private readonly List<List<Text>> _rows;
private readonly Border _border;
private readonly BorderKind _borderKind;
private readonly bool _showHeaders;
/// <summary>
/// Gets the number of columns in the table.
/// </summary>
public int ColumnCount => _columns.Count;
/// <summary>
/// Gets the number of rows in the table.
/// </summary>
public int RowCount => _rows.Count;
/// <summary>
/// Gets or sets the kind of border to use.
/// </summary>
public BorderKind Border { get; set; } = BorderKind.Square;
/// <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 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; } = false;
/// <summary>
/// Gets or sets the width of the table.
/// </summary>
public int? Width { get; set; } = null;
/// <summary>
/// Initializes a new instance of the <see cref="Table"/> class.
/// </summary>
/// <param name="border">The border to use.</param>
/// <param name="showHeaders">Whether or not to show table headers.</param>
public Table(BorderKind border = BorderKind.Square, bool showHeaders = true)
public Table()
{
_columns = new List<Text>();
_columns = new List<TableColumn>();
_rows = new List<List<Text>>();
_border = Border.GetBorder(border);
_borderKind = border;
_showHeaders = showHeaders;
}
/// <summary>
@ -48,7 +67,21 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(column));
}
_columns.Add(Text.New(column));
_columns.Add(new TableColumn(column));
}
/// <summary>
/// Adds a column to the table.
/// </summary>
/// <param name="column">The column to add.</param>
public void AddColumn(TableColumn column)
{
if (column is null)
{
throw new ArgumentNullException(nameof(column));
}
_columns.Add(column);
}
/// <summary>
@ -62,7 +95,7 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(columns));
}
_columns.AddRange(columns.Select(column => Text.New(column)));
_columns.AddRange(columns.Select(column => new TableColumn(column)));
}
/// <summary>
@ -90,66 +123,55 @@ namespace Spectre.Console
}
/// <inheritdoc/>
public int Measure(Encoding encoding, int maxWidth)
Measurement IRenderable.Measure(Encoding encoding, int maxWidth)
{
// Calculate the max width for each column
var maxColumnWidth = (maxWidth - (2 + (_columns.Count * 2) + (_columns.Count - 1))) / _columns.Count;
var columnWidths = _columns.Select(c => c.Measure(encoding, maxColumnWidth)).ToArray();
for (var rowIndex = 0; rowIndex < _rows.Count; rowIndex++)
if (Width != null)
{
for (var columnIndex = 0; columnIndex < _rows[rowIndex].Count; columnIndex++)
{
var columnWidth = _rows[rowIndex][columnIndex].Measure(encoding, maxColumnWidth);
if (columnWidth > columnWidths[columnIndex])
{
columnWidths[columnIndex] = columnWidth;
}
}
maxWidth = Math.Min(Width.Value, maxWidth);
}
// We now know the max width of each column, so let's recalculate the width
return columnWidths.Sum() + 2 + (_columns.Count * 2) + (_columns.Count - 1);
maxWidth -= GetExtraWidth(includePadding: true);
var measurements = _columns.Select(column => MeasureColumn(column, encoding, 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/>
public IEnumerable<Segment> Render(Encoding encoding, int width)
IEnumerable<Segment> IRenderable.Render(Encoding encoding, int width)
{
var showBorder = _borderKind != BorderKind.None;
var hideBorder = _borderKind == BorderKind.None;
var border = Composition.Border.GetBorder(Border);
var leftRightBorderWidth = _borderKind == BorderKind.None ? 0 : 2;
var columnPadding = _borderKind == BorderKind.None ? _columns.Count : _columns.Count * 2;
var separatorCount = _borderKind == BorderKind.None ? 0 : _columns.Count - 1;
var showBorder = Border != BorderKind.None;
var hideBorder = Border == BorderKind.None;
// Calculate the max width for each column.
var maxColumnWidth = (width - (leftRightBorderWidth + columnPadding + separatorCount)) / _columns.Count;
var columnWidths = _columns.Select(c => c.Measure(encoding, maxColumnWidth)).ToArray();
for (var rowIndex = 0; rowIndex < _rows.Count; rowIndex++)
var maxWidth = width;
if (Width != null)
{
for (var columnIndex = 0; columnIndex < _rows[rowIndex].Count; columnIndex++)
{
var columnWidth = _rows[rowIndex][columnIndex].Measure(encoding, maxColumnWidth);
if (columnWidth > columnWidths[columnIndex])
{
columnWidths[columnIndex] = columnWidth;
}
}
maxWidth = Math.Min(Width.Value, maxWidth);
}
// We now know the max width of each column, so let's recalculate the width
width = columnWidths.Sum() + leftRightBorderWidth + columnPadding + separatorCount;
maxWidth -= GetExtraWidth(includePadding: true);
// Calculate the column and table widths
var columnWidths = CalculateColumnWidths(encoding, maxWidth);
// Update the table width.
width = columnWidths.Sum() + GetExtraWidth(includePadding: false);
var rows = new List<List<Text>>();
if (_showHeaders)
if (ShowHeaders)
{
// Add columns to top of rows
rows.Add(new List<Text>(_columns));
rows.Add(new List<Text>(_columns.Select(c => c.Text)));
}
// Add tows.
// Add rows.
rows.AddRange(_rows);
// Iterate all rows.
// Iterate all rows
var result = new List<Segment>();
foreach (var (index, firstRow, lastRow, row) in rows.Enumerate())
{
@ -159,7 +181,7 @@ namespace Spectre.Console
var cells = new List<List<SegmentLine>>();
foreach (var (rowWidth, cell) in columnWidths.Zip(row, (f, s) => (f, s)))
{
var lines = Segment.SplitLines(cell.Render(encoding, rowWidth));
var lines = Segment.SplitLines(((IRenderable)cell).Render(encoding, rowWidth));
cellHeight = Math.Max(cellHeight, lines.Count);
cells.Add(lines);
}
@ -167,20 +189,20 @@ namespace Spectre.Console
// Show top of header?
if (firstRow && showBorder)
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopLeft)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderTopLeft)));
foreach (var (columnIndex, _, lastColumn, columnWidth) in columnWidths.Enumerate())
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop))); // Left padding
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop, columnWidth)));
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop))); // Right padding
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop))); // Left padding
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, columnWidth)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop))); // Right padding
if (!lastColumn)
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopSeparator)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderTopSeparator)));
}
}
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopRight)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderTopRight)));
result.Add(Segment.LineBreak());
}
@ -188,21 +210,24 @@ namespace Spectre.Console
foreach (var cellRowIndex in Enumerable.Range(0, cellHeight))
{
// Make cells the same shape
MakeSameHeight(cellHeight, cells);
cells = Segment.MakeSameHeight(cellHeight, cells);
var w00t = cells.Enumerate().ToArray();
foreach (var (cellIndex, firstCell, lastCell, cell) in w00t)
foreach (var (cellIndex, firstCell, lastCell, cell) in cells.Enumerate())
{
if (firstCell && showBorder)
{
// Show left column edge
result.Add(new Segment(_border.GetPart(BorderPart.CellLeft)));
result.Add(new Segment(border.GetPart(BorderPart.CellLeft)));
}
// Pad column on left side.
if (showBorder)
{
result.Add(new Segment(" "));
var leftPadding = _columns[cellIndex].LeftPadding;
if (leftPadding > 0)
{
result.Add(new Segment(new string(' ', leftPadding)));
}
}
// Add content
@ -218,18 +243,22 @@ namespace Spectre.Console
// Pad column on the right side
if (showBorder || (hideBorder && !lastCell))
{
result.Add(new Segment(" "));
var rightPadding = _columns[cellIndex].RightPadding;
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)));
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(new Segment(border.GetPart(BorderPart.CellSeparator)));
}
}
@ -237,42 +266,42 @@ namespace Spectre.Console
}
// Show bottom of header?
if (firstRow && showBorder)
if (firstRow && showBorder && ShowHeaders)
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomLeft)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomLeft)));
foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom))); // Left padding
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom, columnWidth)));
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom))); // Right padding
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom))); // Left padding
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom, columnWidth)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom))); // Right padding
if (!lastColumn)
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomSeparator)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomSeparator)));
}
}
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomRight)));
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)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft)));
foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
{
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom)));
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom, columnWidth)));
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, columnWidth)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom)));
if (!lastColumn)
{
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomSeparator)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomSeparator)));
}
}
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomRight)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight)));
result.Add(Segment.LineBreak());
}
}
@ -280,18 +309,9 @@ namespace Spectre.Console
return result;
}
private static void MakeSameHeight(int cellHeight, List<List<SegmentLine>> cells)
private bool ShouldExpand()
{
foreach (var cell in cells)
{
if (cell.Count < cellHeight)
{
while (cell.Count != cellHeight)
{
cell.Add(new SegmentLine());
}
}
}
return Expand || Width != null;
}
}
}

View File

@ -0,0 +1,67 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Represents a table column.
/// </summary>
public sealed class TableColumn
{
/// <summary>
/// Gets the text associated with the column.
/// </summary>
public Text Text { get; }
/// <summary>
/// Gets or sets the width of the column.
/// If <c>null</c>, the column will adapt to it's contents.
/// </summary>
public int? Width { get; set; }
/// <summary>
/// Gets or sets the left padding.
/// </summary>
public int LeftPadding { get; set; }
/// <summary>
/// Gets or sets the right padding.
/// </summary>
public int RightPadding { get; set; }
/// <summary>
/// Gets or sets the ratio to use when calculating column width.
/// If <c>null</c>, the column will adapt to it's contents.
/// </summary>
public int? Ratio { get; set; }
/// <summary>
/// Gets or sets a value indicating whether wrapping of
/// text within the column should be prevented.
/// </summary>
public bool NoWrap { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="TableColumn"/> class.
/// </summary>
/// <param name="text">The table column text.</param>
public TableColumn(string text)
{
Text = Text.New(text ?? throw new ArgumentNullException(nameof(text)));
Width = null;
LeftPadding = 1;
RightPadding = 1;
Ratio = null;
NoWrap = false;
}
internal int GetPadding()
{
return LeftPadding + RightPadding;
}
internal bool IsFlexible()
{
return Width == null;
}
}
}

View File

@ -98,19 +98,22 @@ namespace Spectre.Console
}
/// <inheritdoc/>
public int Measure(Encoding encoding, int maxWidth)
Measurement IRenderable.Measure(Encoding encoding, int maxWidth)
{
var lines = Segment.SplitLines(Render(encoding, maxWidth));
var lines = Segment.SplitLines(((IRenderable)this).Render(encoding, maxWidth));
if (lines.Count == 0)
{
return 0;
return new Measurement(0, maxWidth);
}
return lines.Max(x => x.Length);
var max = lines.Max(line => line.Length);
var min = lines.SelectMany(line => line.Select(segment => segment.Text.Length)).Max();
return new Measurement(min, max);
}
/// <inheritdoc/>
public IEnumerable<Segment> Render(Encoding encoding, int width)
IEnumerable<Segment> IRenderable.Render(Encoding encoding, int width)
{
if (string.IsNullOrWhiteSpace(_text))
{

View File

@ -1,5 +1,6 @@
using System;
using System.Globalization;
using Spectre.Console.Internal;
namespace Spectre.Console
{
@ -19,7 +20,12 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(console));
}
console.Write(Environment.NewLine);
using (console.PushColor(Color.Default, true))
using (console.PushColor(Color.Default, false))
using (console.PushDecoration(Decoration.None))
{
console.Write(Environment.NewLine);
}
}
/// <summary>

View File

@ -6,6 +6,19 @@ namespace Spectre.Console.Internal
{
internal static class EnumerableExtensions
{
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
foreach (var item in source)
{
action(item);
}
}
public static bool AnyTrue(this IEnumerable<bool> source)
{
return source.Any(b => b);
}
public static IEnumerable<(int Index, bool First, bool Last, T Item)> Enumerate<T>(this IEnumerable<T> source)
{
if (source is null)
@ -40,5 +53,18 @@ namespace Spectre.Console.Internal
{
return source.Select((value, index) => func(value, index));
}
public static IEnumerable<(TFirst First, TSecond Second)> Zip<TFirst, TSecond>(
this IEnumerable<TFirst> source, IEnumerable<TSecond> first)
{
return source.Zip(first, (first, second) => (first, second));
}
public static IEnumerable<(TFirst First, TSecond Second, TThird Third)> Zip<TFirst, TSecond, TThird>(
this IEnumerable<TFirst> first, IEnumerable<TSecond> second, IEnumerable<TThird> third)
{
return first.Zip(second, (a, b) => (a, b))
.Zip(third, (a, b) => (a.a, a.b, b));
}
}
}

View File

@ -0,0 +1,75 @@
// Ported from Rich by Will McGugan, licensed under MIT.
// https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/_ratio.py
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Spectre.Console.Internal
{
internal static class Ratio
{
public static List<int> Reduce(int total, List<int> ratios, List<int> maximums, List<int> values)
{
ratios = ratios.Zip(maximums, (a, b) => (ratio: a, max: b)).Select(a => a.max > 0 ? a.ratio : 0).ToList();
var totalRatio = ratios.Sum();
if (totalRatio == 0)
{
return values;
}
var totalRemaining = total;
var result = new List<int>();
foreach (var (ratio, maximum, value) in ratios.Zip(maximums, values))
{
if (ratio > 0 && totalRatio > 0)
{
var distributed = (int)Math.Min(maximum, Math.Round(ratio * totalRemaining / (double)totalRatio));
result.Add(value - distributed);
totalRemaining -= distributed;
totalRatio -= ratio;
}
else
{
result.Add(value);
}
}
return result;
}
public static List<int> Distribute(int total, List<int> ratios, List<int> minimums = null)
{
if (minimums != null)
{
ratios = ratios.Zip(minimums, (a, b) => (ratio: a, min: b)).Select(a => a.min > 0 ? a.ratio : 0).ToList();
}
var totalRatio = ratios.Sum();
Debug.Assert(totalRatio > 0, "Sum or ratios must be > 0");
var totalRemaining = total;
var distributedTotal = new List<int>();
if (minimums == null)
{
minimums = ratios.Select(_ => 0).ToList();
}
foreach (var (ratio, minimum) in ratios.Zip(minimums, (a, b) => (a, b)))
{
var distributed = (totalRatio > 0)
? Math.Max(minimum, (int)Math.Ceiling(ratio * totalRemaining / (double)totalRatio))
: totalRemaining;
distributedTotal.Add(distributed);
totalRatio -= ratio;
totalRemaining -= distributed;
}
return distributedTotal;
}
}
}

View File

@ -22,6 +22,9 @@
<Compile Update="ConsoleExtensions.*.cs">
<DependentUpon>ConsoleExtensions.cs</DependentUpon>
</Compile>
<Compile Update="**/Table.*.cs">
<DependentUpon>**/Table.cs</DependentUpon>
</Compile>
</ItemGroup>