using System; using System.Collections.Generic; using System.Linq; using Spectre.Console.Rendering; namespace Spectre.Console { /// /// Renders things in columns. /// public sealed class Columns : Renderable, IPaddable, IExpandable { private readonly List _items; /// public Padding? Padding { get; set; } = new Padding(0, 0, 1, 0); /// /// Gets or sets a value indicating whether or not the columns should /// expand to the available space. If false, the column /// width will be auto calculated. Defaults to true. /// public bool Expand { get; set; } = true; /// /// Initializes a new instance of the class. /// /// The items to render as columns. public Columns(params IRenderable[] items) : this((IEnumerable)items) { } /// /// Initializes a new instance of the class. /// /// The items to render as columns. public Columns(IEnumerable items) { if (items is null) { throw new ArgumentNullException(nameof(items)); } _items = new List(items); } /// /// Initializes a new instance of the class. /// /// The items to render. public Columns(IEnumerable items) { if (items is null) { throw new ArgumentNullException(nameof(items)); } _items = new List(items.Select(item => new Markup(item))); } /// 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); } /// protected override IEnumerable 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(); 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 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; } } } } }