mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-06-19 13:28:16 +08:00
Restructure solution a bit
This commit is contained in:
141
src/Spectre.Console/Widgets/Columns.cs
Normal file
141
src/Spectre.Console/Widgets/Columns.cs
Normal file
@ -0,0 +1,141 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Renders things in columns.
|
||||
/// </summary>
|
||||
public sealed class Columns : Renderable, IPaddable, IExpandable
|
||||
{
|
||||
private readonly List<IRenderable> _items;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Padding Padding { get; set; } = new Padding(0, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not the columns should
|
||||
/// expand to the available space. If <c>false</c>, the column
|
||||
/// width will be auto calculated. Defaults to <c>true</c>.
|
||||
/// </summary>
|
||||
public bool Expand { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Columns"/> class.
|
||||
/// </summary>
|
||||
/// <param name="items">The items to render.</param>
|
||||
public Columns(IEnumerable<IRenderable> items)
|
||||
{
|
||||
if (items is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(items));
|
||||
}
|
||||
|
||||
_items = new List<IRenderable>(items);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Columns"/> class.
|
||||
/// </summary>
|
||||
/// <param name="items">The items to render.</param>
|
||||
public Columns(IEnumerable<string> items)
|
||||
{
|
||||
if (items is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(items));
|
||||
}
|
||||
|
||||
_items = new List<IRenderable>(items.Select(item => new Markup(item)));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
|
||||
{
|
||||
var maxPadding = Math.Max(Padding.Left, Padding.Right);
|
||||
|
||||
var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray();
|
||||
var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding);
|
||||
|
||||
var table = new Table();
|
||||
table.NoBorder();
|
||||
table.HideHeaders();
|
||||
table.PadRightCell = false;
|
||||
|
||||
if (Expand)
|
||||
{
|
||||
table.Expand();
|
||||
}
|
||||
|
||||
// Add columns
|
||||
for (var index = 0; index < columnCount; index++)
|
||||
{
|
||||
table.AddColumn(new TableColumn(string.Empty)
|
||||
{
|
||||
Padding = Padding,
|
||||
NoWrap = true,
|
||||
});
|
||||
}
|
||||
|
||||
// Add rows
|
||||
for (var start = 0; start < _items.Count; start += columnCount)
|
||||
{
|
||||
table.AddRow(_items.Skip(start).Take(columnCount).ToArray());
|
||||
}
|
||||
|
||||
return ((IRenderable)table).Render(context, maxWidth);
|
||||
}
|
||||
|
||||
// Algorithm borrowed from https://github.com/willmcgugan/rich/blob/master/rich/columns.py
|
||||
private int CalculateColumnCount(int maxWidth, int[] itemWidths, int columnCount, int padding)
|
||||
{
|
||||
var widths = new Dictionary<int, int>();
|
||||
while (columnCount > 1)
|
||||
{
|
||||
var columnIndex = 0;
|
||||
widths.Clear();
|
||||
|
||||
var exceededTotalWidth = false;
|
||||
foreach (var renderableWidth in IterateWidths(itemWidths, columnCount))
|
||||
{
|
||||
widths[columnIndex] = Math.Max(widths.ContainsKey(columnIndex) ? widths[columnIndex] : 0, renderableWidth);
|
||||
var totalWidth = widths.Values.Sum() + (padding * (widths.Count - 1));
|
||||
if (totalWidth > maxWidth)
|
||||
{
|
||||
columnCount = widths.Count - 1;
|
||||
exceededTotalWidth = true;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
columnIndex = (columnIndex + 1) % columnCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (!exceededTotalWidth)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return columnCount;
|
||||
}
|
||||
|
||||
private IEnumerable<int> IterateWidths(int[] itemWidths, int columnCount)
|
||||
{
|
||||
foreach (var width in itemWidths)
|
||||
{
|
||||
yield return width;
|
||||
}
|
||||
|
||||
if (_items.Count % columnCount != 0)
|
||||
{
|
||||
for (var i = 0; i < columnCount - (_items.Count % columnCount) - 1; i++)
|
||||
{
|
||||
yield return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
119
src/Spectre.Console/Widgets/Grid.cs
Normal file
119
src/Spectre.Console/Widgets/Grid.cs
Normal file
@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// A renderable grid.
|
||||
/// </summary>
|
||||
public sealed class Grid : Renderable, IExpandable
|
||||
{
|
||||
private readonly Table _table;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of columns in the table.
|
||||
/// </summary>
|
||||
public int ColumnCount => _table.ColumnCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of rows in the table.
|
||||
/// </summary>
|
||||
public int RowCount => _table.RowCount;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Expand
|
||||
{
|
||||
get => _table.Expand;
|
||||
set => _table.Expand = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Grid"/> class.
|
||||
/// </summary>
|
||||
public Grid()
|
||||
{
|
||||
_table = new Table
|
||||
{
|
||||
Border = Border.None,
|
||||
ShowHeaders = false,
|
||||
IsGrid = true,
|
||||
PadRightCell = false,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Measurement Measure(RenderContext context, int maxWidth)
|
||||
{
|
||||
return ((IRenderable)_table).Measure(context, maxWidth);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<Segment> Render(RenderContext context, int width)
|
||||
{
|
||||
return ((IRenderable)_table).Render(context, width);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a column to the grid.
|
||||
/// </summary>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public Grid AddColumn()
|
||||
{
|
||||
AddColumn(new GridColumn());
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a column to the grid.
|
||||
/// </summary>
|
||||
/// <param name="column">The column to add.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public Grid AddColumn(GridColumn column)
|
||||
{
|
||||
if (column is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(column));
|
||||
}
|
||||
|
||||
if (_table.RowCount > 0)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot add new columns to grid with existing rows.");
|
||||
}
|
||||
|
||||
// Only pad the most right cell if we've explicitly set a padding.
|
||||
_table.PadRightCell = column.HasExplicitPadding;
|
||||
|
||||
_table.AddColumn(new TableColumn(string.Empty)
|
||||
{
|
||||
Width = column.Width,
|
||||
NoWrap = column.NoWrap,
|
||||
Padding = column.Padding,
|
||||
Alignment = column.Alignment,
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new row to the grid.
|
||||
/// </summary>
|
||||
/// <param name="columns">The columns to add.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public Grid AddRow(params IRenderable[] columns)
|
||||
{
|
||||
if (columns is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(columns));
|
||||
}
|
||||
|
||||
if (columns.Length > _table.ColumnCount)
|
||||
{
|
||||
throw new InvalidOperationException("The number of row columns are greater than the number of grid columns.");
|
||||
}
|
||||
|
||||
_table.AddRow(columns);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
54
src/Spectre.Console/Widgets/GridColumn.cs
Normal file
54
src/Spectre.Console/Widgets/GridColumn.cs
Normal file
@ -0,0 +1,54 @@
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a grid column.
|
||||
/// </summary>
|
||||
public sealed class GridColumn : IColumn
|
||||
{
|
||||
private Padding _padding;
|
||||
|
||||
/// <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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the padding of the column.
|
||||
/// </summary>
|
||||
public Padding Padding
|
||||
{
|
||||
get => _padding;
|
||||
set
|
||||
{
|
||||
HasExplicitPadding = true;
|
||||
_padding = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alignment of the column.
|
||||
/// </summary>
|
||||
public Justify? Alignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the user
|
||||
/// has set an explicit padding for this column.
|
||||
/// </summary>
|
||||
internal bool HasExplicitPadding { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GridColumn"/> class.
|
||||
/// </summary>
|
||||
public GridColumn()
|
||||
{
|
||||
_padding = new Padding(0, 2);
|
||||
}
|
||||
}
|
||||
}
|
50
src/Spectre.Console/Widgets/Markup.cs
Normal file
50
src/Spectre.Console/Widgets/Markup.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System.Collections.Generic;
|
||||
using Spectre.Console.Internal;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// A renderable piece of markup text.
|
||||
/// </summary>
|
||||
public sealed class Markup : Renderable, IAlignable, IOverflowable
|
||||
{
|
||||
private readonly Paragraph _paragraph;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Justify? Alignment
|
||||
{
|
||||
get => _paragraph.Alignment;
|
||||
set => _paragraph.Alignment = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Overflow? Overflow
|
||||
{
|
||||
get => _paragraph.Overflow;
|
||||
set => _paragraph.Overflow = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Markup"/> class.
|
||||
/// </summary>
|
||||
/// <param name="text">The markup text.</param>
|
||||
/// <param name="style">The style of the text.</param>
|
||||
public Markup(string text, Style? style = null)
|
||||
{
|
||||
_paragraph = MarkupParser.Parse(text, style);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Measurement Measure(RenderContext context, int maxWidth)
|
||||
{
|
||||
return ((IRenderable)_paragraph).Measure(context, maxWidth);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
|
||||
{
|
||||
return ((IRenderable)_paragraph).Render(context, maxWidth);
|
||||
}
|
||||
}
|
||||
}
|
187
src/Spectre.Console/Widgets/Panel.cs
Normal file
187
src/Spectre.Console/Widgets/Panel.cs
Normal file
@ -0,0 +1,187 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// A renderable panel.
|
||||
/// </summary>
|
||||
public sealed class Panel : Renderable, IHasBorder, IExpandable, IPaddable
|
||||
{
|
||||
private const int EdgeWidth = 2;
|
||||
|
||||
private readonly IRenderable _child;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Border Border { get; set; } = Border.Square;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool UseSafeBorder { get; set; } = true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Style? BorderStyle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not the panel should
|
||||
/// fit the available space. If <c>false</c>, the panel width will be
|
||||
/// auto calculated. Defaults to <c>false</c>.
|
||||
/// </summary>
|
||||
public bool Expand { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the padding.
|
||||
/// </summary>
|
||||
public Padding Padding { get; set; } = new Padding(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the header.
|
||||
/// </summary>
|
||||
public PanelHeader? Header { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Panel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="text">The panel content.</param>
|
||||
public Panel(string text)
|
||||
: this(new Markup(text))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Panel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="content">The panel content.</param>
|
||||
public Panel(IRenderable content)
|
||||
{
|
||||
_child = content ?? throw new System.ArgumentNullException(nameof(content));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Measurement Measure(RenderContext context, int maxWidth)
|
||||
{
|
||||
var childWidth = _child.Measure(context, maxWidth);
|
||||
return new Measurement(
|
||||
childWidth.Min + EdgeWidth + Padding.GetHorizontalPadding(),
|
||||
childWidth.Max + EdgeWidth + Padding.GetHorizontalPadding());
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
|
||||
{
|
||||
var border = Border.GetSafeBorder((context.LegacyConsole || !context.Unicode) && UseSafeBorder);
|
||||
var borderStyle = BorderStyle ?? Style.Plain;
|
||||
|
||||
var paddingWidth = Padding.GetHorizontalPadding();
|
||||
var childWidth = maxWidth - EdgeWidth - paddingWidth;
|
||||
|
||||
if (!Expand)
|
||||
{
|
||||
var measurement = _child.Measure(context, maxWidth - EdgeWidth - paddingWidth);
|
||||
childWidth = measurement.Max;
|
||||
}
|
||||
|
||||
var panelWidth = childWidth + EdgeWidth + paddingWidth;
|
||||
panelWidth = Math.Min(panelWidth, maxWidth);
|
||||
|
||||
var result = new List<Segment>();
|
||||
|
||||
// Panel top
|
||||
AddTopBorder(result, context, border, borderStyle, panelWidth);
|
||||
|
||||
// Split the child segments into lines.
|
||||
var childSegments = _child.Render(context, childWidth);
|
||||
foreach (var line in Segment.SplitLines(childSegments, panelWidth))
|
||||
{
|
||||
result.Add(new Segment(border.GetPart(BorderPart.CellLeft), borderStyle));
|
||||
|
||||
// Left padding
|
||||
if (Padding.Left > 0)
|
||||
{
|
||||
result.Add(new Segment(new string(' ', Padding.Left)));
|
||||
}
|
||||
|
||||
var content = new List<Segment>();
|
||||
content.AddRange(line);
|
||||
|
||||
// Do we need to pad the panel?
|
||||
var length = line.Sum(segment => segment.CellLength(context.Encoding));
|
||||
if (length < childWidth)
|
||||
{
|
||||
var diff = childWidth - length;
|
||||
content.Add(new Segment(new string(' ', diff)));
|
||||
}
|
||||
|
||||
result.AddRange(content);
|
||||
|
||||
// Right padding
|
||||
if (Padding.Right > 0)
|
||||
{
|
||||
result.Add(new Segment(new string(' ', Padding.Right)));
|
||||
}
|
||||
|
||||
result.Add(new Segment(border.GetPart(BorderPart.CellRight), borderStyle));
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
|
||||
// Panel bottom
|
||||
AddBottomBorder(result, border, borderStyle, panelWidth);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void AddBottomBorder(List<Segment> result, Border border, Style borderStyle, int panelWidth)
|
||||
{
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft), borderStyle));
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, panelWidth - EdgeWidth), borderStyle));
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight), borderStyle));
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
|
||||
private void AddTopBorder(List<Segment> segments, RenderContext context, Border border, Style borderStyle, int panelWidth)
|
||||
{
|
||||
segments.Add(new Segment(border.GetPart(BorderPart.HeaderTopLeft), borderStyle));
|
||||
|
||||
if (Header != null)
|
||||
{
|
||||
var leftSpacing = 0;
|
||||
var rightSpacing = 0;
|
||||
|
||||
var headerWidth = panelWidth - (EdgeWidth * 2);
|
||||
var header = Segment.TruncateWithEllipsis(Header.Text, Header.Style ?? borderStyle, context.Encoding, headerWidth);
|
||||
|
||||
var excessWidth = headerWidth - header.CellLength(context.Encoding);
|
||||
if (excessWidth > 0)
|
||||
{
|
||||
switch (Header.Alignment ?? Justify.Left)
|
||||
{
|
||||
case Justify.Left:
|
||||
leftSpacing = 0;
|
||||
rightSpacing = excessWidth;
|
||||
break;
|
||||
case Justify.Right:
|
||||
leftSpacing = excessWidth;
|
||||
rightSpacing = 0;
|
||||
break;
|
||||
case Justify.Center:
|
||||
leftSpacing = excessWidth / 2;
|
||||
rightSpacing = (excessWidth / 2) + (excessWidth % 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
segments.Add(new Segment(border.GetPart(BorderPart.HeaderTop, leftSpacing + 1), borderStyle));
|
||||
segments.Add(header);
|
||||
segments.Add(new Segment(border.GetPart(BorderPart.HeaderTop, rightSpacing + 1), borderStyle));
|
||||
}
|
||||
else
|
||||
{
|
||||
segments.Add(new Segment(border.GetPart(BorderPart.HeaderTop, panelWidth - EdgeWidth), borderStyle));
|
||||
}
|
||||
|
||||
segments.Add(new Segment(border.GetPart(BorderPart.HeaderTopRight), borderStyle));
|
||||
segments.Add(Segment.LineBreak);
|
||||
}
|
||||
}
|
||||
}
|
60
src/Spectre.Console/Widgets/PanelHeader.cs
Normal file
60
src/Spectre.Console/Widgets/PanelHeader.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using System;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a header.
|
||||
/// </summary>
|
||||
public sealed class PanelHeader : IAlignable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the header text.
|
||||
/// </summary>
|
||||
public string Text { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the header style.
|
||||
/// </summary>
|
||||
public Style? Style { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the header alignment.
|
||||
/// </summary>
|
||||
public Justify? Alignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PanelHeader"/> class.
|
||||
/// </summary>
|
||||
/// <param name="text">The header text.</param>
|
||||
/// <param name="style">The header style.</param>
|
||||
/// <param name="alignment">The header alignment.</param>
|
||||
public PanelHeader(string text, Style? style = null, Justify? alignment = null)
|
||||
{
|
||||
Text = text ?? throw new ArgumentNullException(nameof(text));
|
||||
Style = style;
|
||||
Alignment = alignment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the header style.
|
||||
/// </summary>
|
||||
/// <param name="style">The header style.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public PanelHeader SetStyle(Style? style)
|
||||
{
|
||||
Style = style ?? Style.Plain;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the header alignment.
|
||||
/// </summary>
|
||||
/// <param name="alignment">The header alignment.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public PanelHeader SetAlignment(Justify alignment)
|
||||
{
|
||||
Alignment = alignment;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
303
src/Spectre.Console/Widgets/Paragraph.cs
Normal file
303
src/Spectre.Console/Widgets/Paragraph.cs
Normal file
@ -0,0 +1,303 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Internal;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// A paragraph of text where different parts
|
||||
/// of the paragraph can have individual styling.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{_text,nq}")]
|
||||
public sealed class Paragraph : Renderable, IAlignable, IOverflowable
|
||||
{
|
||||
private readonly List<SegmentLine> _lines;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alignment of the whole paragraph.
|
||||
/// </summary>
|
||||
public Justify? Alignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text overflow strategy.
|
||||
/// </summary>
|
||||
public Overflow? Overflow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Paragraph"/> class.
|
||||
/// </summary>
|
||||
public Paragraph()
|
||||
{
|
||||
_lines = new List<SegmentLine>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Paragraph"/> class.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="style">The style of the text.</param>
|
||||
public Paragraph(string text, Style? style = null)
|
||||
: this()
|
||||
{
|
||||
if (text is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
Append(text, style);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends some text to this paragraph.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to append.</param>
|
||||
/// <param name="style">The style of the appended text.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public Paragraph Append(string text, Style? style = null)
|
||||
{
|
||||
if (text is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
foreach (var (_, first, last, part) in text.SplitLines().Enumerate())
|
||||
{
|
||||
var current = part;
|
||||
|
||||
if (first)
|
||||
{
|
||||
var line = _lines.LastOrDefault();
|
||||
if (line == null)
|
||||
{
|
||||
_lines.Add(new SegmentLine());
|
||||
line = _lines.Last();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(current))
|
||||
{
|
||||
line.Add(Segment.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var span in current.SplitWords())
|
||||
{
|
||||
line.Add(new Segment(span, style ?? Style.Plain));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var line = new SegmentLine();
|
||||
|
||||
if (string.IsNullOrEmpty(current))
|
||||
{
|
||||
line.Add(Segment.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var span in current.SplitWords())
|
||||
{
|
||||
line.Add(new Segment(span, style ?? Style.Plain));
|
||||
}
|
||||
}
|
||||
|
||||
_lines.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Measurement Measure(RenderContext context, int maxWidth)
|
||||
{
|
||||
if (_lines.Count == 0)
|
||||
{
|
||||
return new Measurement(0, 0);
|
||||
}
|
||||
|
||||
var min = _lines.Max(line => line.Max(segment => segment.CellLength(context.Encoding)));
|
||||
var max = _lines.Max(x => x.CellWidth(context.Encoding));
|
||||
|
||||
return new Measurement(min, Math.Min(max, maxWidth));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (_lines.Count == 0)
|
||||
{
|
||||
return Array.Empty<Segment>();
|
||||
}
|
||||
|
||||
var lines = SplitLines(context, maxWidth);
|
||||
|
||||
// Justify lines
|
||||
var justification = context.Justification ?? Alignment ?? Justify.Left;
|
||||
foreach (var (_, _, last, line) in lines.Enumerate())
|
||||
{
|
||||
var length = line.Sum(l => l.StripLineEndings().CellLength(context.Encoding));
|
||||
if (length < maxWidth)
|
||||
{
|
||||
// Justify right side
|
||||
if (justification == Justify.Right)
|
||||
{
|
||||
var diff = maxWidth - length;
|
||||
line.Prepend(new Segment(new string(' ', diff)));
|
||||
}
|
||||
else if (justification == Justify.Center)
|
||||
{
|
||||
// Left side.
|
||||
var diff = (maxWidth - length) / 2;
|
||||
line.Prepend(new Segment(new string(' ', diff)));
|
||||
|
||||
// Right side
|
||||
line.Add(new Segment(new string(' ', diff)));
|
||||
var remainder = (maxWidth - length) % 2;
|
||||
if (remainder != 0)
|
||||
{
|
||||
line.Add(new Segment(new string(' ', remainder)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new SegmentLineEnumerator(lines);
|
||||
}
|
||||
|
||||
private List<SegmentLine> Clone()
|
||||
{
|
||||
var result = new List<SegmentLine>();
|
||||
|
||||
foreach (var line in _lines)
|
||||
{
|
||||
var newLine = new SegmentLine();
|
||||
foreach (var segment in line)
|
||||
{
|
||||
newLine.Add(segment);
|
||||
}
|
||||
|
||||
result.Add(newLine);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<SegmentLine> SplitLines(RenderContext context, int maxWidth)
|
||||
{
|
||||
if (_lines.Max(x => x.CellWidth(context.Encoding)) <= maxWidth)
|
||||
{
|
||||
return Clone();
|
||||
}
|
||||
|
||||
var lines = new List<SegmentLine>();
|
||||
var line = new SegmentLine();
|
||||
|
||||
var newLine = true;
|
||||
|
||||
using var iterator = new SegmentLineIterator(_lines);
|
||||
var queue = new Queue<Segment>();
|
||||
while (true)
|
||||
{
|
||||
var current = (Segment?)null;
|
||||
if (queue.Count == 0)
|
||||
{
|
||||
if (!iterator.MoveNext())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
current = iterator.Current;
|
||||
}
|
||||
else
|
||||
{
|
||||
current = queue.Dequeue();
|
||||
}
|
||||
|
||||
if (current == null)
|
||||
{
|
||||
throw new InvalidOperationException("Iterator returned empty segment.");
|
||||
}
|
||||
|
||||
if (newLine && current.IsWhiteSpace && !current.IsLineBreak)
|
||||
{
|
||||
newLine = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
newLine = false;
|
||||
|
||||
if (current.IsLineBreak)
|
||||
{
|
||||
line.Add(current);
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
newLine = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
var length = current.CellLength(context.Encoding);
|
||||
if (length > maxWidth)
|
||||
{
|
||||
// The current segment is longer than the width of the console,
|
||||
// so we will need to crop it up, into new segments.
|
||||
var segments = Segment.SplitOverflow(current, Overflow, context.Encoding, maxWidth);
|
||||
if (segments.Count > 0)
|
||||
{
|
||||
if (line.CellWidth(context.Encoding) + segments[0].CellLength(context.Encoding) > maxWidth)
|
||||
{
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
newLine = true;
|
||||
|
||||
segments.ForEach(s => queue.Enqueue(s));
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add the segment and push the rest of them to the queue.
|
||||
line.Add(segments[0]);
|
||||
segments.Skip(1).ForEach(s => queue.Enqueue(s));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (line.CellWidth(context.Encoding) + length > maxWidth)
|
||||
{
|
||||
line.Add(Segment.Empty);
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
newLine = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (newLine && current.IsWhiteSpace)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
newLine = false;
|
||||
|
||||
line.Add(current);
|
||||
}
|
||||
|
||||
// Flush remaining.
|
||||
if (line.Count > 0)
|
||||
{
|
||||
lines.Add(line);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
}
|
456
src/Spectre.Console/Widgets/Table.cs
Normal file
456
src/Spectre.Console/Widgets/Table.cs
Normal file
@ -0,0 +1,456 @@
|
||||
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, IHasBorder, IExpandable
|
||||
{
|
||||
private const int EdgeCount = 2;
|
||||
|
||||
private readonly List<TableColumn> _columns;
|
||||
private readonly List<List<IRenderable>> _rows;
|
||||
|
||||
/// <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;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Border Border { get; set; } = Border.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 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; }
|
||||
|
||||
// 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<List<IRenderable>>();
|
||||
}
|
||||
|
||||
/// <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(params IRenderable[] columns)
|
||||
{
|
||||
if (columns is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(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.ToList());
|
||||
|
||||
// Need to add missing columns?
|
||||
if (columns.Length < _columns.Count)
|
||||
{
|
||||
var diff = _columns.Count - columns.Length;
|
||||
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 showBorder = Border.Visible;
|
||||
var hideBorder = !Border.Visible;
|
||||
var hasRows = _rows.Count > 0;
|
||||
|
||||
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);
|
||||
|
||||
var rows = new List<List<IRenderable>>();
|
||||
if (ShowHeaders)
|
||||
{
|
||||
// Add columns to top of rows
|
||||
rows.Add(new List<IRenderable>(_columns.Select(c => c.Text)));
|
||||
}
|
||||
|
||||
// Add rows.
|
||||
rows.AddRange(_rows);
|
||||
|
||||
// Iterate all rows
|
||||
var result = new List<Segment>();
|
||||
foreach (var (index, firstRow, lastRow, row) in rows.Enumerate())
|
||||
{
|
||||
var cellHeight = 1;
|
||||
|
||||
// Get the list of cells for the row 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(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), borderStyle));
|
||||
foreach (var (columnIndex, _, lastColumn, columnWidth) in columnWidths.Enumerate())
|
||||
{
|
||||
var padding = _columns[columnIndex].Padding;
|
||||
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, padding.Left), borderStyle)); // Left padding
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, columnWidth), borderStyle));
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, padding.Right), borderStyle)); // Right padding
|
||||
|
||||
if (!lastColumn)
|
||||
{
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderTopSeparator), borderStyle));
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderTopRight), borderStyle));
|
||||
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
|
||||
var part = firstRow && ShowHeaders ? BorderPart.HeaderLeft : BorderPart.CellLeft;
|
||||
result.Add(new Segment(border.GetPart(part), borderStyle));
|
||||
}
|
||||
|
||||
// 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) || (hideBorder && lastCell && IsGrid && PadRightCell))
|
||||
{
|
||||
var rightPadding = _columns[cellIndex].Padding.Right;
|
||||
if (rightPadding > 0)
|
||||
{
|
||||
result.Add(new Segment(new string(' ', rightPadding)));
|
||||
}
|
||||
}
|
||||
|
||||
if (lastCell && showBorder)
|
||||
{
|
||||
// Add right column edge
|
||||
var part = firstRow && ShowHeaders ? BorderPart.HeaderRight : BorderPart.CellRight;
|
||||
result.Add(new Segment(border.GetPart(part), borderStyle));
|
||||
}
|
||||
else if (showBorder)
|
||||
{
|
||||
// Add column separator
|
||||
var part = firstRow && ShowHeaders ? BorderPart.HeaderSeparator : BorderPart.CellSeparator;
|
||||
result.Add(new Segment(border.GetPart(part), borderStyle));
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
|
||||
// Show header separator?
|
||||
if (firstRow && showBorder && ShowHeaders && hasRows)
|
||||
{
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomLeft), borderStyle));
|
||||
foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
|
||||
{
|
||||
var padding = _columns[columnIndex].Padding;
|
||||
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom, padding.Left), borderStyle)); // Left padding
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom, columnWidth), borderStyle));
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom, padding.Right), borderStyle)); // Right padding
|
||||
|
||||
if (!lastColumn)
|
||||
{
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomSeparator), borderStyle));
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomRight), borderStyle));
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
|
||||
// Show bottom of footer?
|
||||
if (lastRow && showBorder)
|
||||
{
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft), borderStyle));
|
||||
foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
|
||||
{
|
||||
var padding = _columns[columnIndex].Padding;
|
||||
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, padding.Left), borderStyle)); // Left padding
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, columnWidth), borderStyle));
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, padding.Right), borderStyle)); // Right padding
|
||||
|
||||
if (!lastColumn)
|
||||
{
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomSeparator), borderStyle));
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight), borderStyle));
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
}
|
||||
|
||||
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 (int Min, int Max) MeasureColumn(TableColumn column, RenderContext options, int maxWidth)
|
||||
{
|
||||
var padding = column.Padding.GetHorizontalPadding();
|
||||
|
||||
// Predetermined width?
|
||||
if (column.Width != null)
|
||||
{
|
||||
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>();
|
||||
|
||||
// Include columns in measurement
|
||||
var measure = column.Text.Measure(options, maxWidth);
|
||||
minWidths.Add(measure.Min);
|
||||
maxWidths.Add(measure.Max);
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
measure = row.Measure(options, maxWidth);
|
||||
minWidths.Add(measure.Min);
|
||||
maxWidths.Add(measure.Max);
|
||||
}
|
||||
|
||||
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.GetHorizontalPadding()).Sum() : 0;
|
||||
|
||||
if (!PadRightCell)
|
||||
{
|
||||
padding -= _columns.Last().Padding.Right;
|
||||
}
|
||||
|
||||
return separators + edges + padding;
|
||||
}
|
||||
|
||||
private bool ShouldExpand()
|
||||
{
|
||||
return Expand || Width != null;
|
||||
}
|
||||
}
|
||||
}
|
60
src/Spectre.Console/Widgets/TableColumn.cs
Normal file
60
src/Spectre.Console/Widgets/TableColumn.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a table column.
|
||||
/// </summary>
|
||||
public sealed class TableColumn : IColumn
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the text associated with the column.
|
||||
/// </summary>
|
||||
public IRenderable 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 padding of the column.
|
||||
/// </summary>
|
||||
public Padding Padding { 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>
|
||||
/// Gets or sets the alignment of the column.
|
||||
/// </summary>
|
||||
public Justify? Alignment { 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)
|
||||
: this(new Markup(text))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TableColumn"/> class.
|
||||
/// </summary>
|
||||
/// <param name="renderable">The <see cref="IRenderable"/> instance to use as the table column.</param>
|
||||
public TableColumn(IRenderable renderable)
|
||||
{
|
||||
Text = renderable ?? throw new ArgumentNullException(nameof(renderable));
|
||||
Width = null;
|
||||
Padding = new Padding(1, 1);
|
||||
NoWrap = false;
|
||||
Alignment = null;
|
||||
}
|
||||
}
|
||||
}
|
62
src/Spectre.Console/Widgets/Text.cs
Normal file
62
src/Spectre.Console/Widgets/Text.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// A renderable piece of text.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{_text,nq}")]
|
||||
[SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
|
||||
public sealed class Text : Renderable, IAlignable, IOverflowable
|
||||
{
|
||||
private readonly Paragraph _paragraph;
|
||||
|
||||
/// <summary>
|
||||
/// Gets an empty <see cref="Text"/> instance.
|
||||
/// </summary>
|
||||
public static Text Empty { get; } = new Text(string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Text"/> class.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="style">The style of the text.</param>
|
||||
public Text(string text, Style? style = null)
|
||||
{
|
||||
_paragraph = new Paragraph(text, style);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text alignment.
|
||||
/// </summary>
|
||||
public Justify? Alignment
|
||||
{
|
||||
get => _paragraph.Alignment;
|
||||
set => _paragraph.Alignment = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text overflow strategy.
|
||||
/// </summary>
|
||||
public Overflow? Overflow
|
||||
{
|
||||
get => _paragraph.Overflow;
|
||||
set => _paragraph.Overflow = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Measurement Measure(RenderContext context, int maxWidth)
|
||||
{
|
||||
return ((IRenderable)_paragraph).Measure(context, maxWidth);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
|
||||
{
|
||||
return ((IRenderable)_paragraph).Render(context, maxWidth);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user