Add table heading and footnote support

Closes #116
This commit is contained in:
Patrik Svensson
2020-10-19 00:41:40 +02:00
committed by Patrik Svensson
parent 5c119ee0c3
commit cb2924a609
15 changed files with 299 additions and 259 deletions

View File

@ -20,17 +20,18 @@ namespace Spectre.Console.Tests.Unit
console.Render(calendar);
// Then
console.Lines.Count.ShouldBe(10);
console.Lines[0].ShouldBe("┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐");
console.Lines[1].ShouldBe("│ Sun │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │");
console.Lines[2].ShouldBe("├─────┼─────┼─────┼─────┼─────┼─────┼─────┤");
console.Lines[3].ShouldBe("│ │ │ │ │ 1 │ 2 │ 3* │");
console.Lines[4].ShouldBe("│ 45678910 │");
console.Lines[5].ShouldBe("│ 1112*13141516 │ 17 │");
console.Lines[6].ShouldBe("│ 18 │ 19 2021 │ 222324 │");
console.Lines[7].ShouldBe("│ 2526 │ 27 │ 28 │ 29 │ 3031 │");
console.Lines[8].ShouldBe("│ │");
console.Lines[9].ShouldBe("└─────┴─────┴─────┴─────┴─────┴─────┴─────┘");
console.Lines.Count.ShouldBe(11);
console.Lines[00].ShouldBe(" 2020 October ");
console.Lines[01].ShouldBe("┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐");
console.Lines[02].ShouldBe("│ Sun │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │");
console.Lines[03].ShouldBe("├─────┼─────┼─────┼─────┼─────┼─────┼─────┤");
console.Lines[04].ShouldBe("│ 123* │");
console.Lines[05].ShouldBe("│ 4 5 6 7 8 9 │ 10 │");
console.Lines[06].ShouldBe("│ 11 │ 12*13 │ 14151617 │");
console.Lines[07].ShouldBe("│ 1819 │ 20 │ 21 │ 2223 │ 24 │");
console.Lines[08].ShouldBe("│ 25262728293031 │");
console.Lines[09].ShouldBe("│ │ │ │ │ │ │ │");
console.Lines[10].ShouldBe("└─────┴─────┴─────┴─────┴─────┴─────┴─────┘");
}
[Fact]
@ -48,45 +49,18 @@ namespace Spectre.Console.Tests.Unit
console.Render(calendar);
// Then
console.Lines.Count.ShouldBe(10);
console.Lines[0].ShouldBe("┌─────┬────┬────┬────┬────┬────┬────┐");
console.Lines[1].ShouldBe("│ Mo │ Di │ Mi │ Do │ Fr │ Sa │ So │");
console.Lines[2].ShouldBe("├─────┼────┼────┼────┼────┼────┼────┤");
console.Lines[3].ShouldBe("│ │ │ │ 1 │ 2 │ 3* │ 4 │");
console.Lines[4].ShouldBe("│ 567891011 │");
console.Lines[5].ShouldBe("│ 12*13141516 │ 17 │ 18 │");
console.Lines[6].ShouldBe("│ 19 2021 │ 22232425 │");
console.Lines[7].ShouldBe("│ 26 │ 27 │ 28 │ 29 │ 3031 │");
console.Lines[8].ShouldBe("│ │ │");
console.Lines[9].ShouldBe("└─────┴────┴────┴────┴────┴────┴────┘");
}
[Fact]
public void Should_Render_List_Of_Events_If_Enabled()
{
// Given
var console = new PlainConsole(width: 80);
var calendar = new Calendar(2020, 10, 15)
.SetCulture("de-DE")
.AddCalendarEvent(new DateTime(2020, 9, 1))
.AddCalendarEvent(new DateTime(2020, 10, 3))
.AddCalendarEvent(new DateTime(2020, 10, 12));
// When
console.Render(calendar);
// Then
console.Lines.Count.ShouldBe(10);
console.Lines[0].ShouldBe("┌─────┬────┬────┬────┬────┬────┬────┐");
console.Lines[1].ShouldBe("│ Mo │ Di │ Mi │ Do │ Fr │ Sa │ So │");
console.Lines[2].ShouldBe("├─────┼────┼────┼────┼────┼────┼────┤");
console.Lines[3].ShouldBe("│ │ │ │ 1 │ 2 │ 3* │ 4 │");
console.Lines[4].ShouldBe("│ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │");
console.Lines[5].ShouldBe("│ 12* │ 13 │ 14 │ 15 │ 16 │ 17 │ 18 │");
console.Lines[6].ShouldBe("│ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │ 25 │");
console.Lines[7].ShouldBe("│ 26 │ 27 │ 28 │ 29 │ 30 │ 31 │ │");
console.Lines[8].ShouldBe("│ │ │ │ │ │ │ │");
console.Lines[9].ShouldBe("└─────┴────┴────┴────┴────┴────┴────┘");
console.Lines.Count.ShouldBe(11);
console.Lines[00].ShouldBe(" Oktober 2020 ");
console.Lines[01].ShouldBe("┌─────┬────┬────┬────┬────┬────┬────┐");
console.Lines[02].ShouldBe("│ Mo │ Di │ Mi │ Do │ Fr │ Sa │ So │");
console.Lines[03].ShouldBe("├─────┼────┼────┼────┼────┼────┼────┤");
console.Lines[04].ShouldBe("│ 123*4 │");
console.Lines[05].ShouldBe("│ 5 6 7 8 9 │ 10 │ 11 │");
console.Lines[06].ShouldBe("│ 12*13 │ 1415161718 │");
console.Lines[07].ShouldBe("│ 19 │ 20 │ 21 │ 2223 │ 2425 │");
console.Lines[08].ShouldBe("│ 262728293031 │ │");
console.Lines[09].ShouldBe("│ │ │ │ │ │ │ │");
console.Lines[10].ShouldBe("└─────┴────┴────┴────┴────┴────┴────┘");
}
}
}

