Add support for aligning tables

This commit is contained in:
Patrik Svensson
2020-10-21 17:59:55 +02:00
committed by Patrik Svensson
parent b52056ee49
commit 9afc1ea721
17 changed files with 542 additions and 132 deletions

View File

@ -11,7 +11,7 @@ namespace Spectre.Console
/// <summary>
/// A renderable calendar.
/// </summary>
public sealed class Calendar : Renderable, IHasCulture, IHasTableBorder
public sealed class Calendar : Renderable, IHasCulture, IHasTableBorder, IAlignable
{
private const int NumberOfWeekDays = 7;
private const int ExpectedRowCount = 6;
@ -30,6 +30,7 @@ namespace Spectre.Console
private Style _highlightStyle;
private bool _showHeader;
private Style? _headerStyle;
private Justify? _alignment;
/// <summary>
/// Gets or sets the calendar year.
@ -115,6 +116,13 @@ namespace Spectre.Console
set => MarkAsDirty(() => _headerStyle = value);
}
/// <inheritdoc/>
public Justify? Alignment
{
get => _alignment;
set => MarkAsDirty(() => _alignment = value);
}
/// <summary>
/// Gets a list containing all calendar events.
/// </summary>
@ -195,12 +203,13 @@ namespace Spectre.Console
Border = _border,
UseSafeBorder = _useSafeBorder,
BorderStyle = _borderStyle,
Alignment = _alignment,
};
if (ShowHeader)
{
var heading = new DateTime(Year, Month, Day).ToString("Y", culture).EscapeMarkup();
table.Heading = new Title(heading, HeaderStyle);
table.Heading = new TableTitle(heading, HeaderStyle);
}
// Add columns

View File

@ -7,7 +7,7 @@ namespace Spectre.Console
/// <summary>
/// A renderable grid.
/// </summary>
public sealed class Grid : Renderable, IExpandable
public sealed class Grid : Renderable, IExpandable, IAlignable
{
private readonly Table _table;
@ -28,6 +28,13 @@ namespace Spectre.Console
set => _table.Expand = value;
}
/// <inheritdoc/>
public Justify? Alignment
{
get => _table.Alignment;
set => _table.Alignment = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="Grid"/> class.
/// </summary>

View File

@ -39,7 +39,7 @@ namespace Spectre.Console
/// <summary>
/// Gets or sets the header.
/// </summary>
public Title? Header { get; set; }
public PanelHeader? Header { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Panel"/> class.

View File

@ -0,0 +1,76 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Represents a panel header.
/// </summary>
public sealed class PanelHeader : IAlignable
{
/// <summary>
/// Gets the panel header text.
/// </summary>
public string Text { get; }
/// <summary>
/// Gets or sets the panel header style.
/// </summary>
public Style? Style { get; set; }
/// <summary>
/// Gets or sets the panel header alignment.
/// </summary>
public Justify? Alignment { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="PanelHeader"/> class.
/// </summary>
/// <param name="text">The panel header text.</param>
/// <param name="style">The panel header style.</param>
/// <param name="alignment">The panel 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 panel header style.
/// </summary>
/// <param name="style">The panel 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 panel header style.
/// </summary>
/// <param name="style">The panel header style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public PanelHeader SetStyle(string style)
{
if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
Style = Style.Parse(style);
return this;
}
/// <summary>
/// Sets the panel header alignment.
/// </summary>
/// <param name="alignment">The panel header alignment.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public PanelHeader SetAlignment(Justify alignment)
{
Alignment = alignment;
return this;
}
}
}

View File

@ -144,37 +144,18 @@ namespace Spectre.Console
// Justify lines
var justification = context.Justification ?? Alignment ?? Justify.Left;
foreach (var (_, _, last, line) in lines.Enumerate())
if (justification != Justify.Left)
{
var length = line.Sum(l => l.StripLineEndings().CellLength(context));
if (length < maxWidth)
foreach (var line in lines)
{
// 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)));
}
}
Aligner.Align(context, line, justification, maxWidth);
}
}
if (context.SingleLine)
{
return lines.First().Where(segment => !segment.IsLineBreak);
// Return the first line
return lines[0].Where(segment => !segment.IsLineBreak);
}
return new SegmentLineEnumerator(lines);

View File

