mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-06-19 21:38:16 +08:00

committed by
Patrik Svensson

parent
effdecb1d4
commit
8a01b93aca
61
src/Spectre.Console/Rendering/AlignableExtensions.cs
Normal file
61
src/Spectre.Console/Rendering/AlignableExtensions.cs
Normal file
@ -0,0 +1,61 @@
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="IAlignable"/>.
|
||||
/// </summary>
|
||||
public static class AlignableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the alignment for an <see cref="IAlignable"/> object.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The alignable type.</typeparam>
|
||||
/// <param name="alignable">The alignable object.</param>
|
||||
/// <param name="alignment">The alignment.</param>
|
||||
/// <returns>The same alignable object.</returns>
|
||||
public static T WithAlignment<T>(this T alignable, Justify alignment)
|
||||
where T : IAlignable
|
||||
{
|
||||
alignable.Alignment = alignment;
|
||||
return alignable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="IAlignable"/> object to be left aligned.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The alignable type.</typeparam>
|
||||
/// <param name="alignable">The alignable object.</param>
|
||||
/// <returns>The same alignable object.</returns>
|
||||
public static T LeftAligned<T>(this T alignable)
|
||||
where T : IAlignable
|
||||
{
|
||||
alignable.Alignment = Justify.Left;
|
||||
return alignable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="IAlignable"/> object to be centered.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The alignable type.</typeparam>
|
||||
/// <param name="alignable">The alignable object.</param>
|
||||
/// <returns>The same alignable object.</returns>
|
||||
public static T Centered<T>(this T alignable)
|
||||
where T : IAlignable
|
||||
{
|
||||
alignable.Alignment = Justify.Center;
|
||||
return alignable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="IAlignable"/> object to be right aligned.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The alignable type.</typeparam>
|
||||
/// <param name="alignable">The alignable object.</param>
|
||||
/// <returns>The same alignable object.</returns>
|
||||
public static T RightAligned<T>(this T alignable)
|
||||
where T : IAlignable
|
||||
{
|
||||
alignable.Alignment = Justify.Right;
|
||||
return alignable;
|
||||
}
|
||||
}
|
||||
}
|
108
src/Spectre.Console/Rendering/Border.cs
Normal file
108
src/Spectre.Console/Rendering/Border.cs
Normal file
@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Spectre.Console.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a border used by tables.
|
||||
/// </summary>
|
||||
public abstract class Border
|
||||
{
|
||||
private readonly Dictionary<BorderPart, string> _lookup;
|
||||
|
||||
private static readonly Dictionary<BorderKind, Border> _borders = new Dictionary<BorderKind, Border>
|
||||
{
|
||||
{ BorderKind.None, new NoBorder() },
|
||||
{ BorderKind.Ascii, new AsciiBorder() },
|
||||
{ BorderKind.Square, new SquareBorder() },
|
||||
{ BorderKind.Rounded, new RoundedBorder() },
|
||||
};
|
||||
|
||||
private static readonly Dictionary<BorderKind, BorderKind> _safeLookup = new Dictionary<BorderKind, BorderKind>
|
||||
{
|
||||
{ BorderKind.Rounded, BorderKind.Square },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Border"/> class.
|
||||
/// </summary>
|
||||
protected Border()
|
||||
{
|
||||
_lookup = Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="Border"/> represented by the specified <see cref="BorderKind"/>.
|
||||
/// </summary>
|
||||
/// <param name="kind">The kind of border to get.</param>
|
||||
/// <param name="safe">Whether or not to get a "safe" border that can be rendered in a legacy console.</param>
|
||||
/// <returns>A <see cref="Border"/> instance representing the specified <see cref="BorderKind"/>.</returns>
|
||||
public static Border GetBorder(BorderKind kind, bool safe)
|
||||
{
|
||||
if (safe && _safeLookup.TryGetValue(kind, out var safeKind))
|
||||
{
|
||||
kind = safeKind;
|
||||
}
|
||||
|
||||
if (!_borders.TryGetValue(kind, out var border))
|
||||
{
|
||||
throw new InvalidOperationException("Unknown border kind");
|
||||
}
|
||||
|
||||
return border;
|
||||
}
|
||||
|
||||
private Dictionary<BorderPart, string> Initialize()
|
||||
{
|
||||
var lookup = new Dictionary<BorderPart, string>();
|
||||
foreach (BorderPart? part in Enum.GetValues(typeof(BorderPart)))
|
||||
{
|
||||
if (part == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var text = GetBoxPart(part.Value);
|
||||
if (text.Length > 1)
|
||||
{
|
||||
throw new InvalidOperationException("A box part cannot contain more than one character.");
|
||||
}
|
||||
|
||||
lookup.Add(part.Value, GetBoxPart(part.Value));
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string representation of a specific border part.
|
||||
/// </summary>
|
||||
/// <param name="part">The part to get a string representation for.</param>
|
||||
/// <param name="count">The number of repetitions.</param>
|
||||
/// <returns>A string representation of the specified border part.</returns>
|
||||
public string GetPart(BorderPart part, int count)
|
||||
{
|
||||
// TODO: This need some optimization...
|
||||
return string.Join(string.Empty, Enumerable.Repeat(GetBoxPart(part)[0], count));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string representation of a specific border part.
|
||||
/// </summary>
|
||||
/// <param name="part">The part to get a string representation for.</param>
|
||||
/// <returns>A string representation of the specified border part.</returns>
|
||||
public string GetPart(BorderPart part)
|
||||
{
|
||||
return _lookup[part].ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the character representing the specified border part.
|
||||
/// </summary>
|
||||
/// <param name="part">The part to get the character representation for.</param>
|
||||
/// <returns>A character representation of the specified border part.</returns>
|
||||
protected abstract string GetBoxPart(BorderPart part);
|
||||
}
|
||||
}
|
28
src/Spectre.Console/Rendering/BorderKind.cs
Normal file
28
src/Spectre.Console/Rendering/BorderKind.cs
Normal file
@ -0,0 +1,28 @@
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents different kinds of borders.
|
||||
/// </summary>
|
||||
public enum BorderKind
|
||||
{
|
||||
/// <summary>
|
||||
/// No border.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// A square border.
|
||||
/// </summary>
|
||||
Square = 1,
|
||||
|
||||
/// <summary>
|
||||
/// An old school ASCII border.
|
||||
/// </summary>
|
||||
Ascii = 2,
|
||||
|
||||
/// <summary>
|
||||
/// A rounded border.
|
||||
/// </summary>
|
||||
Rounded = 3,
|
||||
}
|
||||
}
|
98
src/Spectre.Console/Rendering/BorderPart.cs
Normal file
98
src/Spectre.Console/Rendering/BorderPart.cs
Normal file
@ -0,0 +1,98 @@
|
||||
namespace Spectre.Console.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the different border parts.
|
||||
/// </summary>
|
||||
public enum BorderPart
|
||||
{
|
||||
/// <summary>
|
||||
/// The top left part of a header.
|
||||
/// </summary>
|
||||
HeaderTopLeft,
|
||||
|
||||
/// <summary>
|
||||
/// The top part of a header.
|
||||
/// </summary>
|
||||
HeaderTop,
|
||||
|
||||
/// <summary>
|
||||
/// The top separator part of a header.
|
||||
/// </summary>
|
||||
HeaderTopSeparator,
|
||||
|
||||
/// <summary>
|
||||
/// The top right part of a header.
|
||||
/// </summary>
|
||||
HeaderTopRight,
|
||||
|
||||
/// <summary>
|
||||
/// The left part of a header.
|
||||
/// </summary>
|
||||
HeaderLeft,
|
||||
|
||||
/// <summary>
|
||||
/// A header separator.
|
||||
/// </summary>
|
||||
HeaderSeparator,
|
||||
|
||||
/// <summary>
|
||||
/// The right part of a header.
|
||||
/// </summary>
|
||||
HeaderRight,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom left part of a header.
|
||||
/// </summary>
|
||||
HeaderBottomLeft,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom part of a header.
|
||||
/// </summary>
|
||||
HeaderBottom,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom separator part of a header.
|
||||
/// </summary>
|
||||
HeaderBottomSeparator,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom right part of a header.
|
||||
/// </summary>
|
||||
HeaderBottomRight,
|
||||
|
||||
/// <summary>
|
||||
/// The left part of a cell.
|
||||
/// </summary>
|
||||
CellLeft,
|
||||
|
||||
/// <summary>
|
||||
/// A cell separator.
|
||||
/// </summary>
|
||||
CellSeparator,
|
||||
|
||||
/// <summary>
|
||||
/// The right part of a cell.
|
||||
/// </summary>
|
||||
CellRight,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom left part of a footer.
|
||||
/// </summary>
|
||||
FooterBottomLeft,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom part of a footer.
|
||||
/// </summary>
|
||||
FooterBottom,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom separator part of a footer.
|
||||
/// </summary>
|
||||
FooterBottomSeparator,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom right part of a footer.
|
||||
/// </summary>
|
||||
FooterBottomRight,
|
||||
}
|
||||
}
|
37
src/Spectre.Console/Rendering/Borders/AsciiBorder.cs
Normal file
37
src/Spectre.Console/Rendering/Borders/AsciiBorder.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace Spectre.Console.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an old school ASCII border.
|
||||
/// </summary>
|
||||
public sealed class AsciiBorder : Border
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override string GetBoxPart(BorderPart part)
|
||||
{
|
||||
return part switch
|
||||
{
|
||||
BorderPart.HeaderTopLeft => "+",
|
||||
BorderPart.HeaderTop => "-",
|
||||
BorderPart.HeaderTopSeparator => "-",
|
||||
BorderPart.HeaderTopRight => "+",
|
||||
BorderPart.HeaderLeft => "|",
|
||||
BorderPart.HeaderSeparator => "|",
|
||||
BorderPart.HeaderRight => "|",
|
||||
BorderPart.HeaderBottomLeft => "|",
|
||||
BorderPart.HeaderBottom => "-",
|
||||
BorderPart.HeaderBottomSeparator => "+",
|
||||
BorderPart.HeaderBottomRight => "|",
|
||||
BorderPart.CellLeft => "|",
|
||||
BorderPart.CellSeparator => "|",
|
||||
BorderPart.CellRight => "|",
|
||||
BorderPart.FooterBottomLeft => "+",
|
||||
BorderPart.FooterBottom => "-",
|
||||
BorderPart.FooterBottomSeparator => "-",
|
||||
BorderPart.FooterBottomRight => "+",
|
||||
_ => throw new InvalidOperationException("Unknown box part."),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
14
src/Spectre.Console/Rendering/Borders/NoBorder.cs
Normal file
14
src/Spectre.Console/Rendering/Borders/NoBorder.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace Spectre.Console.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an invisible border.
|
||||
/// </summary>
|
||||
public sealed class NoBorder : Border
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override string GetBoxPart(BorderPart part)
|
||||
{
|
||||
return " ";
|
||||
}
|
||||
}
|
||||
}
|
37
src/Spectre.Console/Rendering/Borders/RoundedBorder.cs
Normal file
37
src/Spectre.Console/Rendering/Borders/RoundedBorder.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace Spectre.Console.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a rounded border.
|
||||
/// </summary>
|
||||
public sealed class RoundedBorder : Border
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override string GetBoxPart(BorderPart part)
|
||||
{
|
||||
return part switch
|
||||
{
|
||||
BorderPart.HeaderTopLeft => "╭",
|
||||
BorderPart.HeaderTop => "─",
|
||||
BorderPart.HeaderTopSeparator => "┬",
|
||||
BorderPart.HeaderTopRight => "╮",
|
||||
BorderPart.HeaderLeft => "│",
|
||||
BorderPart.HeaderSeparator => "│",
|
||||
BorderPart.HeaderRight => "│",
|
||||
BorderPart.HeaderBottomLeft => "├",
|
||||
BorderPart.HeaderBottom => "─",
|
||||
BorderPart.HeaderBottomSeparator => "┼",
|
||||
BorderPart.HeaderBottomRight => "┤",
|
||||
BorderPart.CellLeft => "│",
|
||||
BorderPart.CellSeparator => "│",
|
||||
BorderPart.CellRight => "│",
|
||||
BorderPart.FooterBottomLeft => "╰",
|
||||
BorderPart.FooterBottom => "─",
|
||||
BorderPart.FooterBottomSeparator => "┴",
|
||||
BorderPart.FooterBottomRight => "╯",
|
||||
_ => throw new InvalidOperationException("Unknown box part."),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
37
src/Spectre.Console/Rendering/Borders/SquareBorder.cs
Normal file
37
src/Spectre.Console/Rendering/Borders/SquareBorder.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace Spectre.Console.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a square border.
|
||||
/// </summary>
|
||||
public sealed class SquareBorder : Border
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override string GetBoxPart(BorderPart part)
|
||||
{
|
||||
return part switch
|
||||
{
|
||||
BorderPart.HeaderTopLeft => "┌",
|
||||
BorderPart.HeaderTop => "─",
|
||||
BorderPart.HeaderTopSeparator => "┬",
|
||||
BorderPart.HeaderTopRight => "┐",
|
||||
BorderPart.HeaderLeft => "│",
|
||||
BorderPart.HeaderSeparator => "│",
|
||||
BorderPart.HeaderRight => "│",
|
||||
BorderPart.HeaderBottomLeft => "├",
|
||||
BorderPart.HeaderBottom => "─",
|
||||
BorderPart.HeaderBottomSeparator => "┼",
|
||||
BorderPart.HeaderBottomRight => "┤",
|
||||
BorderPart.CellLeft => "│",
|
||||
BorderPart.CellSeparator => "│",
|
||||
BorderPart.CellRight => "│",
|
||||
BorderPart.FooterBottomLeft => "└",
|
||||
BorderPart.FooterBottom => "─",
|
||||
BorderPart.FooterBottomSeparator => "┴",
|
||||
BorderPart.FooterBottomRight => "┘",
|
||||
_ => throw new InvalidOperationException("Unknown box part."),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
141
src/Spectre.Console/Rendering/Grid.cs
Normal file
141
src/Spectre.Console/Rendering/Grid.cs
Normal file
@ -0,0 +1,141 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Internal;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// A renderable grid.
|
||||
/// </summary>
|
||||
public sealed class Grid : Renderable
|
||||
{
|
||||
private readonly Table _table;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Grid"/> class.
|
||||
/// </summary>
|
||||
public Grid()
|
||||
{
|
||||
_table = new Table
|
||||
{
|
||||
Border = BorderKind.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>
|
||||
public void AddColumn()
|
||||
{
|
||||
AddColumn(new GridColumn());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a column to the grid.
|
||||
/// </summary>
|
||||
/// <param name="column">The column to add.</param>
|
||||
public void 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.Padding != null;
|
||||
|
||||
_table.AddColumn(new TableColumn(string.Empty)
|
||||
{
|
||||
Width = column.Width,
|
||||
NoWrap = column.NoWrap,
|
||||
Padding = column.Padding ?? new Padding(0, 2),
|
||||
Alignment = column.Alignment,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a column to the grid.
|
||||
/// </summary>
|
||||
/// <param name="count">The number of columns to add.</param>
|
||||
public void AddColumns(int count)
|
||||
{
|
||||
for (var index = 0; index < count; index++)
|
||||
{
|
||||
AddColumn(new GridColumn());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a column to the grid.
|
||||
/// </summary>
|
||||
/// <param name="columns">The columns to add.</param>
|
||||
public void AddColumns(params GridColumn[] columns)
|
||||
{
|
||||
if (columns is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(columns));
|
||||
}
|
||||
|
||||
foreach (var column in columns)
|
||||
{
|
||||
AddColumn(column);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an empty row to the grid.
|
||||
/// </summary>
|
||||
public void AddEmptyRow()
|
||||
{
|
||||
var columns = new IRenderable[_table.ColumnCount];
|
||||
Enumerable.Range(0, _table.ColumnCount).ForEach(index => columns[index] = Text.Empty);
|
||||
AddRow(columns);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new row to the grid.
|
||||
/// </summary>
|
||||
/// <param name="columns">The columns to add.</param>
|
||||
public void 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 less than the number of grid 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);
|
||||
}
|
||||
}
|
||||
}
|
30
src/Spectre.Console/Rendering/GridColumn.cs
Normal file
30
src/Spectre.Console/Rendering/GridColumn.cs
Normal file
@ -0,0 +1,30 @@
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a grid column.
|
||||
/// </summary>
|
||||
public sealed class GridColumn : IAlignable
|
||||
{
|
||||
/// <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; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alignment of the column.
|
||||
/// </summary>
|
||||
public Justify? Alignment { get; set; }
|
||||
}
|
||||
}
|
31
src/Spectre.Console/Rendering/GridExtensions.cs
Normal file
31
src/Spectre.Console/Rendering/GridExtensions.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="Grid"/>.
|
||||
/// </summary>
|
||||
public static class GridExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a new row to the grid.
|
||||
/// </summary>
|
||||
/// <param name="grid">The grid to add the row to.</param>
|
||||
/// <param name="columns">The columns to add.</param>
|
||||
public static void AddRow(this Grid grid, params string[] columns)
|
||||
{
|
||||
if (grid is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(grid));
|
||||
}
|
||||
|
||||
if (columns is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(columns));
|
||||
}
|
||||
|
||||
grid.AddRow(columns.Select(column => new Markup(column)).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
13
src/Spectre.Console/Rendering/IAlignable.cs
Normal file
13
src/Spectre.Console/Rendering/IAlignable.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents something that is alignable.
|
||||
/// </summary>
|
||||
public interface IAlignable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the alignment.
|
||||
/// </summary>
|
||||
Justify? Alignment { get; set; }
|
||||
}
|
||||
}
|
26
src/Spectre.Console/Rendering/IRenderable.cs
Normal file
26
src/Spectre.Console/Rendering/IRenderable.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Spectre.Console.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents something that can be rendered to the console.
|
||||
/// </summary>
|
||||
public interface IRenderable
|
||||
{
|
||||
/// <summary>
|
||||
/// Measures the renderable object.
|
||||
/// </summary>
|
||||
/// <param name="context">The render context.</param>
|
||||
/// <param name="maxWidth">The maximum allowed width.</param>
|
||||
/// <returns>The minimum and maximum width of the object.</returns>
|
||||
Measurement Measure(RenderContext context, int maxWidth);
|
||||
|
||||
/// <summary>
|
||||
/// Renders the object.
|
||||
/// </summary>
|
||||
/// <param name="context">The render context.</param>
|
||||
/// <param name="maxWidth">The maximum allowed width.</param>
|
||||
/// <returns>A collection of segments.</returns>
|
||||
IEnumerable<Segment> Render(RenderContext context, int maxWidth);
|
||||
}
|
||||
}
|
23
src/Spectre.Console/Rendering/Justify.cs
Normal file
23
src/Spectre.Console/Rendering/Justify.cs
Normal file
@ -0,0 +1,23 @@
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents text justification.
|
||||
/// </summary>
|
||||
public enum Justify
|
||||
{
|
||||
/// <summary>
|
||||
/// Left aligned.
|
||||
/// </summary>
|
||||
Left = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Right aligned.
|
||||
/// </summary>
|
||||
Right = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Centered.
|
||||
/// </summary>
|
||||
Center = 2,
|
||||
}
|
||||
}
|
43
src/Spectre.Console/Rendering/Markup.cs
Normal file
43
src/Spectre.Console/Rendering/Markup.cs
Normal file
@ -0,0 +1,43 @@
|
||||
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
|
||||
{
|
||||
private readonly Text _text;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Justify? Alignment
|
||||
{
|
||||
get => _text.Alignment;
|
||||
set => _text.Alignment = 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)
|
||||
{
|
||||
_text = MarkupParser.Parse(text, style);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Measurement Measure(RenderContext context, int maxWidth)
|
||||
{
|
||||
return ((IRenderable)_text).Measure(context, maxWidth);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
|
||||
{
|
||||
return ((IRenderable)_text).Render(context, maxWidth);
|
||||
}
|
||||
}
|
||||
}
|
77
src/Spectre.Console/Rendering/Measurement.cs
Normal file
77
src/Spectre.Console/Rendering/Measurement.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
|
||||
namespace Spectre.Console.Rendering
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
86
src/Spectre.Console/Rendering/Padding.cs
Normal file
86
src/Spectre.Console/Rendering/Padding.cs
Normal file
@ -0,0 +1,86 @@
|
||||
using System;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a measurement.
|
||||
/// </summary>
|
||||
public struct Padding : IEquatable<Padding>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the left padding.
|
||||
/// </summary>
|
||||
public int Left { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the right padding.
|
||||
/// </summary>
|
||||
public int Right { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Padding"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="left">The left padding.</param>
|
||||
/// <param name="right">The right padding.</param>
|
||||
public Padding(int left, int right)
|
||||
{
|
||||
Left = left;
|
||||
Right = right;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is Padding padding && Equals(padding);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var hash = (int)2166136261;
|
||||
hash = (hash * 16777619) ^ Left.GetHashCode();
|
||||
hash = (hash * 16777619) ^ Right.GetHashCode();
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(Padding other)
|
||||
{
|
||||
return Left == other.Left && Right == other.Right;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if two <see cref="Padding"/> instances are equal.
|
||||
/// </summary>
|
||||
/// <param name="left">The first <see cref="Padding"/> instance to compare.</param>
|
||||
/// <param name="right">The second <see cref="Padding"/> instance to compare.</param>
|
||||
/// <returns><c>true</c> if the two instances are equal, otherwise <c>false</c>.</returns>
|
||||
public static bool operator ==(Padding left, Padding right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if two <see cref="Padding"/> instances are not equal.
|
||||
/// </summary>
|
||||
/// <param name="left">The first <see cref="Padding"/> instance to compare.</param>
|
||||
/// <param name="right">The second <see cref="Padding"/> instance to compare.</param>
|
||||
/// <returns><c>true</c> if the two instances are not equal, otherwise <c>false</c>.</returns>
|
||||
public static bool operator !=(Padding left, Padding right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the horizontal padding.
|
||||
/// </summary>
|
||||
/// <returns>The horizontal padding.</returns>
|
||||
public int GetHorizontalPadding()
|
||||
{
|
||||
return Left + Right;
|
||||
}
|
||||
}
|
||||
}
|
145
src/Spectre.Console/Rendering/Panel.cs
Normal file
145
src/Spectre.Console/Rendering/Panel.cs
Normal file
@ -0,0 +1,145 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Rendering;
|
||||
using SpectreBorder = Spectre.Console.Rendering.Border;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// A renderable panel.
|
||||
/// </summary>
|
||||
public sealed class Panel : Renderable
|
||||
{
|
||||
private const int EdgeWidth = 2;
|
||||
|
||||
private readonly IRenderable _child;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not to use
|
||||
/// a "safe" border on legacy consoles that might not be able
|
||||
/// to render non-ASCII characters. Defaults to <c>true</c>.
|
||||
/// </summary>
|
||||
public bool SafeBorder { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the kind of border to use.
|
||||
/// </summary>
|
||||
public BorderKind Border { get; set; } = BorderKind.Square;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alignment of the panel contents.
|
||||
/// </summary>
|
||||
public Justify? Alignment { 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>
|
||||
/// 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 + 2 + Padding.GetHorizontalPadding(),
|
||||
childWidth.Max + 2 + Padding.GetHorizontalPadding());
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
|
||||
{
|
||||
var border = SpectreBorder.GetBorder(Border, (context.LegacyConsole || !context.Unicode) && SafeBorder);
|
||||
|
||||
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 + paddingWidth;
|
||||
|
||||
// Panel top
|
||||
var result = new List<Segment>
|
||||
{
|
||||
new Segment(border.GetPart(BorderPart.HeaderTopLeft)),
|
||||
new Segment(border.GetPart(BorderPart.HeaderTop, panelWidth)),
|
||||
new Segment(border.GetPart(BorderPart.HeaderTopRight)),
|
||||
Segment.LineBreak,
|
||||
};
|
||||
|
||||
// Render the child.
|
||||
var childContext = context.WithJustification(Alignment);
|
||||
var childSegments = _child.Render(childContext, childWidth);
|
||||
|
||||
// Split the child segments into lines.
|
||||
foreach (var line in Segment.SplitLines(childSegments, panelWidth))
|
||||
{
|
||||
result.Add(new Segment(border.GetPart(BorderPart.CellLeft)));
|
||||
|
||||
// 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)));
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
|
||||
// Panel bottom
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft)));
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, panelWidth)));
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight)));
|
||||
result.Add(Segment.LineBreak);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
54
src/Spectre.Console/Rendering/RenderContext.cs
Normal file
54
src/Spectre.Console/Rendering/RenderContext.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Spectre.Console.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a render context.
|
||||
/// </summary>
|
||||
public sealed class RenderContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the console's output encoding.
|
||||
/// </summary>
|
||||
public Encoding Encoding { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not this a legacy console (i.e. cmd.exe).
|
||||
/// </summary>
|
||||
public bool LegacyConsole { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not unicode is supported.
|
||||
/// </summary>
|
||||
public bool Unicode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current justification.
|
||||
/// </summary>
|
||||
public Justify? Justification { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RenderContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="encoding">The console's output encoding.</param>
|
||||
/// <param name="legacyConsole">A value indicating whether or not this a legacy console (i.e. cmd.exe).</param>
|
||||
/// <param name="justification">The justification to use when rendering.</param>
|
||||
public RenderContext(Encoding encoding, bool legacyConsole, Justify? justification = null)
|
||||
{
|
||||
Encoding = encoding ?? throw new System.ArgumentNullException(nameof(encoding));
|
||||
LegacyConsole = legacyConsole;
|
||||
Justification = justification;
|
||||
Unicode = Encoding == Encoding.UTF8 || Encoding == Encoding.Unicode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new context with the specified justification.
|
||||
/// </summary>
|
||||
/// <param name="justification">The justification.</param>
|
||||
/// <returns>A new <see cref="RenderContext"/> instance with the specified justification.</returns>
|
||||
public RenderContext WithJustification(Justify? justification)
|
||||
{
|
||||
return new RenderContext(Encoding, LegacyConsole, justification);
|
||||
}
|
||||
}
|
||||
}
|
41
src/Spectre.Console/Rendering/Renderable.cs
Normal file
41
src/Spectre.Console/Rendering/Renderable.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Spectre.Console.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for a renderable object implementing <see cref="IRenderable"/>.
|
||||
/// </summary>
|
||||
public abstract class Renderable : IRenderable
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
Measurement IRenderable.Measure(RenderContext context, int maxWidth)
|
||||
{
|
||||
return Measure(context, maxWidth);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerable<Segment> IRenderable.Render(RenderContext context, int maxWidth)
|
||||
{
|
||||
return Render(context, maxWidth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Measures the renderable object.
|
||||
/// </summary>
|
||||
/// <param name="context">The render context.</param>
|
||||
/// <param name="maxWidth">The maximum allowed width.</param>
|
||||
/// <returns>The minimum and maximum width of the object.</returns>
|
||||
protected virtual Measurement Measure(RenderContext context, int maxWidth)
|
||||
{
|
||||
return new Measurement(maxWidth, maxWidth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the object.
|
||||
/// </summary>
|
||||
/// <param name="context">The render context.</param>
|
||||
/// <param name="maxWidth">The maximum allowed width.</param>
|
||||
/// <returns>A collection of segments.</returns>
|
||||
protected abstract IEnumerable<Segment> Render(RenderContext context, int maxWidth);
|
||||
}
|
||||
}
|
245
src/Spectre.Console/Rendering/Segment.cs
Normal file
245
src/Spectre.Console/Rendering/Segment.cs
Normal file
@ -0,0 +1,245 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Spectre.Console.Internal;
|
||||
|
||||
namespace Spectre.Console.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a renderable segment.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Text,nq}")]
|
||||
public class Segment
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the segment text.
|
||||
/// </summary>
|
||||
public string Text { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not this is an expicit line break
|
||||
/// that should be preserved.
|
||||
/// </summary>
|
||||
public bool IsLineBreak { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not this is a whitespace
|
||||
/// that should be preserved but not taken into account when
|
||||
/// layouting text.
|
||||
/// </summary>
|
||||
public bool IsWhiteSpace { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the segment style.
|
||||
/// </summary>
|
||||
public Style Style { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a segment representing a line break.
|
||||
/// </summary>
|
||||
public static Segment LineBreak { get; } = new Segment(Environment.NewLine, Style.Plain, true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an empty segment.
|
||||
/// </summary>
|
||||
public static Segment Empty { get; } = new Segment(string.Empty, Style.Plain);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Segment"/> class.
|
||||
/// </summary>
|
||||
/// <param name="text">The segment text.</param>
|
||||
public Segment(string text)
|
||||
: this(text, Style.Plain)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Segment"/> class.
|
||||
/// </summary>
|
||||
/// <param name="text">The segment text.</param>
|
||||
/// <param name="style">The segment style.</param>
|
||||
public Segment(string text, Style style)
|
||||
: this(text, style, false)
|
||||
{
|
||||
}
|
||||
|
||||
private Segment(string text, Style style, bool lineBreak)
|
||||
{
|
||||
if (text is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
Text = text.NormalizeLineEndings();
|
||||
Style = style;
|
||||
IsLineBreak = lineBreak;
|
||||
IsWhiteSpace = string.IsNullOrWhiteSpace(text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of cells that this segment
|
||||
/// occupies in the console.
|
||||
/// </summary>
|
||||
/// <param name="encoding">The encoding to use.</param>
|
||||
/// <returns>The number of cells that this segment occupies in the console.</returns>
|
||||
public int CellLength(Encoding encoding)
|
||||
{
|
||||
return Text.CellLength(encoding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new segment without any trailing line endings.
|
||||
/// </summary>
|
||||
/// <returns>A new segment without any trailing line endings.</returns>
|
||||
public Segment StripLineEndings()
|
||||
{
|
||||
return new Segment(Text.TrimEnd('\n').TrimEnd('\r'), Style);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits the segment at the offset.
|
||||
/// </summary>
|
||||
/// <param name="offset">The offset where to split the segment.</param>
|
||||
/// <returns>One or two new segments representing the split.</returns>
|
||||
public (Segment First, Segment? Second) Split(int offset)
|
||||
{
|
||||
if (offset < 0)
|
||||
{
|
||||
return (this, null);
|
||||
}
|
||||
|
||||
if (offset >= Text.Length)
|
||||
{
|
||||
return (this, null);
|
||||
}
|
||||
|
||||
return (
|
||||
new Segment(Text.Substring(0, offset), Style),
|
||||
new Segment(Text.Substring(offset, Text.Length - offset), Style));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits the provided segments into lines.
|
||||
/// </summary>
|
||||
/// <param name="segments">The segments to split.</param>
|
||||
/// <returns>A collection of lines.</returns>
|
||||
public static List<SegmentLine> SplitLines(IEnumerable<Segment> segments)
|
||||
{
|
||||
return SplitLines(segments, int.MaxValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits the provided segments into lines with a maximum width.
|
||||
/// </summary>
|
||||
/// <param name="segments">The segments to split into lines.</param>
|
||||
/// <param name="maxWidth">The maximum width.</param>
|
||||
/// <returns>A list of lines.</returns>
|
||||
public static List<SegmentLine> SplitLines(IEnumerable<Segment> segments, int maxWidth)
|
||||
{
|
||||
if (segments is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(segments));
|
||||
}
|
||||
|
||||
var lines = new List<SegmentLine>();
|
||||
var line = new SegmentLine();
|
||||
|
||||
var stack = new Stack<Segment>(segments.Reverse());
|
||||
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
var segment = stack.Pop();
|
||||
|
||||
if (line.Width + segment.Text.Length > maxWidth)
|
||||
{
|
||||
var diff = -(maxWidth - (line.Width + segment.Text.Length));
|
||||
var offset = segment.Text.Length - diff;
|
||||
|
||||
var (first, second) = segment.Split(offset);
|
||||
|
||||
line.Add(first);
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
|
||||
if (second != null)
|
||||
{
|
||||
stack.Push(second);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (segment.Text.Contains("\n"))
|
||||
{
|
||||
if (segment.Text == "\n")
|
||||
{
|
||||
if (line.Width > 0 || segment.IsLineBreak)
|
||||
{
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var text = segment.Text;
|
||||
while (text != null)
|
||||
{
|
||||
var parts = text.SplitLines();
|
||||
if (parts.Length > 0)
|
||||
{
|
||||
if (parts[0].Length > 0)
|
||||
{
|
||||
line.Add(new Segment(parts[0], segment.Style));
|
||||
}
|
||||
}
|
||||
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
if (line.Width > 0)
|
||||
{
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
}
|
||||
|
||||
text = string.Concat(parts.Skip(1).Take(parts.Length - 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
text = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
line.Add(segment);
|
||||
}
|
||||
}
|
||||
|
||||
if (line.Count > 0)
|
||||
{
|
||||
lines.Add(line);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
38
src/Spectre.Console/Rendering/SegmentLine.cs
Normal file
38
src/Spectre.Console/Rendering/SegmentLine.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Spectre.Console.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a collection of segments.
|
||||
/// </summary>
|
||||
[SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix")]
|
||||
public sealed class SegmentLine : List<Segment>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the width of the line.
|
||||
/// </summary>
|
||||
public int Width => this.Sum(line => line.Text.Length);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cell width of the segment line.
|
||||
/// </summary>
|
||||
/// <param name="encoding">The encoding to use.</param>
|
||||
/// <returns>The cell width of the segment line.</returns>
|
||||
public int CellWidth(Encoding encoding)
|
||||
{
|
||||
return this.Sum(line => line.CellLength(encoding));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Preprends a segment to the line.
|
||||
/// </summary>
|
||||
/// <param name="segment">The segment to prepend.</param>
|
||||
public void Prepend(Segment segment)
|
||||
{
|
||||
Insert(0, segment);
|
||||
}
|
||||
}
|
||||
}
|
25
src/Spectre.Console/Rendering/SegmentLineEnumerator.cs
Normal file
25
src/Spectre.Console/Rendering/SegmentLineEnumerator.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Spectre.Console.Rendering
|
||||
{
|
||||
internal sealed class SegmentLineEnumerator : IEnumerable<Segment>
|
||||
{
|
||||
private readonly List<SegmentLine> _lines;
|
||||
|
||||
public SegmentLineEnumerator(List<SegmentLine> lines)
|
||||
{
|
||||
_lines = lines;
|
||||
}
|
||||
|
||||
public IEnumerator<Segment> GetEnumerator()
|
||||
{
|
||||
return new SegmentLineIterator(_lines);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
99
src/Spectre.Console/Rendering/SegmentLineIterator.cs
Normal file
99
src/Spectre.Console/Rendering/SegmentLineIterator.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Spectre.Console.Rendering
|
||||
{
|
||||
internal sealed class SegmentLineIterator : IEnumerator<Segment>
|
||||
{
|
||||
private readonly List<SegmentLine> _lines;
|
||||
private int _currentLine;
|
||||
private int _currentIndex;
|
||||
private bool _lineBreakEmitted;
|
||||
|
||||
public Segment Current { get; private set; }
|
||||
object? IEnumerator.Current => Current;
|
||||
|
||||
public SegmentLineIterator(List<SegmentLine> lines)
|
||||
{
|
||||
_currentLine = 0;
|
||||
_currentIndex = -1;
|
||||
_lines = lines;
|
||||
|
||||
Current = Segment.Empty;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_currentLine > _lines.Count - 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_currentIndex += 1;
|
||||
|
||||
// Did we go past the end of the line?
|
||||
if (_currentIndex > _lines[_currentLine].Count - 1)
|
||||
{
|
||||
// We haven't just emitted a line break?
|
||||
if (!_lineBreakEmitted)
|
||||
{
|
||||
// Got any more lines?
|
||||
if (_currentIndex + 1 > _lines[_currentLine].Count - 1)
|
||||
{
|
||||
// Only emit a line break if the next one isn't a line break.
|
||||
if ((_currentLine + 1 <= _lines.Count - 1)
|
||||
&& _lines[_currentLine + 1].Count > 0
|
||||
&& !_lines[_currentLine + 1][0].IsLineBreak)
|
||||
{
|
||||
_lineBreakEmitted = true;
|
||||
Current = Segment.LineBreak;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Increase the line and reset the index.
|
||||
_currentLine += 1;
|
||||
_currentIndex = 0;
|
||||
|
||||
_lineBreakEmitted = false;
|
||||
|
||||
// No more lines?
|
||||
if (_currentLine > _lines.Count - 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Nothing on the line?
|
||||
while (_currentIndex > _lines[_currentLine].Count - 1)
|
||||
{
|
||||
_currentLine += 1;
|
||||
_currentIndex = 0;
|
||||
|
||||
if (_currentLine > _lines.Count - 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the flag
|
||||
_lineBreakEmitted = false;
|
||||
|
||||
Current = _lines[_currentLine][_currentIndex];
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_currentLine = 0;
|
||||
_currentIndex = -1;
|
||||
|
||||
Current = Segment.Empty;
|
||||
}
|
||||
}
|
||||
}
|
125
src/Spectre.Console/Rendering/Table.Calculations.cs
Normal file
125
src/Spectre.Console/Rendering/Table.Calculations.cs
Normal file
@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Internal;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a table.
|
||||
/// </summary>
|
||||
public sealed partial class Table
|
||||
{
|
||||
private const int EdgeCount = 2;
|
||||
|
||||
// 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));
|
||||
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 separators = _columns.Count - 1;
|
||||
var padding = includePadding ? _columns.Select(x => x.Padding.GetHorizontalPadding()).Sum() : 0;
|
||||
return separators + EdgeCount + padding;
|
||||
}
|
||||
}
|
||||
}
|
356
src/Spectre.Console/Rendering/Table.cs
Normal file
356
src/Spectre.Console/Rendering/Table.cs
Normal file
@ -0,0 +1,356 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Internal;
|
||||
using Spectre.Console.Rendering;
|
||||
using SpectreBorder = Spectre.Console.Rendering.Border;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// A renderable table.
|
||||
/// </summary>
|
||||
public sealed partial class Table : Renderable
|
||||
{
|
||||
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;
|
||||
|
||||
/// <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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width of the table.
|
||||
/// </summary>
|
||||
public int? Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not to use
|
||||
/// a "safe" border on legacy consoles that might not be able
|
||||
/// to render non-ASCII characters. Defaults to <c>true</c>.
|
||||
/// </summary>
|
||||
public bool SafeBorder { get; set; } = true;
|
||||
|
||||
// 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>
|
||||
public void 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds multiple columns to the table.
|
||||
/// </summary>
|
||||
/// <param name="columns">The columns to add.</param>
|
||||
public void AddColumns(params TableColumn[] columns)
|
||||
{
|
||||
if (columns is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(columns));
|
||||
}
|
||||
|
||||
foreach (var column in columns)
|
||||
{
|
||||
AddColumn(column);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an empty row to the table.
|
||||
/// </summary>
|
||||
public void AddEmptyRow()
|
||||
{
|
||||
var columns = new IRenderable[ColumnCount];
|
||||
Enumerable.Range(0, ColumnCount).ForEach(index => columns[index] = Text.Empty);
|
||||
AddRow(columns);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a row to the table.
|
||||
/// </summary>
|
||||
/// <param name="columns">The row columns to add.</param>
|
||||
public void 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 less than the number of table 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());
|
||||
}
|
||||
|
||||
/// <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 = SpectreBorder.GetBorder(Border, (context.LegacyConsole || !context.Unicode) && SafeBorder);
|
||||
var tableWidth = maxWidth;
|
||||
|
||||
var showBorder = Border != BorderKind.None;
|
||||
var hideBorder = Border == BorderKind.None;
|
||||
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)));
|
||||
foreach (var (columnIndex, _, lastColumn, columnWidth) in columnWidths.Enumerate())
|
||||
{
|
||||
var padding = _columns[columnIndex].Padding;
|
||||
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, padding.Left))); // Left padding
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, columnWidth)));
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, padding.Right))); // Right padding
|
||||
|
||||
if (!lastColumn)
|
||||
{
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderTopSeparator)));
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderTopRight)));
|
||||
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
|
||||
result.Add(new Segment(border.GetPart(BorderPart.CellLeft)));
|
||||
}
|
||||
|
||||
// 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
|
||||
result.Add(new Segment(border.GetPart(BorderPart.CellRight)));
|
||||
}
|
||||
else if (showBorder)
|
||||
{
|
||||
// Add column separator
|
||||
result.Add(new Segment(border.GetPart(BorderPart.CellSeparator)));
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
|
||||
// Show header separator?
|
||||
if (firstRow && showBorder && ShowHeaders && hasRows)
|
||||
{
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomLeft)));
|
||||
foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
|
||||
{
|
||||
var padding = _columns[columnIndex].Padding;
|
||||
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom, padding.Left))); // Left padding
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom, columnWidth)));
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom, padding.Right))); // Right padding
|
||||
|
||||
if (!lastColumn)
|
||||
{
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomSeparator)));
|
||||
}
|
||||
}
|
||||
|
||||
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)));
|
||||
foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
|
||||
{
|
||||
var padding = _columns[columnIndex].Padding;
|
||||
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, padding.Left))); // Left padding
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, columnWidth)));
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, padding.Right))); // Right padding
|
||||
|
||||
if (!lastColumn)
|
||||
{
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomSeparator)));
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight)));
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool ShouldExpand()
|
||||
{
|
||||
return Expand || Width != null;
|
||||
}
|
||||
}
|
||||
}
|
60
src/Spectre.Console/Rendering/TableColumn.cs
Normal file
60
src/Spectre.Console/Rendering/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 : IAlignable
|
||||
{
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
78
src/Spectre.Console/Rendering/TableExtensions.cs
Normal file
78
src/Spectre.Console/Rendering/TableExtensions.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="Table"/>.
|
||||
/// </summary>
|
||||
public static class TableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a column to the table.
|
||||
/// </summary>
|
||||
/// <param name="table">The table to add the column to.</param>
|
||||
/// <param name="column">The column to add.</param>
|
||||
/// <returns>The added <see cref="TableColumn"/> instance.</returns>
|
||||
public static TableColumn AddColumn(this Table table, string column)
|
||||
{
|
||||
if (table is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(table));
|
||||
}
|
||||
|
||||
if (column is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(column));
|
||||
}
|
||||
|
||||
var tableColumn = new TableColumn(column);
|
||||
table.AddColumn(tableColumn);
|
||||
|
||||
return tableColumn;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds multiple columns to the table.
|
||||
/// </summary>
|
||||
/// <param name="table">The table to add the columns to.</param>
|
||||
/// <param name="columns">The columns to add.</param>
|
||||
public static void AddColumns(this Table table, params string[] columns)
|
||||
{
|
||||
if (table is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(table));
|
||||
}
|
||||
|
||||
if (columns is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(columns));
|
||||
}
|
||||
|
||||
foreach (var column in columns)
|
||||
{
|
||||
AddColumn(table, column);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a row to the table.
|
||||
/// </summary>
|
||||
/// <param name="table">The table to add the row to.</param>
|
||||
/// <param name="columns">The row columns to add.</param>
|
||||
public static void AddRow(this Table table, params string[] columns)
|
||||
{
|
||||
if (table is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(table));
|
||||
}
|
||||
|
||||
if (columns is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(columns));
|
||||
}
|
||||
|
||||
table.AddRow(columns.Select(column => new Markup(column)).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
272
src/Spectre.Console/Rendering/Text.cs
Normal file
272
src/Spectre.Console/Rendering/Text.cs
Normal file
@ -0,0 +1,272 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Internal;
|
||||
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
|
||||
{
|
||||
private readonly List<SegmentLine> _lines;
|
||||
|
||||
/// <summary>
|
||||
/// Gets an empty <see cref="Text"/> instance.
|
||||
/// </summary>
|
||||
public static Text Empty { get; } = new Text(string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text alignment.
|
||||
/// </summary>
|
||||
public Justify? Alignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Text"/> class.
|
||||
/// </summary>
|
||||
public Text()
|
||||
{
|
||||
_lines = new List<SegmentLine>();
|
||||
}
|
||||
|
||||
/// <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)
|
||||
: this()
|
||||
{
|
||||
if (text is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
Append(text, style);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Text"/> instance representing
|
||||
/// the specified markup text.
|
||||
/// </summary>
|
||||
/// <param name="text">The markup text.</param>
|
||||
/// <param name="style">The text style.</param>
|
||||
/// <returns>a <see cref="Text"/> instance representing the specified markup text.</returns>
|
||||
public static Text Markup(string text, Style? style = null)
|
||||
{
|
||||
var result = MarkupParser.Parse(text, style ?? Style.Plain);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <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, max);
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a piece of text.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to append.</param>
|
||||
/// <param name="style">The style of the appended text.</param>
|
||||
public void 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
while (iterator.MoveNext())
|
||||
{
|
||||
var current = iterator.Current;
|
||||
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 (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;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user