View File

@ -73,7 +73,7 @@ namespace Spectre.Console.Tests.Unit
// When
console.Render(new Panel("Hello World")
{
Header = new PanelHeader("Greeting"),
Header = new Title("Greeting"),
Expand = true,
Padding = new Padding(2, 0, 2, 0),
});
@ -94,7 +94,7 @@ namespace Spectre.Console.Tests.Unit
// When
console.Render(new Panel("Hello World")
{
Header = new PanelHeader("Greeting").LeftAligned(),
Header = new Title("Greeting").LeftAligned(),
Expand = true,
});
@ -114,7 +114,7 @@ namespace Spectre.Console.Tests.Unit
// When
console.Render(new Panel("Hello World")
{
Header = new PanelHeader("Greeting").Centered(),
Header = new Title("Greeting").Centered(),
Expand = true,
});
@ -134,7 +134,7 @@ namespace Spectre.Console.Tests.Unit
// When
console.Render(new Panel("Hello World")
{
Header = new PanelHeader("Greeting").RightAligned(),
Header = new Title("Greeting").RightAligned(),
Expand = true,
});
@ -154,7 +154,7 @@ namespace Spectre.Console.Tests.Unit
// When
console.Render(new Panel("Hello World")
{
Header = new PanelHeader("Greeting"),
Header = new Title("Greeting"),
Expand = true,
});

View File

@ -250,52 +250,6 @@ namespace Spectre.Console.Tests.Unit
console.Lines[5].ShouldBe("└───────────────────────────┴───────────────────────────┴──────────────────────┘");
}
[Fact]
public void Should_Render_Table_With_Ascii_Border_Correctly()
{
// Given
var console = new PlainConsole(width: 80);
var table = new Table { Border = TableBorder.Ascii };
table.AddColumns("Foo", "Bar", "Baz");
table.AddRow("Qux", "Corgi", "Waldo");
table.AddRow("Grault", "Garply", "Fred");
// When
console.Render(table);
// Then
console.Lines.Count.ShouldBe(6);
console.Lines[0].ShouldBe("+-------------------------+");
console.Lines[1].ShouldBe("| Foo | Bar | Baz |");
console.Lines[2].ShouldBe("|--------+--------+-------|");
console.Lines[3].ShouldBe("| Qux | Corgi | Waldo |");
console.Lines[4].ShouldBe("| Grault | Garply | Fred |");
console.Lines[5].ShouldBe("+-------------------------+");
}
[Fact]
public void Should_Render_Table_With_Rounded_Border_Correctly()
{
// Given
var console = new PlainConsole(width: 80);
var table = new Table { Border = TableBorder.Rounded };
table.AddColumns("Foo", "Bar", "Baz");
table.AddRow("Qux", "Corgi", "Waldo");
table.AddRow("Grault", "Garply", "Fred");
// When
console.Render(table);
// Then
console.Lines.Count.ShouldBe(6);
console.Lines[0].ShouldBe("╭────────┬────────┬───────╮");
console.Lines[1].ShouldBe("│ Foo │ Bar │ Baz │");
console.Lines[2].ShouldBe("├────────┼────────┼───────┤");
console.Lines[3].ShouldBe("│ Qux │ Corgi │ Waldo │");
console.Lines[4].ShouldBe("│ Grault │ Garply │ Fred │");
console.Lines[5].ShouldBe("╰────────┴────────┴───────╯");
}
[Fact]
public void Should_Render_Table_With_No_Border_Correctly()
{
@ -432,5 +386,32 @@ namespace Spectre.Console.Tests.Unit
console.Lines[10].ShouldBe("│ │ en │ │");
console.Lines[11].ShouldBe("╰───────┴───────┴───────╯");
}
[Fact]
public void Should_Render_Table_With_Title_And_Footnote_Correctly()
{
// Given
var console = new PlainConsole(width: 80);
var table = new Table { Border = TableBorder.Rounded };
table.Heading = new Title("Hello World");
table.Footnote = new Title("Goodbye World");
table.AddColumns("Foo", "Bar", "Baz");
table.AddRow("Qux", "Corgi", "Waldo");
table.AddRow("Grault", "Garply", "Fred");
// When
console.Render(table);
// Then
console.Lines.Count.ShouldBe(8);
console.Lines[0].ShouldBe(" Hello World ");
console.Lines[1].ShouldBe("╭────────┬────────┬───────╮");
console.Lines[2].ShouldBe("│ Foo │ Bar │ Baz │");
console.Lines[3].ShouldBe("├────────┼────────┼───────┤");
console.Lines[4].ShouldBe("│ Qux │ Corgi │ Waldo │");
console.Lines[5].ShouldBe("│ Grault │ Garply │ Fred │");
console.Lines[6].ShouldBe("╰────────┴────────┴───────╯");
console.Lines[7].ShouldBe(" Goodbye World ");
}
}
}

