2021-12-22 08:51:17 -05:00

182 lines
5.6 KiB
C#

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, 0, 1, 0);
/// <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 as columns.</param>
public Columns(params IRenderable[] items)
: this((IEnumerable<IRenderable>)items)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Columns"/> class.
/// </summary>
/// <param name="items">The items to render as columns.</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 Measurement Measure(RenderContext context, int maxWidth)
{
var maxPadding = Math.Max(Padding.GetLeftSafe(), Padding.GetRightSafe());
var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray();
var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding);
if (columnCount == 0)
{
// Temporary work around for extremely small consoles
return new Measurement(maxWidth, maxWidth);
}
var rows = _items.Count / Math.Max(columnCount, 1);
var greatestWidth = 0;
for (var row = 0; row < rows; row += Math.Max(1, columnCount))
{
var widths = itemWidths.Skip(row * columnCount).Take(columnCount).ToList();
var totalWidth = widths.Sum() + (maxPadding * (widths.Count - 1));
if (totalWidth > greatestWidth)
{
greatestWidth = totalWidth;
}
}
return new Measurement(greatestWidth, greatestWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var maxPadding = Math.Max(Padding.GetLeftSafe(), Padding.GetRightSafe());
var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray();
var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding);
if (columnCount == 0)
{
// Temporary work around for extremely small consoles
columnCount = 1;
}
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;
}
}
}
}