@ -9,7 +9,7 @@ namespace Spectre.Console
/// <summary>
/// A renderable table.
/// </summary>
public sealed class Table : Renderable, IHasTableBorder, IExpandable
public sealed class Table : Renderable, IHasTableBorder, IExpandable, IAlignable
{
private const int EdgeCount = 2;
@ -58,12 +58,15 @@ namespace Spectre.Console
/// <summary>
/// Gets or sets the table title.
/// </summary>
public Title? Heading { get; set; }
public TableTitle? Heading { get; set; }
/// <summary>
/// Gets or sets the table footnote.
/// </summary>
public Title? Footnote { get; set; }
public TableTitle? Footnote { get; set; }
/// <inheritdoc/>
public Justify? Alignment { get; set; }
// Whether this is a grid or not.
internal bool IsGrid { get; set; }
@ -200,7 +203,7 @@ namespace Spectre.Console
rows.AddRange(_rows);
var result = new List<Segment>();
result.AddRange(RenderAnnotation(context, Heading, tableWidth, _defaultHeadingStyle));
result.AddRange(RenderAnnotation(context, Heading, actualMaxWidth, tableWidth, _defaultHeadingStyle));
// Iterate all rows
foreach (var (index, firstRow, lastRow, row) in rows.Enumerate())
@ -222,7 +225,7 @@ namespace Spectre.Console
// Show top of header?
if (firstRow && showBorder)
{
var separator = border.GetColumnRow(TablePart.Top, columnWidths, _columns);
var separator = Aligner.Align(context, border.GetColumnRow(TablePart.Top, columnWidths, _columns), Alignment, actualMaxWidth);
result.Add(new Segment(separator, borderStyle));
result.Add(Segment.LineBreak);
}
@ -288,6 +291,9 @@ namespace Spectre.Console
}
}
// Align the row result.
Aligner.Align(context, rowResult, Alignment, actualMaxWidth);
// Is the row larger than the allowed max width?
if (Segment.CellLength(context, rowResult) > actualMaxWidth)
{
@ -304,7 +310,7 @@ namespace Spectre.Console
// Show header separator?
if (firstRow && showBorder && ShowHeaders && hasRows)
{
var separator = border.GetColumnRow(TablePart.Separator, columnWidths, _columns);
var separator = Aligner.Align(context, border.GetColumnRow(TablePart.Separator, columnWidths, _columns), Alignment, actualMaxWidth);
result.Add(new Segment(separator, borderStyle));
result.Add(Segment.LineBreak);
}
@ -312,13 +318,13 @@ namespace Spectre.Console
// Show bottom of footer?
if (lastRow && showBorder)
{
var separator = border.GetColumnRow(TablePart.Bottom, columnWidths, _columns);
var separator = Aligner.Align(context, border.GetColumnRow(TablePart.Bottom, columnWidths, _columns), Alignment, actualMaxWidth);
result.Add(new Segment(separator, borderStyle));
result.Add(Segment.LineBreak);
}
}
result.AddRange(RenderAnnotation(context, Footnote, tableWidth, _defaultFootnoteStyle));
result.AddRange(RenderAnnotation(context, Footnote, actualMaxWidth, tableWidth, _defaultFootnoteStyle));
return result;
}
@ -392,25 +398,27 @@ namespace Spectre.Console
return widths;
}
private static IEnumerable<Segment> RenderAnnotation(
RenderContext context, Title? header,
int maxWidth, Style defaultStyle)
private IEnumerable<Segment> RenderAnnotation(
RenderContext context, TableTitle? header,
int maxWidth, int tableWidth, Style defaultStyle)
{
if (header == null)
{
yield break;
return Array.Empty<Segment>();
}
var paragraph = new Markup(header.Text.Capitalize(), header.Style ?? defaultStyle)
.SetAlignment(header.Alignment ?? Justify.Center)
.SetAlignment(Justify.Center)
.SetOverflow(Overflow.Ellipsis);
foreach (var segment in ((IRenderable)paragraph).Render(context, maxWidth))
{
yield return segment;
}
var items = new List<Segment>();
items.AddRange(((IRenderable)paragraph).Render(context, tableWidth));
yield return Segment.LineBreak;
// Align over the whole buffer area
Aligner.Align(context, items, Alignment, maxWidth);
items.Add(Segment.LineBreak);
return items;
}
private (int Min, int Max) MeasureColumn(TableColumn column, RenderContext options, int maxWidth)

View File

@ -3,9 +3,9 @@ using System;
namespace Spectre.Console
{
/// <summary>
/// Represents a title.
/// Represents a table title such as a heading or footnote.
/// </summary>
public sealed class Title : IAlignable
public sealed class TableTitle
{
/// <summary>
/// Gets the title text.
@ -18,21 +18,14 @@ namespace Spectre.Console
public Style? Style { get; set; }
/// <summary>
/// Gets or sets the title alignment.
/// </summary>
public Justify? Alignment { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Title"/> class.
/// Initializes a new instance of the <see cref="TableTitle"/> class.
/// </summary>
/// <param name="text">The title text.</param>
/// <param name="style">The title style.</param>
/// <param name="alignment">The title alignment.</param>
public Title(string text, Style? style = null, Justify? alignment = null)
public TableTitle(string text, Style? style = null)
{
Text = text ?? throw new ArgumentNullException(nameof(text));
Style = style;
Alignment = alignment;
}
/// <summary>
@ -40,20 +33,25 @@ namespace Spectre.Console
/// </summary>
/// <param name="style">The title style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Title SetStyle(Style? style)
public TableTitle SetStyle(Style? style)
{
Style = style ?? Style.Plain;
return this;
}
/// <summary>
/// Sets the title alignment.
/// Sets the title style.
/// </summary>
/// <param name="alignment">The title alignment.</param>
/// <param name="style">The title style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Title SetAlignment(Justify alignment)
public TableTitle SetStyle(string style)
{
Alignment = alignment;
if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
Style = Style.Parse(style);
return this;
}
}