View File

@ -79,5 +79,22 @@ namespace Spectre.Console
calendar.HightlightStyle = style ?? Style.Plain;
return calendar;
}
/// <summary>
/// Sets the calendar's header <see cref="Style"/>.
/// </summary>
/// <param name="calendar">The calendar.</param>
/// <param name="style">The header style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Calendar SetHeaderStyle(this Calendar calendar, Style? style)
{
if (calendar is null)
{
throw new ArgumentNullException(nameof(calendar));
}
calendar.HeaderStyle = style ?? Style.Plain;
return calendar;
}
}
}

View File

@ -27,7 +27,7 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(text));
}
return SetHeader(panel, new PanelHeader(text, style, alignment));
return SetHeader(panel, new Title(text, style, alignment));
}
/// <summary>
@ -36,7 +36,7 @@ namespace Spectre.Console
/// <param name="panel">The panel.</param>
/// <param name="header">The header to use.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Panel SetHeader(this Panel panel, PanelHeader header)
public static Panel SetHeader(this Panel panel, Title header)
{
if (panel is null)
{

View File

@ -176,5 +176,85 @@ namespace Spectre.Console
table.ShowHeaders = false;
return table;
}
/// <summary>
/// Sets the table heading.
/// </summary>
/// <param name="table">The table.</param>
/// <param name="text">The heading.</param>
/// <param name="style">The style.</param>
/// <param name="alignment">The alignment.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Table SetHeading(this Table table, string text, Style? style = null, Justify? alignment = null)
{
if (table is null)
{
throw new ArgumentNullException(nameof(table));
}
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
return SetHeading(table, new Title(text, style, alignment));
}
/// <summary>
/// Sets the table heading.
/// </summary>
/// <param name="table">The table.</param>
/// <param name="heading">The heading.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Table SetHeading(this Table table, Title heading)
{
if (table is null)
{
throw new ArgumentNullException(nameof(table));
}
table.Heading = heading;
return table;
}
/// <summary>
/// Sets the table footnote.
/// </summary>
/// <param name="table">The table.</param>
/// <param name="text">The footnote.</param>
/// <param name="style">The style.</param>
/// <param name="alignment">The alignment.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Table SetFootnote(this Table table, string text, Style? style = null, Justify? alignment = null)
{
if (table is null)
{
throw new ArgumentNullException(nameof(table));
}
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
return SetFootnote(table, new Title(text, style, alignment));
}
/// <summary>
/// Sets the table footnote.
/// </summary>
/// <param name="table">The table.</param>
/// <param name="footnote">The footnote.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Table SetFootnote(this Table table, Title footnote)
{
if (table is null)
{
throw new ArgumentNullException(nameof(table));
}
table.Footnote = footnote;
return table;
}
}
}

View File

@ -8,14 +8,9 @@ namespace Spectre.Console.Internal
public static string GetAbbreviatedDayName(this DayOfWeek day, CultureInfo culture)
{
culture ??= CultureInfo.InvariantCulture;
var name = culture.DateTimeFormat.GetAbbreviatedDayName(day);
if (name.Length > 0 && char.IsLower(name[0]))
{
name = string.Format(culture, "{0}{1}", char.ToUpper(name[0], culture), name.Substring(1));
}
return name;
return culture.DateTimeFormat
.GetAbbreviatedDayName(day)
.Capitalize(culture);
}
public static DayOfWeek GetNextWeekDay(this DayOfWeek day)

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Spectre.Console.Rendering;
@ -18,6 +19,23 @@ namespace Spectre.Console.Internal
return Cell.GetCellLength(context, text);
}
public static string Capitalize(this string text, CultureInfo? culture = null)
{
if (text == null)
{
return string.Empty;
}
culture ??= CultureInfo.InvariantCulture;
if (text.Length > 0 && char.IsLower(text[0]))
{
text = string.Format(culture, "{0}{1}", char.ToUpper(text[0], culture), text.Substring(1));
}
return text;
}
public static string NormalizeLineEndings(this string text, bool native = false)
{
text ??= string.Empty;

View File

@ -28,6 +28,8 @@ namespace Spectre.Console
private bool _dirty;
private CultureInfo _culture;
private Style _highlightStyle;
private bool _showHeader;
private Style? _headerStyle;
/// <summary>
/// Gets or sets the calendar year.
@ -95,6 +97,24 @@ namespace Spectre.Console
set => MarkAsDirty(() => _highlightStyle = value);
}
/// <summary>
/// Gets or sets a value indicating whether or not the calendar header should be shown.
/// </summary>
public bool ShowHeader
{
get => _showHeader;
set => MarkAsDirty(() => _showHeader = value);
}
/// <summary>
/// Gets or sets the header style.
/// </summary>
public Style? HeaderStyle
{
get => _headerStyle;
set => MarkAsDirty(() => _headerStyle = value);
}
/// <summary>
/// Gets a list containing all calendar events.
/// </summary>
@ -137,6 +157,7 @@ namespace Spectre.Console
_dirty = true;
_culture = CultureInfo.InvariantCulture;
_highlightStyle = new Style(foreground: Color.Blue);
_showHeader = true;
_calendarEvents = new ListWithCallback<CalendarEvent>(() => _dirty = true);
}
@ -176,6 +197,12 @@ namespace Spectre.Console
BorderStyle = _borderStyle,
};
if (ShowHeader)
{
var heading = new DateTime(Year, Month, Day).ToString("Y", culture).SafeMarkup();
table.Heading = new Title(heading, HeaderStyle);
}
// Add columns
foreach (var order in GetWeekdays())
{

View File

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

View File

@ -16,6 +16,9 @@ namespace Spectre.Console
private readonly List<TableColumn> _columns;
private readonly List<List<IRenderable>> _rows;
private static Style _defaultHeadingStyle = new Style(Color.Silver);
private static Style _defaultFootnoteStyle = new Style(Color.Grey);
/// <summary>
/// Gets the number of columns in the table.
/// </summary>
@ -52,6 +55,16 @@ namespace Spectre.Console
/// </summary>
public int? Width { get; set; }
/// <summary>
/// Gets or sets the table title.
/// </summary>
public Title? Heading { get; set; }
/// <summary>
/// Gets or sets the table footnote.
/// </summary>
public Title? Footnote { get; set; }
// Whether this is a grid or not.
internal bool IsGrid { get; set; }
@ -186,8 +199,10 @@ namespace Spectre.Console
// Add rows.
rows.AddRange(_rows);
// Iterate all rows
var result = new List<Segment>();
result.AddRange(RenderAnnotation(context, Heading, tableWidth, _defaultHeadingStyle));
// Iterate all rows
foreach (var (index, firstRow, lastRow, row) in rows.Enumerate())
{
var cellHeight = 1;
@ -303,6 +318,8 @@ namespace Spectre.Console
}
}
result.AddRange(RenderAnnotation(context, Footnote, tableWidth, _defaultFootnoteStyle));
return result;
}
@ -375,6 +392,27 @@ namespace Spectre.Console
return widths;
}
private static IEnumerable<Segment> RenderAnnotation(
RenderContext context, Title? header,
int maxWidth, Style defaultStyle)
{
if (header == null)
{
yield break;
}
var paragraph = new Markup(header.Text.Capitalize(), header.Style ?? defaultStyle)
.SetAlignment(header.Alignment ?? Justify.Center)
.SetOverflow(Overflow.Ellipsis);
foreach (var segment in ((IRenderable)paragraph).Render(context, maxWidth))
{
yield return segment;
}
yield return Segment.LineBreak;
}
private (int Min, int Max) MeasureColumn(TableColumn column, RenderContext options, int maxWidth)
{
var padding = column.Padding.GetWidth();

View File

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