Use file scoped namespace declarations

This commit is contained in:
Patrik Svensson
2021-12-21 11:06:46 +01:00
committed by Phil Scott
parent 1dbaf50935
commit ec1188b837
607 changed files with 28739 additions and 29245 deletions

View File

@ -4,271 +4,270 @@ using System.Globalization;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// A renderable calendar.
/// </summary>
public sealed class Calendar : JustInTimeRenderable, IHasCulture, IHasTableBorder, IAlignable
{
private const int NumberOfWeekDays = 7;
private const int ExpectedRowCount = 6;
private readonly ListWithCallback<CalendarEvent> _calendarEvents;
private int _year;
private int _month;
private int _day;
private TableBorder _border;
private bool _useSafeBorder;
private Style? _borderStyle;
private CultureInfo? _culture;
private Style _highlightStyle;
private bool _showHeader;
private Style? _headerStyle;
private Justify? _alignment;
/// <summary>
/// A renderable calendar.
/// Gets or sets the calendar year.
/// </summary>
public sealed class Calendar : JustInTimeRenderable, IHasCulture, IHasTableBorder, IAlignable
public int Year
{
private const int NumberOfWeekDays = 7;
private const int ExpectedRowCount = 6;
get => _year;
set => MarkAsDirty(() => _year = value);
}
private readonly ListWithCallback<CalendarEvent> _calendarEvents;
/// <summary>
/// Gets or sets the calendar month.
/// </summary>
public int Month
{
get => _month;
set => MarkAsDirty(() => _month = value);
}
private int _year;
private int _month;
private int _day;
private TableBorder _border;
private bool _useSafeBorder;
private Style? _borderStyle;
private CultureInfo? _culture;
private Style _highlightStyle;
private bool _showHeader;
private Style? _headerStyle;
private Justify? _alignment;
/// <summary>
/// Gets or sets the calendar day.
/// </summary>
public int Day
{
get => _day;
set => MarkAsDirty(() => _day = value);
}
/// <summary>
/// Gets or sets the calendar year.
/// </summary>
public int Year
/// <inheritdoc/>
public TableBorder Border
{
get => _border;
set => MarkAsDirty(() => _border = value);
}
/// <inheritdoc/>
public bool UseSafeBorder
{
get => _useSafeBorder;
set => MarkAsDirty(() => _useSafeBorder = value);
}
/// <inheritdoc/>
public Style? BorderStyle
{
get => _borderStyle;
set => MarkAsDirty(() => _borderStyle = value);
}
/// <summary>
/// Gets or sets the calendar's <see cref="CultureInfo"/>.
/// </summary>
public CultureInfo? Culture
{
get => _culture;
set => MarkAsDirty(() => _culture = value);
}
/// <summary>
/// Gets or sets the calendar's highlight <see cref="Style"/>.
/// </summary>
public Style HightlightStyle
{
get => _highlightStyle;
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);
}
/// <inheritdoc/>
public Justify? Alignment
{
get => _alignment;
set => MarkAsDirty(() => _alignment = value);
}
/// <summary>
/// Gets a list containing all calendar events.
/// </summary>
public IList<CalendarEvent> CalendarEvents => _calendarEvents;
/// <summary>
/// Initializes a new instance of the <see cref="Calendar"/> class.
/// </summary>
/// <param name="date">The calendar date.</param>
public Calendar(DateTime date)
: this(date.Year, date.Month, date.Day)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Calendar"/> class.
/// </summary>
/// <param name="year">The calendar year.</param>
/// <param name="month">The calendar month.</param>
public Calendar(int year, int month)
: this(year, month, 1)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Calendar"/> class.
/// </summary>
/// <param name="year">The calendar year.</param>
/// <param name="month">The calendar month.</param>
/// <param name="day">The calendar day.</param>
public Calendar(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
_border = TableBorder.Square;
_useSafeBorder = true;
_borderStyle = null;
_culture = CultureInfo.InvariantCulture;
_highlightStyle = new Style(foreground: Color.Blue);
_showHeader = true;
_calendarEvents = new ListWithCallback<CalendarEvent>(() => MarkAsDirty());
}
/// <inheritdoc/>
protected override IRenderable Build()
{
var culture = Culture ?? CultureInfo.InvariantCulture;
var table = new Table
{
get => _year;
set => MarkAsDirty(() => _year = value);
Border = _border,
UseSafeBorder = _useSafeBorder,
BorderStyle = _borderStyle,
Alignment = _alignment,
};
if (ShowHeader)
{
var heading = new DateTime(Year, Month, Day).ToString("Y", culture).EscapeMarkup();
table.Title = new TableTitle(heading, HeaderStyle);
}
/// <summary>
/// Gets or sets the calendar month.
/// </summary>
public int Month
// Add columns
foreach (var order in GetWeekdays())
{
get => _month;
set => MarkAsDirty(() => _month = value);
table.AddColumn(new TableColumn(order.GetAbbreviatedDayName(culture)));
}
/// <summary>
/// Gets or sets the calendar day.
/// </summary>
public int Day
var row = new List<IRenderable>();
var currentDay = 1;
var weekday = culture.DateTimeFormat.FirstDayOfWeek;
var weekdays = BuildWeekDayTable();
var daysInMonth = DateTime.DaysInMonth(Year, Month);
while (currentDay <= daysInMonth)
{
get => _day;
set => MarkAsDirty(() => _day = value);
}
/// <inheritdoc/>
public TableBorder Border
{
get => _border;
set => MarkAsDirty(() => _border = value);
}
/// <inheritdoc/>
public bool UseSafeBorder
{
get => _useSafeBorder;
set => MarkAsDirty(() => _useSafeBorder = value);
}
/// <inheritdoc/>
public Style? BorderStyle
{
get => _borderStyle;
set => MarkAsDirty(() => _borderStyle = value);
}
/// <summary>
/// Gets or sets the calendar's <see cref="CultureInfo"/>.
/// </summary>
public CultureInfo? Culture
{
get => _culture;
set => MarkAsDirty(() => _culture = value);
}
/// <summary>
/// Gets or sets the calendar's highlight <see cref="Style"/>.
/// </summary>
public Style HightlightStyle
{
get => _highlightStyle;
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);
}
/// <inheritdoc/>
public Justify? Alignment
{
get => _alignment;
set => MarkAsDirty(() => _alignment = value);
}
/// <summary>
/// Gets a list containing all calendar events.
/// </summary>
public IList<CalendarEvent> CalendarEvents => _calendarEvents;
/// <summary>
/// Initializes a new instance of the <see cref="Calendar"/> class.
/// </summary>
/// <param name="date">The calendar date.</param>
public Calendar(DateTime date)
: this(date.Year, date.Month, date.Day)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Calendar"/> class.
/// </summary>
/// <param name="year">The calendar year.</param>
/// <param name="month">The calendar month.</param>
public Calendar(int year, int month)
: this(year, month, 1)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Calendar"/> class.
/// </summary>
/// <param name="year">The calendar year.</param>
/// <param name="month">The calendar month.</param>
/// <param name="day">The calendar day.</param>
public Calendar(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
_border = TableBorder.Square;
_useSafeBorder = true;
_borderStyle = null;
_culture = CultureInfo.InvariantCulture;
_highlightStyle = new Style(foreground: Color.Blue);
_showHeader = true;
_calendarEvents = new ListWithCallback<CalendarEvent>(() => MarkAsDirty());
}
/// <inheritdoc/>
protected override IRenderable Build()
{
var culture = Culture ?? CultureInfo.InvariantCulture;
var table = new Table
if (weekdays[currentDay - 1] == weekday)
{
Border = _border,
UseSafeBorder = _useSafeBorder,
BorderStyle = _borderStyle,
Alignment = _alignment,
};
if (ShowHeader)
{
var heading = new DateTime(Year, Month, Day).ToString("Y", culture).EscapeMarkup();
table.Title = new TableTitle(heading, HeaderStyle);
}
// Add columns
foreach (var order in GetWeekdays())
{
table.AddColumn(new TableColumn(order.GetAbbreviatedDayName(culture)));
}
var row = new List<IRenderable>();
var currentDay = 1;
var weekday = culture.DateTimeFormat.FirstDayOfWeek;
var weekdays = BuildWeekDayTable();
var daysInMonth = DateTime.DaysInMonth(Year, Month);
while (currentDay <= daysInMonth)
{
if (weekdays[currentDay - 1] == weekday)
if (_calendarEvents.Any(e => e.Month == Month && e.Day == currentDay))
{
if (_calendarEvents.Any(e => e.Month == Month && e.Day == currentDay))
{
row.Add(new Markup(currentDay.ToString(CultureInfo.InvariantCulture) + "*", _highlightStyle));
}
else
{
row.Add(new Text(currentDay.ToString(CultureInfo.InvariantCulture)));
}
currentDay++;
row.Add(new Markup(currentDay.ToString(CultureInfo.InvariantCulture) + "*", _highlightStyle));
}
else
{
// Add empty cell
row.Add(Text.Empty);
row.Add(new Text(currentDay.ToString(CultureInfo.InvariantCulture)));
}
if (row.Count == NumberOfWeekDays)
{
// Flush row
table.AddRow(row.ToArray());
row.Clear();
}
weekday = weekday.GetNextWeekDay();
currentDay++;
}
else
{
// Add empty cell
row.Add(Text.Empty);
}
if (row.Count > 0)
if (row.Count == NumberOfWeekDays)
{
// Flush row
table.AddRow(row.ToArray());
row.Clear();
}
// We want all calendars to have the same height.
if (table.Rows.Count < ExpectedRowCount)
{
var diff = Math.Max(0, ExpectedRowCount - table.Rows.Count);
for (var i = 0; i < diff; i++)
{
table.AddEmptyRow();
}
}
return table;
weekday = weekday.GetNextWeekDay();
}
private DayOfWeek[] GetWeekdays()
if (row.Count > 0)
{
var culture = Culture ?? CultureInfo.InvariantCulture;
var days = new DayOfWeek[7];
days[0] = culture.DateTimeFormat.FirstDayOfWeek;
for (var i = 1; i < 7; i++)
{
days[i] = days[i - 1].GetNextWeekDay();
}
return days;
// Flush row
table.AddRow(row.ToArray());
row.Clear();
}
private DayOfWeek[] BuildWeekDayTable()
// We want all calendars to have the same height.
if (table.Rows.Count < ExpectedRowCount)
{
var result = new List<DayOfWeek>();
for (var day = 0; day < DateTime.DaysInMonth(Year, Month); day++)
var diff = Math.Max(0, ExpectedRowCount - table.Rows.Count);
for (var i = 0; i < diff; i++)
{
result.Add(new DateTime(Year, Month, day + 1).DayOfWeek);
table.AddEmptyRow();
}
return result.ToArray();
}
return table;
}
}
private DayOfWeek[] GetWeekdays()
{
var culture = Culture ?? CultureInfo.InvariantCulture;
var days = new DayOfWeek[7];
days[0] = culture.DateTimeFormat.FirstDayOfWeek;
for (var i = 1; i < 7; i++)
{
days[i] = days[i - 1].GetNextWeekDay();
}
return days;
}
private DayOfWeek[] BuildWeekDayTable()
{
var result = new List<DayOfWeek>();
for (var day = 0; day < DateTime.DaysInMonth(Year, Month); day++)
{
result.Add(new DateTime(Year, Month, day + 1).DayOfWeek);
}
return result.ToArray();
}
}

View File

@ -1,54 +1,53 @@
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents a calendar event.
/// </summary>
public sealed class CalendarEvent
{
/// <summary>
/// Represents a calendar event.
/// Gets the description of the calendar event.
/// </summary>
public sealed class CalendarEvent
public string Description { get; }
/// <summary>
/// Gets the year of the calendar event.
/// </summary>
public int Year { get; }
/// <summary>
/// Gets the month of the calendar event.
/// </summary>
public int Month { get; }
/// <summary>
/// Gets the day of the calendar event.
/// </summary>
public int Day { get; }
/// <summary>
/// Initializes a new instance of the <see cref="CalendarEvent"/> class.
/// </summary>
/// <param name="year">The year of the calendar event.</param>
/// <param name="month">The month of the calendar event.</param>
/// <param name="day">The day of the calendar event.</param>
public CalendarEvent(int year, int month, int day)
: this(string.Empty, year, month, day)
{
/// <summary>
/// Gets the description of the calendar event.
/// </summary>
public string Description { get; }
/// <summary>
/// Gets the year of the calendar event.
/// </summary>
public int Year { get; }
/// <summary>
/// Gets the month of the calendar event.
/// </summary>
public int Month { get; }
/// <summary>
/// Gets the day of the calendar event.
/// </summary>
public int Day { get; }
/// <summary>
/// Initializes a new instance of the <see cref="CalendarEvent"/> class.
/// </summary>
/// <param name="year">The year of the calendar event.</param>
/// <param name="month">The month of the calendar event.</param>
/// <param name="day">The day of the calendar event.</param>
public CalendarEvent(int year, int month, int day)
: this(string.Empty, year, month, day)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CalendarEvent"/> class.
/// </summary>
/// <param name="description">The calendar event description.</param>
/// <param name="year">The year of the calendar event.</param>
/// <param name="month">The month of the calendar event.</param>
/// <param name="day">The day of the calendar event.</param>
public CalendarEvent(string description, int year, int month, int day)
{
Description = description ?? string.Empty;
Year = year;
Month = month;
Day = day;
}
}
}
/// <summary>
/// Initializes a new instance of the <see cref="CalendarEvent"/> class.
/// </summary>
/// <param name="description">The calendar event description.</param>
/// <param name="year">The year of the calendar event.</param>
/// <param name="month">The month of the calendar event.</param>
/// <param name="day">The day of the calendar event.</param>
public CalendarEvent(string description, int year, int month, int day)
{
Description = description ?? string.Empty;
Year = year;
Month = month;
Day = day;
}
}

View File

@ -2,168 +2,167 @@ using System;
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents a renderable canvas.
/// </summary>
public sealed class Canvas : Renderable
{
private readonly Color?[,] _pixels;
/// <summary>
/// Represents a renderable canvas.
/// Gets the width of the canvas.
/// </summary>
public sealed class Canvas : Renderable
public int Width { get; }
/// <summary>
/// Gets the height of the canvas.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets or sets the render width of the canvas.
/// </summary>
public int? MaxWidth { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not
/// to scale the canvas when rendering.
/// </summary>
public bool Scale { get; set; } = true;
/// <summary>
/// Gets or sets the pixel width.
/// </summary>
public int PixelWidth { get; set; } = 2;
/// <summary>
/// Initializes a new instance of the <see cref="Canvas"/> class.
/// </summary>
/// <param name="width">The canvas width.</param>
/// <param name="height">The canvas height.</param>
public Canvas(int width, int height)
{
private readonly Color?[,] _pixels;
/// <summary>
/// Gets the width of the canvas.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the height of the canvas.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets or sets the render width of the canvas.
/// </summary>
public int? MaxWidth { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not
/// to scale the canvas when rendering.
/// </summary>
public bool Scale { get; set; } = true;
/// <summary>
/// Gets or sets the pixel width.
/// </summary>
public int PixelWidth { get; set; } = 2;
/// <summary>
/// Initializes a new instance of the <see cref="Canvas"/> class.
/// </summary>
/// <param name="width">The canvas width.</param>
/// <param name="height">The canvas height.</param>
public Canvas(int width, int height)
if (width < 1)
{
if (width < 1)
{
throw new ArgumentException("Must be > 1", nameof(width));
}
if (height < 1)
{
throw new ArgumentException("Must be > 1", nameof(height));
}
Width = width;
Height = height;
_pixels = new Color?[Width, Height];
throw new ArgumentException("Must be > 1", nameof(width));
}
/// <summary>
/// Sets a pixel with the specified color in the canvas at the specified location.
/// </summary>
/// <param name="x">The X coordinate for the pixel.</param>
/// <param name="y">The Y coordinate for the pixel.</param>
/// <param name="color">The pixel color.</param>
/// <returns>The same <see cref="Canvas"/> instance so that multiple calls can be chained.</returns>
public Canvas SetPixel(int x, int y, Color color)
if (height < 1)
{
_pixels[x, y] = color;
return this;
throw new ArgumentException("Must be > 1", nameof(height));
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
Width = width;
Height = height;
_pixels = new Color?[Width, Height];
}
/// <summary>
/// Sets a pixel with the specified color in the canvas at the specified location.
/// </summary>
/// <param name="x">The X coordinate for the pixel.</param>
/// <param name="y">The Y coordinate for the pixel.</param>
/// <param name="color">The pixel color.</param>
/// <returns>The same <see cref="Canvas"/> instance so that multiple calls can be chained.</returns>
public Canvas SetPixel(int x, int y, Color color)
{
_pixels[x, y] = color;
return this;
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
if (PixelWidth < 0)
{
if (PixelWidth < 0)
{
throw new InvalidOperationException("Pixel width must be greater than zero.");
}
var width = MaxWidth ?? Width;
if (maxWidth < width * PixelWidth)
{
return new Measurement(maxWidth, maxWidth);
}
return new Measurement(width * PixelWidth, width * PixelWidth);
throw new InvalidOperationException("Pixel width must be greater than zero.");
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
var width = MaxWidth ?? Width;
if (maxWidth < width * PixelWidth)
{
if (PixelWidth < 0)
return new Measurement(maxWidth, maxWidth);
}
return new Measurement(width * PixelWidth, width * PixelWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
if (PixelWidth < 0)
{
throw new InvalidOperationException("Pixel width must be greater than zero.");
}
var pixels = _pixels;
var pixel = new string(' ', PixelWidth);
var width = Width;
var height = Height;
// Got a max width?
if (MaxWidth != null)
{
height = (int)(height * ((float)MaxWidth.Value) / Width);
width = MaxWidth.Value;
}
// Exceed the max width when we take pixel width into account?
if (width * PixelWidth > maxWidth)
{
height = (int)(height * (maxWidth / (float)(width * PixelWidth)));
width = maxWidth / PixelWidth;
// If it's not possible to scale the canvas sufficiently, it's too small to render.
if (height == 0)
{
throw new InvalidOperationException("Pixel width must be greater than zero.");
yield break;
}
}
var pixels = _pixels;
var pixel = new string(' ', PixelWidth);
var width = Width;
var height = Height;
// Need to rescale the pixel buffer?
if (Scale && (width != Width || height != Height))
{
pixels = ScaleDown(width, height);
}
// Got a max width?
if (MaxWidth != null)
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
height = (int)(height * ((float)MaxWidth.Value) / Width);
width = MaxWidth.Value;
}
// Exceed the max width when we take pixel width into account?
if (width * PixelWidth > maxWidth)
{
height = (int)(height * (maxWidth / (float)(width * PixelWidth)));
width = maxWidth / PixelWidth;
// If it's not possible to scale the canvas sufficiently, it's too small to render.
if (height == 0)
var color = pixels[x, y];
if (color != null)
{
yield break;
yield return new Segment(pixel, new Style(background: color));
}
else
{
yield return new Segment(pixel);
}
}
// Need to rescale the pixel buffer?
if (Scale && (width != Width || height != Height))
{
pixels = ScaleDown(width, height);
}
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
var color = pixels[x, y];
if (color != null)
{
yield return new Segment(pixel, new Style(background: color));
}
else
{
yield return new Segment(pixel);
}
}
yield return Segment.LineBreak;
}
}
private Color?[,] ScaleDown(int newWidth, int newHeight)
{
var buffer = new Color?[newWidth, newHeight];
var xRatio = ((Width << 16) / newWidth) + 1;
var yRatio = ((Height << 16) / newHeight) + 1;
for (var i = 0; i < newHeight; i++)
{
for (var j = 0; j < newWidth; j++)
{
buffer[j, i] = _pixels[(j * xRatio) >> 16, (i * yRatio) >> 16];
}
}
return buffer;
yield return Segment.LineBreak;
}
}
}
private Color?[,] ScaleDown(int newWidth, int newHeight)
{
var buffer = new Color?[newWidth, newHeight];
var xRatio = ((Width << 16) / newWidth) + 1;
var yRatio = ((Height << 16) / newHeight) + 1;
for (var i = 0; i < newHeight; i++)
{
for (var j = 0; j < newWidth; j++)
{
buffer[j, i] = _pixels[(j * xRatio) >> 16, (i * yRatio) >> 16];
}
}
return buffer;
}
}

View File

@ -4,102 +4,101 @@ using System.Globalization;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// A renderable (horizontal) bar chart.
/// </summary>
public sealed class BarChart : Renderable, IHasCulture
{
/// <summary>
/// A renderable (horizontal) bar chart.
/// Gets the bar chart data.
/// </summary>
public sealed class BarChart : Renderable, IHasCulture
public List<IBarChartItem> Data { get; }
/// <summary>
/// Gets or sets the width of the bar chart.
/// </summary>
public int? Width { get; set; }
/// <summary>
/// Gets or sets the bar chart label.
/// </summary>
public string? Label { get; set; }
/// <summary>
/// Gets or sets the bar chart label alignment.
/// </summary>
public Justify? LabelAlignment { get; set; } = Justify.Center;
/// <summary>
/// Gets or sets a value indicating whether or not
/// values should be shown next to each bar.
/// </summary>
public bool ShowValues { get; set; } = true;
/// <summary>
/// Gets or sets the culture that's used to format values.
/// </summary>
/// <remarks>Defaults to invariant culture.</remarks>
public CultureInfo? Culture { get; set; }
/// <summary>
/// Gets or sets the fixed max value for a bar chart.
/// </summary>
/// <remarks>Defaults to null, which corresponds to largest value in chart.</remarks>
public double? MaxValue { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="BarChart"/> class.
/// </summary>
public BarChart()
{
/// <summary>
/// Gets the bar chart data.
/// </summary>
public List<IBarChartItem> Data { get; }
Data = new List<IBarChartItem>();
}
/// <summary>
/// Gets or sets the width of the bar chart.
/// </summary>
public int? Width { get; set; }
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
return new Measurement(width, width);
}
/// <summary>
/// Gets or sets the bar chart label.
/// </summary>
public string? Label { get; set; }
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
var maxValue = Math.Max(MaxValue ?? 0d, Data.Max(item => item.Value));
/// <summary>
/// Gets or sets the bar chart label alignment.
/// </summary>
public Justify? LabelAlignment { get; set; } = Justify.Center;
var grid = new Grid();
grid.Collapse();
grid.AddColumn(new GridColumn().PadRight(2).RightAligned());
grid.AddColumn(new GridColumn().PadLeft(0));
grid.Width = width;
/// <summary>
/// Gets or sets a value indicating whether or not
/// values should be shown next to each bar.
/// </summary>
public bool ShowValues { get; set; } = true;
/// <summary>
/// Gets or sets the culture that's used to format values.
/// </summary>
/// <remarks>Defaults to invariant culture.</remarks>
public CultureInfo? Culture { get; set; }
/// <summary>
/// Gets or sets the fixed max value for a bar chart.
/// </summary>
/// <remarks>Defaults to null, which corresponds to largest value in chart.</remarks>
public double? MaxValue { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="BarChart"/> class.
/// </summary>
public BarChart()
if (!string.IsNullOrWhiteSpace(Label))
{
Data = new List<IBarChartItem>();
grid.AddRow(Text.Empty, new Markup(Label).Alignment(LabelAlignment));
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
foreach (var item in Data)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
return new Measurement(width, width);
grid.AddRow(
new Markup(item.Label),
new ProgressBar()
{
Value = item.Value,
MaxValue = maxValue,
ShowRemaining = false,
CompletedStyle = new Style().Foreground(item.Color ?? Color.Default),
FinishedStyle = new Style().Foreground(item.Color ?? Color.Default),
UnicodeBar = '█',
AsciiBar = '█',
ShowValue = ShowValues,
Culture = Culture,
});
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
var maxValue = Math.Max(MaxValue ?? 0d, Data.Max(item => item.Value));
var grid = new Grid();
grid.Collapse();
grid.AddColumn(new GridColumn().PadRight(2).RightAligned());
grid.AddColumn(new GridColumn().PadLeft(0));
grid.Width = width;
if (!string.IsNullOrWhiteSpace(Label))
{
grid.AddRow(Text.Empty, new Markup(Label).Alignment(LabelAlignment));
}
foreach (var item in Data)
{
grid.AddRow(
new Markup(item.Label),
new ProgressBar()
{
Value = item.Value,
MaxValue = maxValue,
ShowRemaining = false,
CompletedStyle = new Style().Foreground(item.Color ?? Color.Default),
FinishedStyle = new Style().Foreground(item.Color ?? Color.Default),
UnicodeBar = '█',
AsciiBar = '█',
ShowValue = ShowValues,
Culture = Culture,
});
}
return ((IRenderable)grid).Render(context, width);
}
return ((IRenderable)grid).Render(context, width);
}
}

View File

@ -1,38 +1,37 @@
using System;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// An item that's shown in a bar chart.
/// </summary>
public sealed class BarChartItem : IBarChartItem
{
/// <summary>
/// An item that's shown in a bar chart.
/// Gets the item label.
/// </summary>
public sealed class BarChartItem : IBarChartItem
public string Label { get; }
/// <summary>
/// Gets the item value.
/// </summary>
public double Value { get; }
/// <summary>
/// Gets the item color.
/// </summary>
public Color? Color { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BarChartItem"/> class.
/// </summary>
/// <param name="label">The item label.</param>
/// <param name="value">The item value.</param>
/// <param name="color">The item color.</param>
public BarChartItem(string label, double value, Color? color = null)
{
/// <summary>
/// Gets the item label.
/// </summary>
public string Label { get; }
/// <summary>
/// Gets the item value.
/// </summary>
public double Value { get; }
/// <summary>
/// Gets the item color.
/// </summary>
public Color? Color { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BarChartItem"/> class.
/// </summary>
/// <param name="label">The item label.</param>
/// <param name="value">The item value.</param>
/// <param name="color">The item color.</param>
public BarChartItem(string label, double value, Color? color = null)
{
Label = label ?? throw new ArgumentNullException(nameof(label));
Value = value;
Color = color;
}
Label = label ?? throw new ArgumentNullException(nameof(label));
Value = value;
Color = color;
}
}
}

View File

@ -3,40 +3,39 @@ using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class BreakdownBar : Renderable
{
internal sealed class BreakdownBar : Renderable
private readonly List<IBreakdownChartItem> _data;
public int? Width { get; set; }
public BreakdownBar(List<IBreakdownChartItem> data)
{
private readonly List<IBreakdownChartItem> _data;
public int? Width { get; set; }
public BreakdownBar(List<IBreakdownChartItem> data)
{
_data = data ?? throw new ArgumentNullException(nameof(data));
}
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
return new Measurement(width, width);
}
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
// Chart
var maxValue = _data.Sum(i => i.Value);
var items = _data.ToArray();
var bars = Ratio.Distribute(width, items.Select(i => Math.Max(0, (int)(width * (i.Value / maxValue)))).ToArray());
for (var index = 0; index < items.Length; index++)
{
yield return new Segment(new string('█', bars[index]), new Style(items[index].Color));
}
yield return Segment.LineBreak;
}
_data = data ?? throw new ArgumentNullException(nameof(data));
}
}
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
return new Measurement(width, width);
}
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
// Chart
var maxValue = _data.Sum(i => i.Value);
var items = _data.ToArray();
var bars = Ratio.Distribute(width, items.Select(i => Math.Max(0, (int)(width * (i.Value / maxValue)))).ToArray());
for (var index = 0; index < items.Length; index++)
{
yield return new Segment(new string('█', bars[index]), new Style(items[index].Color));
}
yield return Segment.LineBreak;
}
}

View File

@ -3,99 +3,98 @@ using System.Collections.Generic;
using System.Globalization;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// A renderable breakdown chart.
/// </summary>
public sealed class BreakdownChart : Renderable, IHasCulture
{
/// <summary>
/// A renderable breakdown chart.
/// Gets the breakdown chart data.
/// </summary>
public sealed class BreakdownChart : Renderable, IHasCulture
public List<IBreakdownChartItem> Data { get; }
/// <summary>
/// Gets or sets the width of the breakdown chart.
/// </summary>
public int? Width { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not to show tags.
/// </summary>
public bool ShowTags { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not to show tag values.
/// </summary>
public bool ShowTagValues { get; set; } = true;
/// <summary>
/// Gets or sets the tag value formatter.
/// </summary>
public Func<double, CultureInfo, string>? ValueFormatter { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not the
/// chart and tags should be rendered in compact mode.
/// </summary>
public bool Compact { get; set; } = true;
/// <summary>
/// Gets or sets the <see cref="CultureInfo"/> to use
/// when rendering values.
/// </summary>
/// <remarks>Defaults to invariant culture.</remarks>
public CultureInfo? Culture { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="BreakdownChart"/> class.
/// </summary>
public BreakdownChart()
{
/// <summary>
/// Gets the breakdown chart data.
/// </summary>
public List<IBreakdownChartItem> Data { get; }
Data = new List<IBreakdownChartItem>();
Culture = CultureInfo.InvariantCulture;
}
/// <summary>
/// Gets or sets the width of the breakdown chart.
/// </summary>
public int? Width { get; set; }
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
return new Measurement(width, width);
}
/// <summary>
/// Gets or sets a value indicating whether or not to show tags.
/// </summary>
public bool ShowTags { get; set; } = true;
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
/// <summary>
/// Gets or sets a value indicating whether or not to show tag values.
/// </summary>
public bool ShowTagValues { get; set; } = true;
var grid = new Grid().Width(width);
grid.AddColumn(new GridColumn().NoWrap());
/// <summary>
/// Gets or sets the tag value formatter.
/// </summary>
public Func<double, CultureInfo, string>? ValueFormatter { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not the
/// chart and tags should be rendered in compact mode.
/// </summary>
public bool Compact { get; set; } = true;
/// <summary>
/// Gets or sets the <see cref="CultureInfo"/> to use
/// when rendering values.
/// </summary>
/// <remarks>Defaults to invariant culture.</remarks>
public CultureInfo? Culture { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="BreakdownChart"/> class.
/// </summary>
public BreakdownChart()
// Bar
grid.AddRow(new BreakdownBar(Data)
{
Data = new List<IBreakdownChartItem>();
Culture = CultureInfo.InvariantCulture;
}
Width = width,
});
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
if (ShowTags)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
return new Measurement(width, width);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
var grid = new Grid().Width(width);
grid.AddColumn(new GridColumn().NoWrap());
// Bar
grid.AddRow(new BreakdownBar(Data)
if (!Compact)
{
Width = width,
});
if (ShowTags)
{
if (!Compact)
{
grid.AddEmptyRow();
}
// Tags
grid.AddRow(new BreakdownTags(Data)
{
Width = width,
Culture = Culture,
ShowTagValues = ShowTagValues,
ValueFormatter = ValueFormatter,
});
grid.AddEmptyRow();
}
return ((IRenderable)grid).Render(context, width);
// Tags
grid.AddRow(new BreakdownTags(Data)
{
Width = width,
Culture = Culture,
ShowTagValues = ShowTagValues,
ValueFormatter = ValueFormatter,
});
}
return ((IRenderable)grid).Render(context, width);
}
}
}

View File

@ -1,38 +1,37 @@
using System;
using System;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// An item that's shown in a breakdown chart.
/// </summary>
public sealed class BreakdownChartItem : IBreakdownChartItem
{
/// <summary>
/// An item that's shown in a breakdown chart.
/// Gets the item label.
/// </summary>
public sealed class BreakdownChartItem : IBreakdownChartItem
public string Label { get; }
/// <summary>
/// Gets the item value.
/// </summary>
public double Value { get; }
/// <summary>
/// Gets the item color.
/// </summary>
public Color Color { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BreakdownChartItem"/> class.
/// </summary>
/// <param name="label">The item label.</param>
/// <param name="value">The item value.</param>
/// <param name="color">The item color.</param>
public BreakdownChartItem(string label, double value, Color color)
{
/// <summary>
/// Gets the item label.
/// </summary>
public string Label { get; }
/// <summary>
/// Gets the item value.
/// </summary>
public double Value { get; }
/// <summary>
/// Gets the item color.
/// </summary>
public Color Color { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BreakdownChartItem"/> class.
/// </summary>
/// <param name="label">The item label.</param>
/// <param name="value">The item value.</param>
/// <param name="color">The item color.</param>
public BreakdownChartItem(string label, double value, Color color)
{
Label = label ?? throw new ArgumentNullException(nameof(label));
Value = value;
Color = color;
}
Label = label ?? throw new ArgumentNullException(nameof(label));
Value = value;
Color = color;
}
}
}

View File

@ -3,74 +3,73 @@ using System.Collections.Generic;
using System.Globalization;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class BreakdownTags : Renderable
{
internal sealed class BreakdownTags : Renderable
private readonly List<IBreakdownChartItem> _data;
public int? Width { get; set; }
public CultureInfo? Culture { get; set; }
public bool ShowTagValues { get; set; } = true;
public Func<double, CultureInfo, string>? ValueFormatter { get; set; }
public BreakdownTags(List<IBreakdownChartItem> data)
{
private readonly List<IBreakdownChartItem> _data;
_data = data ?? throw new ArgumentNullException(nameof(data));
}
public int? Width { get; set; }
public CultureInfo? Culture { get; set; }
public bool ShowTagValues { get; set; } = true;
public Func<double, CultureInfo, string>? ValueFormatter { get; set; }
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
return new Measurement(width, width);
}
public BreakdownTags(List<IBreakdownChartItem> data)
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var culture = Culture ?? CultureInfo.InvariantCulture;
var panels = new List<Panel>();
foreach (var item in _data)
{
_data = data ?? throw new ArgumentNullException(nameof(data));
var panel = new Panel(GetTag(item, culture));
panel.Inline = true;
panel.Padding = new Padding(0, 0);
panel.NoBorder();
panels.Add(panel);
}
protected override Measurement Measure(RenderContext context, int maxWidth)
foreach (var segment in ((IRenderable)new Columns(panels).Padding(0, 0)).Render(context, maxWidth))
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
return new Measurement(width, width);
}
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var culture = Culture ?? CultureInfo.InvariantCulture;
var panels = new List<Panel>();
foreach (var item in _data)
{
var panel = new Panel(GetTag(item, culture));
panel.Inline = true;
panel.Padding = new Padding(0, 0);
panel.NoBorder();
panels.Add(panel);
}
foreach (var segment in ((IRenderable)new Columns(panels).Padding(0, 0)).Render(context, maxWidth))
{
yield return segment;
}
}
private string GetTag(IBreakdownChartItem item, CultureInfo culture)
{
return string.Format(
culture, "[{0}]■[/] {1}",
item.Color.ToMarkup() ?? "default",
FormatValue(item, culture)).Trim();
}
private string FormatValue(IBreakdownChartItem item, CultureInfo culture)
{
var formatter = ValueFormatter ?? DefaultFormatter;
if (ShowTagValues)
{
return string.Format(culture, "{0} [grey]{1}[/]",
item.Label.EscapeMarkup(),
formatter(item.Value, culture));
}
return item.Label.EscapeMarkup();
}
private static string DefaultFormatter(double value, CultureInfo culture)
{
return value.ToString(culture);
yield return segment;
}
}
}
private string GetTag(IBreakdownChartItem item, CultureInfo culture)
{
return string.Format(
culture, "[{0}]■[/] {1}",
item.Color.ToMarkup() ?? "default",
FormatValue(item, culture)).Trim();
}
private string FormatValue(IBreakdownChartItem item, CultureInfo culture)
{
var formatter = ValueFormatter ?? DefaultFormatter;
if (ShowTagValues)
{
return string.Format(culture, "{0} [grey]{1}[/]",
item.Label.EscapeMarkup(),
formatter(item.Value, culture));
}
return item.Label.EscapeMarkup();
}
private static string DefaultFormatter(double value, CultureInfo culture)
{
return value.ToString(culture);
}
}

View File

@ -1,23 +1,22 @@
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents a bar chart item.
/// </summary>
public interface IBarChartItem
{
/// <summary>
/// Represents a bar chart item.
/// Gets the item label.
/// </summary>
public interface IBarChartItem
{
/// <summary>
/// Gets the item label.
/// </summary>
string Label { get; }
string Label { get; }
/// <summary>
/// Gets the item value.
/// </summary>
double Value { get; }
/// <summary>
/// Gets the item value.
/// </summary>
double Value { get; }
/// <summary>
/// Gets the item color.
/// </summary>
Color? Color { get; }
}
}
/// <summary>
/// Gets the item color.
/// </summary>
Color? Color { get; }
}

View File

@ -1,23 +1,22 @@
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents a breakdown chart item.
/// </summary>
public interface IBreakdownChartItem
{
/// <summary>
/// Represents a breakdown chart item.
/// Gets the item label.
/// </summary>
public interface IBreakdownChartItem
{
/// <summary>
/// Gets the item label.
/// </summary>
string Label { get; }
string Label { get; }
/// <summary>
/// Gets the item value.
/// </summary>
double Value { get; }
/// <summary>
/// Gets the item value.
/// </summary>
double Value { get; }
/// <summary>
/// Gets the item color.
/// </summary>
Color Color { get; }
}
}
/// <summary>
/// Gets the item color.
/// </summary>
Color Color { get; }
}

View File

@ -1,15 +1,14 @@
using System;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Indicates that the tree being rendered includes a cycle, and cannot be rendered.
/// </summary>
public sealed class CircularTreeException : Exception
{
/// <summary>
/// Indicates that the tree being rendered includes a cycle, and cannot be rendered.
/// </summary>
public sealed class CircularTreeException : Exception
internal CircularTreeException(string message)
: base(message)
{
internal CircularTreeException(string message)
: base(message)
{
}
}
}

View File

@ -3,181 +3,180 @@ using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Renders things in columns.
/// </summary>
public sealed class Columns : Renderable, IPaddable, IExpandable
{
private readonly List<IRenderable> _items;
/// <inheritdoc/>
public Padding? Padding { get; set; } = new Padding(0, 0, 1, 0);
/// <summary>
/// Renders things in columns.
/// Gets or sets a value indicating whether or not the columns should
/// expand to the available space. If <c>false</c>, the column
/// width will be auto calculated. Defaults to <c>true</c>.
/// </summary>
public sealed class Columns : Renderable, IPaddable, IExpandable
public bool Expand { get; set; } = true;
/// <summary>
/// Initializes a new instance of the <see cref="Columns"/> class.
/// </summary>
/// <param name="items">The items to render as columns.</param>
public Columns(params IRenderable[] items)
: this((IEnumerable<IRenderable>)items)
{
private readonly List<IRenderable> _items;
}
/// <inheritdoc/>
public Padding? Padding { get; set; } = new Padding(0, 0, 1, 0);
/// <summary>
/// Gets or sets a value indicating whether or not the columns should
/// expand to the available space. If <c>false</c>, the column
/// width will be auto calculated. Defaults to <c>true</c>.
/// </summary>
public bool Expand { get; set; } = true;
/// <summary>
/// Initializes a new instance of the <see cref="Columns"/> class.
/// </summary>
/// <param name="items">The items to render as columns.</param>
public Columns(params IRenderable[] items)
: this((IEnumerable<IRenderable>)items)
/// <summary>
/// Initializes a new instance of the <see cref="Columns"/> class.
/// </summary>
/// <param name="items">The items to render as columns.</param>
public Columns(IEnumerable<IRenderable> items)
{
if (items is null)
{
throw new ArgumentNullException(nameof(items));
}
/// <summary>
/// Initializes a new instance of the <see cref="Columns"/> class.
/// </summary>
/// <param name="items">The items to render as columns.</param>
public Columns(IEnumerable<IRenderable> items)
{
if (items is null)
{
throw new ArgumentNullException(nameof(items));
}
_items = new List<IRenderable>(items);
}
_items = new List<IRenderable>(items);
/// <summary>
/// Initializes a new instance of the <see cref="Columns"/> class.
/// </summary>
/// <param name="items">The items to render.</param>
public Columns(IEnumerable<string> items)
{
if (items is null)
{
throw new ArgumentNullException(nameof(items));
}
/// <summary>
/// Initializes a new instance of the <see cref="Columns"/> class.
/// </summary>
/// <param name="items">The items to render.</param>
public Columns(IEnumerable<string> items)
{
if (items is null)
{
throw new ArgumentNullException(nameof(items));
}
_items = new List<IRenderable>(items.Select(item => new Markup(item)));
}
_items = new List<IRenderable>(items.Select(item => new Markup(item)));
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var maxPadding = Math.Max(Padding.GetLeftSafe(), Padding.GetRightSafe());
var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray();
var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding);
if (columnCount == 0)
{
// Temporary work around for extremely small consoles
return new Measurement(maxWidth, maxWidth);
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
var rows = _items.Count / Math.Max(columnCount, 1);
var greatestWidth = 0;
for (var row = 0; row < rows; row += Math.Max(1, columnCount))
{
var maxPadding = Math.Max(Padding.GetLeftSafe(), Padding.GetRightSafe());
var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray();
var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding);
if (columnCount == 0)
var widths = itemWidths.Skip(row * columnCount).Take(columnCount).ToList();
var totalWidth = widths.Sum() + (maxPadding * (widths.Count - 1));
if (totalWidth > greatestWidth)
{
// Temporary work around for extremely small consoles
return new Measurement(maxWidth, maxWidth);
greatestWidth = totalWidth;
}
}
var rows = _items.Count / Math.Max(columnCount, 1);
var greatestWidth = 0;
for (var row = 0; row < rows; row += Math.Max(1, columnCount))
return new Measurement(greatestWidth, greatestWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var maxPadding = Math.Max(Padding.GetLeftSafe(), Padding.GetRightSafe());
var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray();
var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding);
if (columnCount == 0)
{
// Temporary work around for extremely small consoles
columnCount = 1;
}
var table = new Table();
table.NoBorder();
table.HideHeaders();
table.PadRightCell = false;
if (Expand)
{
table.Expand();
}
// Add columns
for (var index = 0; index < columnCount; index++)
{
table.AddColumn(new TableColumn(string.Empty)
{
var widths = itemWidths.Skip(row * columnCount).Take(columnCount).ToList();
var totalWidth = widths.Sum() + (maxPadding * (widths.Count - 1));
if (totalWidth > greatestWidth)
{
greatestWidth = totalWidth;
}
}
return new Measurement(greatestWidth, greatestWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var maxPadding = Math.Max(Padding.GetLeftSafe(), Padding.GetRightSafe());
var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray();
var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding);
if (columnCount == 0)
{
// Temporary work around for extremely small consoles
columnCount = 1;
}
var table = new Table();
table.NoBorder();
table.HideHeaders();
table.PadRightCell = false;
if (Expand)
{
table.Expand();
}
// Add columns
for (var index = 0; index < columnCount; index++)
{
table.AddColumn(new TableColumn(string.Empty)
{
Padding = Padding,
NoWrap = true,
});
}
// Add rows
for (var start = 0; start < _items.Count; start += columnCount)
{
table.AddRow(_items.Skip(start).Take(columnCount).ToArray());
}
return ((IRenderable)table).Render(context, maxWidth);
}
// Algorithm borrowed from https://github.com/willmcgugan/rich/blob/master/rich/columns.py
private int CalculateColumnCount(int maxWidth, int[] itemWidths, int columnCount, int padding)
{
var widths = new Dictionary<int, int>();
while (columnCount > 1)
{
var columnIndex = 0;
widths.Clear();
var exceededTotalWidth = false;
foreach (var renderableWidth in IterateWidths(itemWidths, columnCount))
{
widths[columnIndex] = Math.Max(widths.ContainsKey(columnIndex) ? widths[columnIndex] : 0, renderableWidth);
var totalWidth = widths.Values.Sum() + (padding * (widths.Count - 1));
if (totalWidth > maxWidth)
{
columnCount = widths.Count - 1;
exceededTotalWidth = true;
break;
}
else
{
columnIndex = (columnIndex + 1) % columnCount;
}
}
if (!exceededTotalWidth)
Padding = Padding,
NoWrap = true,
});
}
// Add rows
for (var start = 0; start < _items.Count; start += columnCount)
{
table.AddRow(_items.Skip(start).Take(columnCount).ToArray());
}
return ((IRenderable)table).Render(context, maxWidth);
}
// Algorithm borrowed from https://github.com/willmcgugan/rich/blob/master/rich/columns.py
private int CalculateColumnCount(int maxWidth, int[] itemWidths, int columnCount, int padding)
{
var widths = new Dictionary<int, int>();
while (columnCount > 1)
{
var columnIndex = 0;
widths.Clear();
var exceededTotalWidth = false;
foreach (var renderableWidth in IterateWidths(itemWidths, columnCount))
{
widths[columnIndex] = Math.Max(widths.ContainsKey(columnIndex) ? widths[columnIndex] : 0, renderableWidth);
var totalWidth = widths.Values.Sum() + (padding * (widths.Count - 1));
if (totalWidth > maxWidth)
{
columnCount = widths.Count - 1;
exceededTotalWidth = true;
break;
}
else
{
columnIndex = (columnIndex + 1) % columnCount;
}
}
return columnCount;
if (!exceededTotalWidth)
{
break;
}
}
private IEnumerable<int> IterateWidths(int[] itemWidths, int columnCount)
{
foreach (var width in itemWidths)
{
yield return width;
}
return columnCount;
}
if (_items.Count % columnCount != 0)
private IEnumerable<int> IterateWidths(int[] itemWidths, int columnCount)
{
foreach (var width in itemWidths)
{
yield return width;
}
if (_items.Count % columnCount != 0)
{
for (var i = 0; i < columnCount - (_items.Count % columnCount) - 1; i++)
{
for (var i = 0; i < columnCount - (_items.Count % columnCount) - 1; i++)
{
yield return 0;
}
yield return 0;
}
}
}
}
}

View File

@ -1,28 +1,27 @@
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class ControlCode : Renderable
{
internal sealed class ControlCode : Renderable
private readonly Segment _segment;
public ControlCode(string control)
{
private readonly Segment _segment;
_segment = Segment.Control(control);
}
public ControlCode(string control)
{
_segment = Segment.Control(control);
}
protected override Measurement Measure(RenderContext context, int maxWidth)
{
return new Measurement(0, 0);
}
protected override Measurement Measure(RenderContext context, int maxWidth)
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
if (context.Ansi)
{
return new Measurement(0, 0);
}
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
if (context.Ansi)
{
yield return _segment;
}
yield return _segment;
}
}
}
}

View File

@ -5,61 +5,60 @@ using System.Linq;
using System.Reflection;
using System.Text;
namespace Spectre.Console
namespace Spectre.Console;
internal static class ExceptionConverter
{
internal static class ExceptionConverter
public static ExceptionInfo Convert(Exception exception)
{
public static ExceptionInfo Convert(Exception exception)
if (exception is null)
{
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
var exceptionType = exception.GetType();
var stackTrace = new StackTrace(exception, true);
var frames = stackTrace.GetFrames().Where(f => f != null).Cast<StackFrame>().Select(Convert).ToList();
var inner = exception.InnerException is null ? null : Convert(exception.InnerException);
return new ExceptionInfo(exceptionType.FullName ?? exceptionType.Name, exception.Message, frames, inner);
throw new ArgumentNullException(nameof(exception));
}
private static StackFrameInfo Convert(StackFrame frame)
{
var method = frame.GetMethod();
if (method is null)
{
return new StackFrameInfo("<unknown method>", new List<(string Type, string Name)>(), null, null);
}
var methodName = GetMethodName(method);
var parameters = method.GetParameters().Select(e => (e.ParameterType.Name, e.Name ?? string.Empty)).ToList();
var path = frame.GetFileName();
var lineNumber = frame.GetFileLineNumber();
return new StackFrameInfo(methodName, parameters, path, lineNumber == 0 ? null : lineNumber);
}
private static string GetMethodName(MethodBase method)
{
var builder = new StringBuilder(256);
var fullName = method.DeclaringType?.FullName;
if (fullName != null)
{
// See https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L247-L253
builder.Append(fullName.Replace('+', '.'));
builder.Append('.');
}
builder.Append(method.Name);
if (method.IsGenericMethod)
{
builder.Append('[');
builder.Append(string.Join(",", method.GetGenericArguments().Select(t => t.Name)));
builder.Append(']');
}
return builder.ToString();
}
var exceptionType = exception.GetType();
var stackTrace = new StackTrace(exception, true);
var frames = stackTrace.GetFrames().Where(f => f != null).Cast<StackFrame>().Select(Convert).ToList();
var inner = exception.InnerException is null ? null : Convert(exception.InnerException);
return new ExceptionInfo(exceptionType.FullName ?? exceptionType.Name, exception.Message, frames, inner);
}
}
private static StackFrameInfo Convert(StackFrame frame)
{
var method = frame.GetMethod();
if (method is null)
{
return new StackFrameInfo("<unknown method>", new List<(string Type, string Name)>(), null, null);
}
var methodName = GetMethodName(method);
var parameters = method.GetParameters().Select(e => (e.ParameterType.Name, e.Name ?? string.Empty)).ToList();
var path = frame.GetFileName();
var lineNumber = frame.GetFileLineNumber();
return new StackFrameInfo(methodName, parameters, path, lineNumber == 0 ? null : lineNumber);
}
private static string GetMethodName(MethodBase method)
{
var builder = new StringBuilder(256);
var fullName = method.DeclaringType?.FullName;
if (fullName != null)
{
// See https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L247-L253
builder.Append(fullName.Replace('+', '.'));
builder.Append('.');
}
builder.Append(method.Name);
if (method.IsGenericMethod)
{
builder.Append('[');
builder.Append(string.Join(",", method.GetGenericArguments().Select(t => t.Name)));
builder.Append(']');
}
return builder.ToString();
}
}

View File

@ -1,41 +1,40 @@
using System;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents how an exception is formatted.
/// </summary>
[Flags]
public enum ExceptionFormats
{
/// <summary>
/// Represents how an exception is formatted.
/// The default formatting.
/// </summary>
[Flags]
public enum ExceptionFormats
{
/// <summary>
/// The default formatting.
/// </summary>
Default = 0,
Default = 0,
/// <summary>
/// Whether or not paths should be shortened.
/// </summary>
ShortenPaths = 1,
/// <summary>
/// Whether or not paths should be shortened.
/// </summary>
ShortenPaths = 1,
/// <summary>
/// Whether or not types should be shortened.
/// </summary>
ShortenTypes = 2,
/// <summary>
/// Whether or not types should be shortened.
/// </summary>
ShortenTypes = 2,
/// <summary>
/// Whether or not methods should be shortened.
/// </summary>
ShortenMethods = 4,
/// <summary>
/// Whether or not methods should be shortened.
/// </summary>
ShortenMethods = 4,
/// <summary>
/// Whether or not to show paths as links in the terminal.
/// </summary>
ShowLinks = 8,
/// <summary>
/// Whether or not to show paths as links in the terminal.
/// </summary>
ShowLinks = 8,
/// <summary>
/// Shortens everything that can be shortened.
/// </summary>
ShortenEverything = ShortenMethods | ShortenTypes | ShortenPaths,
}
}
/// <summary>
/// Shortens everything that can be shortened.
/// </summary>
ShortenEverything = ShortenMethods | ShortenTypes | ShortenPaths,
}

View File

@ -3,165 +3,164 @@ using System.Linq;
using System.Text;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
internal static class ExceptionFormatter
{
internal static class ExceptionFormatter
public static IRenderable Format(Exception exception, ExceptionSettings settings)
{
public static IRenderable Format(Exception exception, ExceptionSettings settings)
if (exception is null)
{
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
var info = ExceptionConverter.Convert(exception);
return GetException(info, settings);
throw new ArgumentNullException(nameof(exception));
}
private static IRenderable GetException(ExceptionInfo info, ExceptionSettings settings)
{
if (info is null)
{
throw new ArgumentNullException(nameof(info));
}
var info = ExceptionConverter.Convert(exception);
return new Rows(new IRenderable[]
{
return GetException(info, settings);
}
private static IRenderable GetException(ExceptionInfo info, ExceptionSettings settings)
{
if (info is null)
{
throw new ArgumentNullException(nameof(info));
}
return new Rows(new IRenderable[]
{
GetMessage(info, settings),
GetStackFrames(info, settings),
}).Expand();
}
}).Expand();
}
private static Markup GetMessage(ExceptionInfo ex, ExceptionSettings settings)
private static Markup GetMessage(ExceptionInfo ex, ExceptionSettings settings)
{
var shortenTypes = (settings.Format & ExceptionFormats.ShortenTypes) != 0;
var type = Emphasize(ex.Type, new[] { '.' }, settings.Style.Exception, shortenTypes, settings);
var message = $"[{settings.Style.Message.ToMarkup()}]{ex.Message.EscapeMarkup()}[/]";
return new Markup(string.Concat(type, ": ", message));
}
private static Grid GetStackFrames(ExceptionInfo ex, ExceptionSettings settings)
{
var styles = settings.Style;
var grid = new Grid();
grid.AddColumn(new GridColumn().PadLeft(2).PadRight(0).NoWrap());
grid.AddColumn(new GridColumn().PadLeft(1).PadRight(0));
// Inner
if (ex.Inner != null)
{
var shortenTypes = (settings.Format & ExceptionFormats.ShortenTypes) != 0;
var type = Emphasize(ex.Type, new[] { '.' }, settings.Style.Exception, shortenTypes, settings);
var message = $"[{settings.Style.Message.ToMarkup()}]{ex.Message.EscapeMarkup()}[/]";
return new Markup(string.Concat(type, ": ", message));
grid.AddRow(
Text.Empty,
GetException(ex.Inner, settings));
}
private static Grid GetStackFrames(ExceptionInfo ex, ExceptionSettings settings)
{
var styles = settings.Style;
var grid = new Grid();
grid.AddColumn(new GridColumn().PadLeft(2).PadRight(0).NoWrap());
grid.AddColumn(new GridColumn().PadLeft(1).PadRight(0));
// Inner
if (ex.Inner != null)
{
grid.AddRow(
Text.Empty,
GetException(ex.Inner, settings));
}
// Stack frames
foreach (var frame in ex.Frames)
{
var builder = new StringBuilder();
// Method
var shortenMethods = (settings.Format & ExceptionFormats.ShortenMethods) != 0;
builder.Append(Emphasize(frame.Method, new[] { '.' }, styles.Method, shortenMethods, settings));
builder.AppendWithStyle(styles.Parenthesis, "(");
AppendParameters(builder, frame, settings);
builder.AppendWithStyle(styles.Parenthesis, ")");
if (frame.Path != null)
{
builder.Append(' ');
builder.AppendWithStyle(styles.Dimmed, "in");
builder.Append(' ');
// Path
AppendPath(builder, frame, settings);
// Line number
if (frame.LineNumber != null)
{
builder.AppendWithStyle(styles.Dimmed, ":");
builder.AppendWithStyle(styles.LineNumber, frame.LineNumber);
}
}
grid.AddRow(
$"[{styles.Dimmed.ToMarkup()}]at[/]",
builder.ToString());
}
return grid;
}
private static void AppendParameters(StringBuilder builder, StackFrameInfo frame, ExceptionSettings settings)
{
var typeColor = settings.Style.ParameterType.ToMarkup();
var nameColor = settings.Style.ParameterName.ToMarkup();
var parameters = frame.Parameters.Select(x => $"[{typeColor}]{x.Type.EscapeMarkup()}[/] [{nameColor}]{x.Name.EscapeMarkup()}[/]");
builder.Append(string.Join(", ", parameters));
}
private static void AppendPath(StringBuilder builder, StackFrameInfo frame, ExceptionSettings settings)
{
if (frame?.Path is null)
{
return;
}
void AppendPath()
{
var shortenPaths = (settings.Format & ExceptionFormats.ShortenPaths) != 0;
builder.Append(Emphasize(frame.Path, new[] { '/', '\\' }, settings.Style.Path, shortenPaths, settings));
}
if ((settings.Format & ExceptionFormats.ShowLinks) != 0)
{
var hasLink = frame.TryGetUri(out var uri);
if (hasLink && uri != null)
{
builder.Append("[link=").Append(uri.AbsoluteUri).Append(']');
}
AppendPath();
if (hasLink && uri != null)
{
builder.Append("[/]");
}
}
else
{
AppendPath();
}
}
private static string Emphasize(string input, char[] separators, Style color, bool compact, ExceptionSettings settings)
// Stack frames
foreach (var frame in ex.Frames)
{
var builder = new StringBuilder();
var type = input;
var index = type.LastIndexOfAny(separators);
if (index != -1)
// Method
var shortenMethods = (settings.Format & ExceptionFormats.ShortenMethods) != 0;
builder.Append(Emphasize(frame.Method, new[] { '.' }, styles.Method, shortenMethods, settings));
builder.AppendWithStyle(styles.Parenthesis, "(");
AppendParameters(builder, frame, settings);
builder.AppendWithStyle(styles.Parenthesis, ")");
if (frame.Path != null)
{
if (!compact)
builder.Append(' ');
builder.AppendWithStyle(styles.Dimmed, "in");
builder.Append(' ');
// Path
AppendPath(builder, frame, settings);
// Line number
if (frame.LineNumber != null)
{
builder.AppendWithStyle(
settings.Style.NonEmphasized,
type.Substring(0, index + 1));
builder.AppendWithStyle(styles.Dimmed, ":");
builder.AppendWithStyle(styles.LineNumber, frame.LineNumber);
}
builder.AppendWithStyle(
color,
type.Substring(index + 1, type.Length - index - 1));
}
else
grid.AddRow(
$"[{styles.Dimmed.ToMarkup()}]at[/]",
builder.ToString());
}
return grid;
}
private static void AppendParameters(StringBuilder builder, StackFrameInfo frame, ExceptionSettings settings)
{
var typeColor = settings.Style.ParameterType.ToMarkup();
var nameColor = settings.Style.ParameterName.ToMarkup();
var parameters = frame.Parameters.Select(x => $"[{typeColor}]{x.Type.EscapeMarkup()}[/] [{nameColor}]{x.Name.EscapeMarkup()}[/]");
builder.Append(string.Join(", ", parameters));
}
private static void AppendPath(StringBuilder builder, StackFrameInfo frame, ExceptionSettings settings)
{
if (frame?.Path is null)
{
return;
}
void AppendPath()
{
var shortenPaths = (settings.Format & ExceptionFormats.ShortenPaths) != 0;
builder.Append(Emphasize(frame.Path, new[] { '/', '\\' }, settings.Style.Path, shortenPaths, settings));
}
if ((settings.Format & ExceptionFormats.ShowLinks) != 0)
{
var hasLink = frame.TryGetUri(out var uri);
if (hasLink && uri != null)
{
builder.Append(type.EscapeMarkup());
builder.Append("[link=").Append(uri.AbsoluteUri).Append(']');
}
return builder.ToString();
AppendPath();
if (hasLink && uri != null)
{
builder.Append("[/]");
}
}
else
{
AppendPath();
}
}
}
private static string Emphasize(string input, char[] separators, Style color, bool compact, ExceptionSettings settings)
{
var builder = new StringBuilder();
var type = input;
var index = type.LastIndexOfAny(separators);
if (index != -1)
{
if (!compact)
{
builder.AppendWithStyle(
settings.Style.NonEmphasized,
type.Substring(0, index + 1));
}
builder.AppendWithStyle(
color,
type.Substring(index + 1, type.Length - index - 1));
}
else
{
builder.Append(type.EscapeMarkup());
}
return builder.ToString();
}
}

View File

@ -1,23 +1,22 @@
using System.Collections.Generic;
namespace Spectre.Console
{
internal sealed class ExceptionInfo
{
public string Type { get; }
public string Message { get; }
public List<StackFrameInfo> Frames { get; }
public ExceptionInfo? Inner { get; }
namespace Spectre.Console;
public ExceptionInfo(
string type, string message,
List<StackFrameInfo> frames,
ExceptionInfo? inner)
{
Type = type ?? string.Empty;
Message = message ?? string.Empty;
Frames = frames ?? new List<StackFrameInfo>();
Inner = inner;
}
internal sealed class ExceptionInfo
{
public string Type { get; }
public string Message { get; }
public List<StackFrameInfo> Frames { get; }
public ExceptionInfo? Inner { get; }
public ExceptionInfo(
string type, string message,
List<StackFrameInfo> frames,
ExceptionInfo? inner)
{
Type = type ?? string.Empty;
Message = message ?? string.Empty;
Frames = frames ?? new List<StackFrameInfo>();
Inner = inner;
}
}
}

View File

@ -1,27 +1,26 @@
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Exception settings.
/// </summary>
public sealed class ExceptionSettings
{
/// <summary>
/// Exception settings.
/// Gets or sets the exception format.
/// </summary>
public sealed class ExceptionSettings
public ExceptionFormats Format { get; set; }
/// <summary>
/// Gets or sets the exception style.
/// </summary>
public ExceptionStyle Style { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ExceptionSettings"/> class.
/// </summary>
public ExceptionSettings()
{
/// <summary>
/// Gets or sets the exception format.
/// </summary>
public ExceptionFormats Format { get; set; }
/// <summary>
/// Gets or sets the exception style.
/// </summary>
public ExceptionStyle Style { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ExceptionSettings"/> class.
/// </summary>
public ExceptionSettings()
{
Format = ExceptionFormats.Default;
Style = new ExceptionStyle();
}
Format = ExceptionFormats.Default;
Style = new ExceptionStyle();
}
}
}

View File

@ -1,58 +1,57 @@
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represent an exception style.
/// </summary>
public sealed class ExceptionStyle
{
/// <summary>
/// Represent an exception style.
/// Gets or sets the message color.
/// </summary>
public sealed class ExceptionStyle
{
/// <summary>
/// Gets or sets the message color.
/// </summary>
public Style Message { get; set; } = new Style(Color.Red, Color.Default, Decoration.Bold);
public Style Message { get; set; } = new Style(Color.Red, Color.Default, Decoration.Bold);
/// <summary>
/// Gets or sets the exception color.
/// </summary>
public Style Exception { get; set; } = new Style(Color.White);
/// <summary>
/// Gets or sets the exception color.
/// </summary>
public Style Exception { get; set; } = new Style(Color.White);
/// <summary>
/// Gets or sets the method color.
/// </summary>
public Style Method { get; set; } = new Style(Color.Yellow);
/// <summary>
/// Gets or sets the method color.
/// </summary>
public Style Method { get; set; } = new Style(Color.Yellow);
/// <summary>
/// Gets or sets the parameter type color.
/// </summary>
public Style ParameterType { get; set; } = new Style(Color.Blue);
/// <summary>
/// Gets or sets the parameter type color.
/// </summary>
public Style ParameterType { get; set; } = new Style(Color.Blue);
/// <summary>
/// Gets or sets the parameter name color.
/// </summary>
public Style ParameterName { get; set; } = new Style(Color.Silver);
/// <summary>
/// Gets or sets the parameter name color.
/// </summary>
public Style ParameterName { get; set; } = new Style(Color.Silver);
/// <summary>
/// Gets or sets the parenthesis color.
/// </summary>
public Style Parenthesis { get; set; } = new Style(Color.Silver);
/// <summary>
/// Gets or sets the parenthesis color.
/// </summary>
public Style Parenthesis { get; set; } = new Style(Color.Silver);
/// <summary>
/// Gets or sets the path color.
/// </summary>
public Style Path { get; set; } = new Style(Color.Yellow, Color.Default, Decoration.Bold);
/// <summary>
/// Gets or sets the path color.
/// </summary>
public Style Path { get; set; } = new Style(Color.Yellow, Color.Default, Decoration.Bold);
/// <summary>
/// Gets or sets the line number color.
/// </summary>
public Style LineNumber { get; set; } = new Style(Color.Blue);
/// <summary>
/// Gets or sets the line number color.
/// </summary>
public Style LineNumber { get; set; } = new Style(Color.Blue);
/// <summary>
/// Gets or sets the color for dimmed text such as "at" or "in".
/// </summary>
public Style Dimmed { get; set; } = new Style(Color.Grey);
/// <summary>
/// Gets or sets the color for dimmed text such as "at" or "in".
/// </summary>
public Style Dimmed { get; set; } = new Style(Color.Grey);
/// <summary>
/// Gets or sets the color for non emphasized items.
/// </summary>
public Style NonEmphasized { get; set; } = new Style(Color.Silver);
}
}
/// <summary>
/// Gets or sets the color for non emphasized items.
/// </summary>
public Style NonEmphasized { get; set; } = new Style(Color.Silver);
}

View File

@ -3,62 +3,61 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class StackFrameInfo
{
internal sealed class StackFrameInfo
public string Method { get; }
public List<(string Type, string Name)> Parameters { get; }
public string? Path { get; }
public int? LineNumber { get; }
public StackFrameInfo(
string method, List<(string Type, string Name)> parameters,
string? path, int? lineNumber)
{
public string Method { get; }
public List<(string Type, string Name)> Parameters { get; }
public string? Path { get; }
public int? LineNumber { get; }
Method = method ?? throw new ArgumentNullException(nameof(method));
Parameters = parameters ?? throw new ArgumentNullException(nameof(parameters));
Path = path;
LineNumber = lineNumber;
}
public StackFrameInfo(
string method, List<(string Type, string Name)> parameters,
string? path, int? lineNumber)
public bool TryGetUri([NotNullWhen(true)] out Uri? result)
{
try
{
Method = method ?? throw new ArgumentNullException(nameof(method));
Parameters = parameters ?? throw new ArgumentNullException(nameof(parameters));
Path = path;
LineNumber = lineNumber;
}
public bool TryGetUri([NotNullWhen(true)] out Uri? result)
{
try
{
if (Path == null)
{
result = null;
return false;
}
if (!Uri.TryCreate(Path, UriKind.Absolute, out var uri))
{
result = null;
return false;
}
if (uri.Scheme == "file")
{
// For local files, we need to append
// the host name. Otherwise the terminal
// will most probably not allow it.
var builder = new UriBuilder(uri)
{
Host = Dns.GetHostName(),
};
uri = builder.Uri;
}
result = uri;
return true;
}
catch
if (Path == null)
{
result = null;
return false;
}
if (!Uri.TryCreate(Path, UriKind.Absolute, out var uri))
{
result = null;
return false;
}
if (uri.Scheme == "file")
{
// For local files, we need to append
// the host name. Otherwise the terminal
// will most probably not allow it.
var builder = new UriBuilder(uri)
{
Host = Dns.GetHostName(),
};
uri = builder.Uri;
}
result = uri;
return true;
}
catch
{
result = null;
return false;
}
}
}
}

View File

@ -2,29 +2,28 @@ using System;
using System.Collections.Generic;
using System.Linq;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class FigletCharacter
{
internal sealed class FigletCharacter
public int Code { get; }
public int Width { get; }
public int Height { get; }
public IReadOnlyList<string> Lines { get; }
public FigletCharacter(int code, IEnumerable<string> lines)
{
public int Code { get; }
public int Width { get; }
public int Height { get; }
public IReadOnlyList<string> Lines { get; }
Code = code;
Lines = new List<string>(lines ?? throw new ArgumentNullException(nameof(lines)));
public FigletCharacter(int code, IEnumerable<string> lines)
var min = Lines.Min(x => x.Length);
var max = Lines.Max(x => x.Length);
if (min != max)
{
Code = code;
Lines = new List<string>(lines ?? throw new ArgumentNullException(nameof(lines)));
var min = Lines.Min(x => x.Length);
var max = Lines.Max(x => x.Length);
if (min != max)
{
throw new InvalidOperationException($"Figlet character #{code} has varying width");
}
Width = max;
Height = Lines.Count;
throw new InvalidOperationException($"Figlet character #{code} has varying width");
}
Width = max;
Height = Lines.Count;
}
}
}

View File

@ -2,135 +2,134 @@ using System;
using System.Collections.Generic;
using System.IO;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents a FIGlet font.
/// </summary>
public sealed class FigletFont
{
private const string StandardFont = "Spectre.Console/Widgets/Figlet/Fonts/Standard.flf";
private readonly Dictionary<int, FigletCharacter> _characters;
private static readonly Lazy<FigletFont> _standard;
/// <summary>
/// Represents a FIGlet font.
/// Gets the number of characters in the font.
/// </summary>
public sealed class FigletFont
public int Count => _characters.Count;
/// <summary>
/// Gets the height of the font.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the font's baseline.
/// </summary>
public int Baseline { get; }
/// <summary>
/// Gets the font's maximum width.
/// </summary>
public int MaxWidth { get; }
/// <summary>
/// Gets the default FIGlet font.
/// </summary>
public static FigletFont Default => _standard.Value;
static FigletFont()
{
private const string StandardFont = "Spectre.Console/Widgets/Figlet/Fonts/Standard.flf";
_standard = new Lazy<FigletFont>(() => Parse(
ResourceReader.ReadManifestData(StandardFont)));
}
private readonly Dictionary<int, FigletCharacter> _characters;
private static readonly Lazy<FigletFont> _standard;
internal FigletFont(IEnumerable<FigletCharacter> characters, FigletHeader header)
{
_characters = new Dictionary<int, FigletCharacter>();
/// <summary>
/// Gets the number of characters in the font.
/// </summary>
public int Count => _characters.Count;
/// <summary>
/// Gets the height of the font.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the font's baseline.
/// </summary>
public int Baseline { get; }
/// <summary>
/// Gets the font's maximum width.
/// </summary>
public int MaxWidth { get; }
/// <summary>
/// Gets the default FIGlet font.
/// </summary>
public static FigletFont Default => _standard.Value;
static FigletFont()
foreach (var character in characters)
{
_standard = new Lazy<FigletFont>(() => Parse(
ResourceReader.ReadManifestData(StandardFont)));
}
internal FigletFont(IEnumerable<FigletCharacter> characters, FigletHeader header)
{
_characters = new Dictionary<int, FigletCharacter>();
foreach (var character in characters)
if (_characters.ContainsKey(character.Code))
{
if (_characters.ContainsKey(character.Code))
{
throw new InvalidOperationException("Character already exist");
}
_characters[character.Code] = character;
throw new InvalidOperationException("Character already exist");
}
Height = header.Height;
Baseline = header.Baseline;
MaxWidth = header.MaxLength;
_characters[character.Code] = character;
}
/// <summary>
/// Loads a FIGlet font from the specified stream.
/// </summary>
/// <param name="stream">The stream to load the FIGlet font from.</param>
/// <returns>The loaded FIGlet font.</returns>
public static FigletFont Load(Stream stream)
Height = header.Height;
Baseline = header.Baseline;
MaxWidth = header.MaxLength;
}
/// <summary>
/// Loads a FIGlet font from the specified stream.
/// </summary>
/// <param name="stream">The stream to load the FIGlet font from.</param>
/// <returns>The loaded FIGlet font.</returns>
public static FigletFont Load(Stream stream)
{
using (var reader = new StreamReader(stream))
{
using (var reader = new StreamReader(stream))
{
return Parse(reader.ReadToEnd());
}
}
/// <summary>
/// Loads a FIGlet font from disk.
/// </summary>
/// <param name="path">The path of the FIGlet font to load.</param>
/// <returns>The loaded FIGlet font.</returns>
public static FigletFont Load(string path)
{
return Parse(File.ReadAllText(path));
}
/// <summary>
/// Parses a FIGlet font from the specified <see cref="string"/>.
/// </summary>
/// <param name="source">The FIGlet font source.</param>
/// <returns>The parsed FIGlet font.</returns>
public static FigletFont Parse(string source)
{
return FigletFontParser.Parse(source);
}
internal int GetWidth(string text)
{
var width = 0;
foreach (var character in text)
{
width += GetCharacter(character)?.Width ?? 0;
}
return width;
}
internal FigletCharacter? GetCharacter(char character)
{
_characters.TryGetValue(character, out var result);
return result;
}
internal IEnumerable<FigletCharacter> GetCharacters(string text)
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
var result = new List<FigletCharacter>();
foreach (var character in text)
{
if (_characters.TryGetValue(character, out var figletCharacter))
{
result.Add(figletCharacter);
}
}
return result;
return Parse(reader.ReadToEnd());
}
}
}
/// <summary>
/// Loads a FIGlet font from disk.
/// </summary>
/// <param name="path">The path of the FIGlet font to load.</param>
/// <returns>The loaded FIGlet font.</returns>
public static FigletFont Load(string path)
{
return Parse(File.ReadAllText(path));
}
/// <summary>
/// Parses a FIGlet font from the specified <see cref="string"/>.
/// </summary>
/// <param name="source">The FIGlet font source.</param>
/// <returns>The parsed FIGlet font.</returns>
public static FigletFont Parse(string source)
{
return FigletFontParser.Parse(source);
}
internal int GetWidth(string text)
{
var width = 0;
foreach (var character in text)
{
width += GetCharacter(character)?.Width ?? 0;
}
return width;
}
internal FigletCharacter? GetCharacter(char character)
{
_characters.TryGetValue(character, out var result);
return result;
}
internal IEnumerable<FigletCharacter> GetCharacters(string text)
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
var result = new List<FigletCharacter>();
foreach (var character in text)
{
if (_characters.TryGetValue(character, out var figletCharacter))
{
result.Add(figletCharacter);
}
}
return result;
}
}

View File

@ -3,112 +3,111 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Spectre.Console
namespace Spectre.Console;
internal static class FigletFontParser
{
internal static class FigletFontParser
public static FigletFont Parse(string source)
{
public static FigletFont Parse(string source)
var lines = source.SplitLines();
var header = ParseHeader(lines.FirstOrDefault());
var characters = new List<FigletCharacter>();
var index = 32;
var indexOverridden = false;
var hasOverriddenIndex = false;
var buffer = new List<string>();
foreach (var line in lines.Skip(header.CommentLines + 1))
{
var lines = source.SplitLines();
var header = ParseHeader(lines.FirstOrDefault());
var characters = new List<FigletCharacter>();
var index = 32;
var indexOverridden = false;
var hasOverriddenIndex = false;
var buffer = new List<string>();
foreach (var line in lines.Skip(header.CommentLines + 1))
if (!line.EndsWith("@", StringComparison.Ordinal))
{
if (!line.EndsWith("@", StringComparison.Ordinal))
var words = line.SplitWords();
if (words.Length > 0 && TryParseIndex(words[0], out var newIndex))
{
var words = line.SplitWords();
if (words.Length > 0 && TryParseIndex(words[0], out var newIndex))
{
index = newIndex;
indexOverridden = true;
hasOverriddenIndex = true;
continue;
}
index = newIndex;
indexOverridden = true;
hasOverriddenIndex = true;
continue;
}
if (hasOverriddenIndex && !indexOverridden)
continue;
}
if (hasOverriddenIndex && !indexOverridden)
{
throw new InvalidOperationException("Unknown index for FIGlet character");
}
buffer.Add(line.Replace(header.Hardblank, ' ').ReplaceExact("@", string.Empty));
if (line.EndsWith("@@", StringComparison.Ordinal))
{
characters.Add(new FigletCharacter(index, buffer));
buffer.Clear();
if (!hasOverriddenIndex)
{
throw new InvalidOperationException("Unknown index for FIGlet character");
index++;
}
buffer.Add(line.Replace(header.Hardblank, ' ').ReplaceExact("@", string.Empty));
if (line.EndsWith("@@", StringComparison.Ordinal))
{
characters.Add(new FigletCharacter(index, buffer));
buffer.Clear();
if (!hasOverriddenIndex)
{
index++;
}
// Reset the flag so we know if we're trying to parse
// a character that wasn't prefixed with an ASCII index.
indexOverridden = false;
}
// Reset the flag so we know if we're trying to parse
// a character that wasn't prefixed with an ASCII index.
indexOverridden = false;
}
return new FigletFont(characters, header);
}
private static bool TryParseIndex(string index, out int result)
{
var style = NumberStyles.Integer;
if (index.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
// TODO: ReplaceExact should not be used
index = index.ReplaceExact("0x", string.Empty).ReplaceExact("0x", string.Empty);
style = NumberStyles.HexNumber;
}
return int.TryParse(index, style, CultureInfo.InvariantCulture, out result);
}
private static FigletHeader ParseHeader(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
throw new InvalidOperationException("Invalid Figlet font");
}
var parts = text.SplitWords(StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 6)
{
throw new InvalidOperationException("Invalid Figlet font header");
}
if (!IsValidSignature(parts[0]))
{
throw new InvalidOperationException("Invalid Figlet font header signature");
}
return new FigletHeader
{
Hardblank = parts[0][5],
Height = int.Parse(parts[1], CultureInfo.InvariantCulture),
Baseline = int.Parse(parts[2], CultureInfo.InvariantCulture),
MaxLength = int.Parse(parts[3], CultureInfo.InvariantCulture),
OldLayout = int.Parse(parts[4], CultureInfo.InvariantCulture),
CommentLines = int.Parse(parts[5], CultureInfo.InvariantCulture),
};
}
private static bool IsValidSignature(string signature)
{
return signature.Length == 6
&& signature[0] == 'f' && signature[1] == 'l'
&& signature[2] == 'f' && signature[3] == '2'
&& signature[4] == 'a';
}
return new FigletFont(characters, header);
}
}
private static bool TryParseIndex(string index, out int result)
{
var style = NumberStyles.Integer;
if (index.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
// TODO: ReplaceExact should not be used
index = index.ReplaceExact("0x", string.Empty).ReplaceExact("0x", string.Empty);
style = NumberStyles.HexNumber;
}
return int.TryParse(index, style, CultureInfo.InvariantCulture, out result);
}
private static FigletHeader ParseHeader(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
throw new InvalidOperationException("Invalid Figlet font");
}
var parts = text.SplitWords(StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 6)
{
throw new InvalidOperationException("Invalid Figlet font header");
}
if (!IsValidSignature(parts[0]))
{
throw new InvalidOperationException("Invalid Figlet font header signature");
}
return new FigletHeader
{
Hardblank = parts[0][5],
Height = int.Parse(parts[1], CultureInfo.InvariantCulture),
Baseline = int.Parse(parts[2], CultureInfo.InvariantCulture),
MaxLength = int.Parse(parts[3], CultureInfo.InvariantCulture),
OldLayout = int.Parse(parts[4], CultureInfo.InvariantCulture),
CommentLines = int.Parse(parts[5], CultureInfo.InvariantCulture),
};
}
private static bool IsValidSignature(string signature)
{
return signature.Length == 6
&& signature[0] == 'f' && signature[1] == 'l'
&& signature[2] == 'f' && signature[3] == '2'
&& signature[4] == 'a';
}
}

View File

@ -1,12 +1,11 @@
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class FigletHeader
{
internal sealed class FigletHeader
{
public char Hardblank { get; set; }
public int Height { get; set; }
public int Baseline { get; set; }
public int MaxLength { get; set; }
public int OldLayout { get; set; }
public int CommentLines { get; set; }
}
}
public char Hardblank { get; set; }
public int Height { get; set; }
public int Baseline { get; set; }
public int MaxLength { get; set; }
public int OldLayout { get; set; }
public int CommentLines { get; set; }
}

View File

@ -3,149 +3,148 @@ using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents text rendered with a FIGlet font.
/// </summary>
public sealed class FigletText : Renderable, IAlignable
{
private readonly FigletFont _font;
private readonly string _text;
/// <summary>
/// Represents text rendered with a FIGlet font.
/// Gets or sets the color of the text.
/// </summary>
public sealed class FigletText : Renderable, IAlignable
public Color? Color { get; set; }
/// <inheritdoc/>
public Justify? Alignment { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="FigletText"/> class.
/// </summary>
/// <param name="text">The text.</param>
public FigletText(string text)
: this(FigletFont.Default, text)
{
private readonly FigletFont _font;
private readonly string _text;
}
/// <summary>
/// Gets or sets the color of the text.
/// </summary>
public Color? Color { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="FigletText"/> class.
/// </summary>
/// <param name="font">The FIGlet font to use.</param>
/// <param name="text">The text.</param>
public FigletText(FigletFont font, string text)
{
_font = font ?? throw new ArgumentNullException(nameof(font));
_text = text ?? throw new ArgumentNullException(nameof(text));
}
/// <inheritdoc/>
public Justify? Alignment { get; set; }
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var style = new Style(Color ?? Console.Color.Default);
var alignment = Alignment ?? Justify.Left;
/// <summary>
/// Initializes a new instance of the <see cref="FigletText"/> class.
/// </summary>
/// <param name="text">The text.</param>
public FigletText(string text)
: this(FigletFont.Default, text)
foreach (var row in GetRows(maxWidth))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="FigletText"/> class.
/// </summary>
/// <param name="font">The FIGlet font to use.</param>
/// <param name="text">The text.</param>
public FigletText(FigletFont font, string text)
{
_font = font ?? throw new ArgumentNullException(nameof(font));
_text = text ?? throw new ArgumentNullException(nameof(text));
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var style = new Style(Color ?? Console.Color.Default);
var alignment = Alignment ?? Justify.Left;
foreach (var row in GetRows(maxWidth))
for (var index = 0; index < _font.Height; index++)
{
for (var index = 0; index < _font.Height; index++)
var line = new Segment(string.Concat(row.Select(x => x.Lines[index])), style);
var lineWidth = line.CellCount();
if (alignment == Justify.Left)
{
var line = new Segment(string.Concat(row.Select(x => x.Lines[index])), style);
yield return line;
var lineWidth = line.CellCount();
if (alignment == Justify.Left)
if (lineWidth < maxWidth)
{
yield return line;
if (lineWidth < maxWidth)
{
yield return Segment.Padding(maxWidth - lineWidth);
}
yield return Segment.Padding(maxWidth - lineWidth);
}
else if (alignment == Justify.Center)
{
var left = (maxWidth - lineWidth) / 2;
var right = left + ((maxWidth - lineWidth) % 2);
yield return Segment.Padding(left);
yield return line;
yield return Segment.Padding(right);
}
else if (alignment == Justify.Right)
{
if (lineWidth < maxWidth)
{
yield return Segment.Padding(maxWidth - lineWidth);
}
yield return line;
}
yield return Segment.LineBreak;
}
else if (alignment == Justify.Center)
{
var left = (maxWidth - lineWidth) / 2;
var right = left + ((maxWidth - lineWidth) % 2);
yield return Segment.Padding(left);
yield return line;
yield return Segment.Padding(right);
}
else if (alignment == Justify.Right)
{
if (lineWidth < maxWidth)
{
yield return Segment.Padding(maxWidth - lineWidth);
}
yield return line;
}
yield return Segment.LineBreak;
}
}
}
private List<List<FigletCharacter>> GetRows(int maxWidth)
private List<List<FigletCharacter>> GetRows(int maxWidth)
{
var result = new List<List<FigletCharacter>>();
var words = _text.SplitWords(StringSplitOptions.None);
var totalWidth = 0;
var line = new List<FigletCharacter>();
foreach (var word in words)
{
var result = new List<List<FigletCharacter>>();
var words = _text.SplitWords(StringSplitOptions.None);
var totalWidth = 0;
var line = new List<FigletCharacter>();
foreach (var word in words)
// Does the whole word fit?
var width = _font.GetWidth(word);
if (width + totalWidth < maxWidth)
{
// Does the whole word fit?
var width = _font.GetWidth(word);
if (width + totalWidth < maxWidth)
// Add it to the line
line.AddRange(_font.GetCharacters(word));
totalWidth += width;
}
else
{
// Does it fit on its own line?
if (width < maxWidth)
{
// Add it to the line
// Flush the line
result.Add(line);
line = new List<FigletCharacter>();
totalWidth = 0;
line.AddRange(_font.GetCharacters(word));
totalWidth += width;
}
else
{
// Does it fit on its own line?
if (width < maxWidth)
// We need to split it up.
var queue = new Queue<FigletCharacter>(_font.GetCharacters(word));
while (queue.Count > 0)
{
// Flush the line
result.Add(line);
line = new List<FigletCharacter>();
totalWidth = 0;
line.AddRange(_font.GetCharacters(word));
totalWidth += width;
}
else
{
// We need to split it up.
var queue = new Queue<FigletCharacter>(_font.GetCharacters(word));
while (queue.Count > 0)
var current = queue.Dequeue();
if (totalWidth + current.Width > maxWidth)
{
var current = queue.Dequeue();
if (totalWidth + current.Width > maxWidth)
{
// Flush the line
result.Add(line);
line = new List<FigletCharacter>();
totalWidth = 0;
}
line.Add(current);
totalWidth += current.Width;
// Flush the line
result.Add(line);
line = new List<FigletCharacter>();
totalWidth = 0;
}
line.Add(current);
totalWidth += current.Width;
}
}
}
if (line.Count > 0)
{
result.Add(line);
}
return result;
}
if (line.Count > 0)
{
result.Add(line);
}
return result;
}
}
}

View File

@ -3,151 +3,150 @@ using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// A renderable grid.
/// </summary>
public sealed class Grid : JustInTimeRenderable, IExpandable, IAlignable
{
private readonly ListWithCallback<GridColumn> _columns;
private readonly ListWithCallback<GridRow> _rows;
private bool _expand;
private Justify? _alignment;
private bool _padRightCell;
/// <summary>
/// A renderable grid.
/// Gets the grid columns.
/// </summary>
public sealed class Grid : JustInTimeRenderable, IExpandable, IAlignable
public IReadOnlyList<GridColumn> Columns => _columns;
/// <summary>
/// Gets the grid rows.
/// </summary>
public IReadOnlyList<GridRow> Rows => _rows;
/// <inheritdoc/>
public bool Expand
{
private readonly ListWithCallback<GridColumn> _columns;
private readonly ListWithCallback<GridRow> _rows;
private bool _expand;
private Justify? _alignment;
private bool _padRightCell;
/// <summary>
/// Gets the grid columns.
/// </summary>
public IReadOnlyList<GridColumn> Columns => _columns;
/// <summary>
/// Gets the grid rows.
/// </summary>
public IReadOnlyList<GridRow> Rows => _rows;
/// <inheritdoc/>
public bool Expand
{
get => _expand;
set => MarkAsDirty(() => _expand = value);
}
/// <inheritdoc/>
public Justify? Alignment
{
get => _alignment;
set => MarkAsDirty(() => _alignment = value);
}
/// <summary>
/// Gets or sets the width of the grid.
/// </summary>
public int? Width { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Grid"/> class.
/// </summary>
public Grid()
{
_expand = false;
_alignment = null;
_columns = new ListWithCallback<GridColumn>(() => MarkAsDirty());
_rows = new ListWithCallback<GridRow>(() => MarkAsDirty());
}
/// <summary>
/// Adds a column to the grid.
/// </summary>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Grid AddColumn()
{
AddColumn(new GridColumn());
return this;
}
/// <summary>
/// Adds a column to the grid.
/// </summary>
/// <param name="column">The column to add.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Grid AddColumn(GridColumn column)
{
if (column is null)
{
throw new ArgumentNullException(nameof(column));
}
if (_rows.Count > 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.
_padRightCell = column.HasExplicitPadding;
_columns.Add(column);
return this;
}
/// <summary>
/// Adds a new row to the grid.
/// </summary>
/// <param name="columns">The columns to add.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Grid 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 greater than the number of grid columns.");
}
_rows.Add(new GridRow(columns));
return this;
}
/// <inheritdoc/>
protected override bool HasDirtyChildren()
{
return _columns.Any(c => ((IHasDirtyState)c).IsDirty);
}
/// <inheritdoc/>
protected override IRenderable Build()
{
var table = new Table
{
Border = TableBorder.None,
ShowHeaders = false,
IsGrid = true,
PadRightCell = _padRightCell,
Width = Width,
};
foreach (var column in _columns)
{
table.AddColumn(new TableColumn(string.Empty)
{
Width = column.Width,
NoWrap = column.NoWrap,
Padding = column.Padding ?? new Padding(0, 0, 2, 0),
Alignment = column.Alignment,
});
}
foreach (var row in _rows)
{
table.AddRow(row);
}
return table;
}
get => _expand;
set => MarkAsDirty(() => _expand = value);
}
}
/// <inheritdoc/>
public Justify? Alignment
{
get => _alignment;
set => MarkAsDirty(() => _alignment = value);
}
/// <summary>
/// Gets or sets the width of the grid.
/// </summary>
public int? Width { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Grid"/> class.
/// </summary>
public Grid()
{
_expand = false;
_alignment = null;
_columns = new ListWithCallback<GridColumn>(() => MarkAsDirty());
_rows = new ListWithCallback<GridRow>(() => MarkAsDirty());
}
/// <summary>
/// Adds a column to the grid.
/// </summary>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Grid AddColumn()
{
AddColumn(new GridColumn());
return this;
}
/// <summary>
/// Adds a column to the grid.
/// </summary>
/// <param name="column">The column to add.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Grid AddColumn(GridColumn column)
{
if (column is null)
{
throw new ArgumentNullException(nameof(column));
}
if (_rows.Count > 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.
_padRightCell = column.HasExplicitPadding;
_columns.Add(column);
return this;
}
/// <summary>
/// Adds a new row to the grid.
/// </summary>
/// <param name="columns">The columns to add.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Grid 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 greater than the number of grid columns.");
}
_rows.Add(new GridRow(columns));
return this;
}
/// <inheritdoc/>
protected override bool HasDirtyChildren()
{
return _columns.Any(c => ((IHasDirtyState)c).IsDirty);
}
/// <inheritdoc/>
protected override IRenderable Build()
{
var table = new Table
{
Border = TableBorder.None,
ShowHeaders = false,
IsGrid = true,
PadRightCell = _padRightCell,
Width = Width,
};
foreach (var column in _columns)
{
table.AddColumn(new TableColumn(string.Empty)
{
Width = column.Width,
NoWrap = column.NoWrap,
Padding = column.Padding ?? new Padding(0, 0, 2, 0),
Alignment = column.Alignment,
});
}
foreach (var row in _rows)
{
table.AddRow(row);
}
return table;
}
}

View File

@ -1,71 +1,70 @@
using System;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents a grid column.
/// </summary>
public sealed class GridColumn : IColumn, IHasDirtyState
{
private bool _isDirty;
private int? _width;
private bool _noWrap;
private Padding? _padding;
private Justify? _alignment;
/// <inheritdoc/>
bool IHasDirtyState.IsDirty => _isDirty;
/// <summary>
/// Represents a grid column.
/// Gets or sets the width of the column.
/// If <c>null</c>, the column will adapt to its contents.
/// </summary>
public sealed class GridColumn : IColumn, IHasDirtyState
public int? Width
{
private bool _isDirty;
private int? _width;
private bool _noWrap;
private Padding? _padding;
private Justify? _alignment;
/// <inheritdoc/>
bool IHasDirtyState.IsDirty => _isDirty;
/// <summary>
/// Gets or sets the width of the column.
/// If <c>null</c>, the column will adapt to its contents.
/// </summary>
public int? Width
{
get => _width;
set => MarkAsDirty(() => _width = value);
}
/// <summary>
/// Gets or sets a value indicating whether wrapping of
/// text within the column should be prevented.
/// </summary>
public bool NoWrap
{
get => _noWrap;
set => MarkAsDirty(() => _noWrap = value);
}
/// <summary>
/// Gets or sets the padding of the column.
/// Vertical padding (top and bottom) is ignored.
/// </summary>
public Padding? Padding
{
get => _padding;
set => MarkAsDirty(() => _padding = value);
}
/// <summary>
/// Gets or sets the alignment of the column.
/// </summary>
public Justify? Alignment
{
get => _alignment;
set => MarkAsDirty(() => _alignment = value);
}
/// <summary>
/// Gets a value indicating whether the user
/// has set an explicit padding for this column.
/// </summary>
internal bool HasExplicitPadding => Padding != null;
private void MarkAsDirty(Action action)
{
action();
_isDirty = true;
}
get => _width;
set => MarkAsDirty(() => _width = value);
}
}
/// <summary>
/// Gets or sets a value indicating whether wrapping of
/// text within the column should be prevented.
/// </summary>
public bool NoWrap
{
get => _noWrap;
set => MarkAsDirty(() => _noWrap = value);
}
/// <summary>
/// Gets or sets the padding of the column.
/// Vertical padding (top and bottom) is ignored.
/// </summary>
public Padding? Padding
{
get => _padding;
set => MarkAsDirty(() => _padding = value);
}
/// <summary>
/// Gets or sets the alignment of the column.
/// </summary>
public Justify? Alignment
{
get => _alignment;
set => MarkAsDirty(() => _alignment = value);
}
/// <summary>
/// Gets a value indicating whether the user
/// has set an explicit padding for this column.
/// </summary>
internal bool HasExplicitPadding => Padding != null;
private void MarkAsDirty(Action action)
{
action();
_isDirty = true;
}
}

View File

@ -3,54 +3,53 @@ using System.Collections;
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents a grid row.
/// </summary>
public sealed class GridRow : IEnumerable<IRenderable>
{
private readonly List<IRenderable> _items;
/// <summary>
/// Represents a grid row.
/// Gets a row item at the specified grid column index.
/// </summary>
public sealed class GridRow : IEnumerable<IRenderable>
/// <param name="index">The grid column index.</param>
/// <returns>The row item at the specified grid column index.</returns>
public IRenderable this[int index]
{
private readonly List<IRenderable> _items;
/// <summary>
/// Gets a row item at the specified grid column index.
/// </summary>
/// <param name="index">The grid column index.</param>
/// <returns>The row item at the specified grid column index.</returns>
public IRenderable this[int index]
{
get => _items[index];
}
/// <summary>
/// Initializes a new instance of the <see cref="GridRow"/> class.
/// </summary>
/// <param name="items">The row items.</param>
public GridRow(IEnumerable<IRenderable> items)
{
_items = new List<IRenderable>(items ?? Array.Empty<IRenderable>());
}
internal void Add(IRenderable item)
{
if (item is null)
{
throw new ArgumentNullException(nameof(item));
}
_items.Add(item);
}
/// <inheritdoc/>
public IEnumerator<IRenderable> GetEnumerator()
{
return _items.GetEnumerator();
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
get => _items[index];
}
}
/// <summary>
/// Initializes a new instance of the <see cref="GridRow"/> class.
/// </summary>
/// <param name="items">The row items.</param>
public GridRow(IEnumerable<IRenderable> items)
{
_items = new List<IRenderable>(items ?? Array.Empty<IRenderable>());
}
internal void Add(IRenderable item)
{
if (item is null)
{
throw new ArgumentNullException(nameof(item));
}
_items.Add(item);
}
/// <inheritdoc/>
public IEnumerator<IRenderable> GetEnumerator()
{
return _items.GetEnumerator();
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

View File

@ -3,90 +3,89 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// A renderable piece of markup text.
/// </summary>
[SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
public sealed class Markup : Renderable, IAlignable, IOverflowable
{
/// <summary>
/// A renderable piece of markup text.
/// </summary>
[SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
public sealed class Markup : Renderable, IAlignable, IOverflowable
private readonly Paragraph _paragraph;
/// <inheritdoc/>
public Justify? Alignment
{
private readonly Paragraph _paragraph;
/// <inheritdoc/>
public Justify? Alignment
{
get => _paragraph.Alignment;
set => _paragraph.Alignment = value;
}
/// <inheritdoc/>
public Overflow? Overflow
{
get => _paragraph.Overflow;
set => _paragraph.Overflow = value;
}
/// <summary>
/// Gets the character count.
/// </summary>
public int Length => _paragraph.Length;
/// <summary>
/// Gets the number of lines.
/// </summary>
public int Lines => _paragraph.Lines;
/// <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)
{
_paragraph = MarkupParser.Parse(text, style);
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
return ((IRenderable)_paragraph).Measure(context, maxWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
return ((IRenderable)_paragraph).Render(context, maxWidth);
}
/// <summary>
/// Escapes text so that it wont be interpreted as markup.
/// </summary>
/// <param name="text">The text to escape.</param>
/// <returns>A string that is safe to use in markup.</returns>
public static string Escape(string text)
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
return text.EscapeMarkup();
}
/// <summary>
/// Removes markup from the specified string.
/// </summary>
/// <param name="text">The text to remove markup from.</param>
/// <returns>A string that does not have any markup.</returns>
public static string Remove(string text)
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
return text.RemoveMarkup();
}
get => _paragraph.Alignment;
set => _paragraph.Alignment = value;
}
}
/// <inheritdoc/>
public Overflow? Overflow
{
get => _paragraph.Overflow;
set => _paragraph.Overflow = value;
}
/// <summary>
/// Gets the character count.
/// </summary>
public int Length => _paragraph.Length;
/// <summary>
/// Gets the number of lines.
/// </summary>
public int Lines => _paragraph.Lines;
/// <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)
{
_paragraph = MarkupParser.Parse(text, style);
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
return ((IRenderable)_paragraph).Measure(context, maxWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
return ((IRenderable)_paragraph).Render(context, maxWidth);
}
/// <summary>
/// Escapes text so that it wont be interpreted as markup.
/// </summary>
/// <param name="text">The text to escape.</param>
/// <returns>A string that is safe to use in markup.</returns>
public static string Escape(string text)
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
return text.EscapeMarkup();
}
/// <summary>
/// Removes markup from the specified string.
/// </summary>
/// <param name="text">The text to remove markup from.</param>
/// <returns>A string that does not have any markup.</returns>
public static string Remove(string text)
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
return text.RemoveMarkup();
}
}

View File

@ -1,110 +1,109 @@
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents padding around a <see cref="IRenderable"/> object.
/// </summary>
public sealed class Padder : Renderable, IPaddable, IExpandable
{
private readonly IRenderable _child;
/// <inheritdoc/>
public Padding? Padding { get; set; } = new Padding(1, 1, 1, 1);
/// <summary>
/// Represents padding around a <see cref="IRenderable"/> object.
/// Gets or sets a value indicating whether or not the padding should
/// fit the available space. If <c>false</c>, the padding width will be
/// auto calculated. Defaults to <c>false</c>.
/// </summary>
public sealed class Padder : Renderable, IPaddable, IExpandable
public bool Expand { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Padder"/> class.
/// </summary>
/// <param name="child">The thing to pad.</param>
/// <param name="padding">The padding. Defaults to <c>1,1,1,1</c> if null.</param>
public Padder(IRenderable child, Padding? padding = null)
{
private readonly IRenderable _child;
/// <inheritdoc/>
public Padding? Padding { get; set; } = new Padding(1, 1, 1, 1);
/// <summary>
/// Gets or sets a value indicating whether or not the padding should
/// fit the available space. If <c>false</c>, the padding width will be
/// auto calculated. Defaults to <c>false</c>.
/// </summary>
public bool Expand { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Padder"/> class.
/// </summary>
/// <param name="child">The thing to pad.</param>
/// <param name="padding">The padding. Defaults to <c>1,1,1,1</c> if null.</param>
public Padder(IRenderable child, Padding? padding = null)
{
_child = child;
Padding = padding ?? Padding;
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var paddingWidth = Padding?.GetWidth() ?? 0;
var measurement = _child.Measure(context, maxWidth - paddingWidth);
return new Measurement(
measurement.Min + paddingWidth,
measurement.Max + paddingWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var paddingWidth = Padding?.GetWidth() ?? 0;
var childWidth = maxWidth - paddingWidth;
if (!Expand)
{
var measurement = _child.Measure(context, maxWidth - paddingWidth);
childWidth = measurement.Max;
}
var width = childWidth + paddingWidth;
var result = new List<Segment>();
if (width > maxWidth)
{
width = maxWidth;
}
// Top padding
for (var i = 0; i < Padding.GetTopSafe(); i++)
{
result.Add(Segment.Padding(width));
result.Add(Segment.LineBreak);
}
var child = _child.Render(context, maxWidth - paddingWidth);
foreach (var line in Segment.SplitLines(child))
{
// Left padding
if (Padding.GetLeftSafe() != 0)
{
result.Add(Segment.Padding(Padding.GetLeftSafe()));
}
result.AddRange(line);
// Right padding
if (Padding.GetRightSafe() != 0)
{
result.Add(Segment.Padding(Padding.GetRightSafe()));
}
// Missing space on right side?
var lineWidth = line.CellCount();
var diff = width - lineWidth - Padding.GetLeftSafe() - Padding.GetRightSafe();
if (diff > 0)
{
result.Add(Segment.Padding(diff));
}
result.Add(Segment.LineBreak);
}
// Bottom padding
for (var i = 0; i < Padding.GetBottomSafe(); i++)
{
result.Add(Segment.Padding(width));
result.Add(Segment.LineBreak);
}
return result;
}
_child = child;
Padding = padding ?? Padding;
}
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var paddingWidth = Padding?.GetWidth() ?? 0;
var measurement = _child.Measure(context, maxWidth - paddingWidth);
return new Measurement(
measurement.Min + paddingWidth,
measurement.Max + paddingWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var paddingWidth = Padding?.GetWidth() ?? 0;
var childWidth = maxWidth - paddingWidth;
if (!Expand)
{
var measurement = _child.Measure(context, maxWidth - paddingWidth);
childWidth = measurement.Max;
}
var width = childWidth + paddingWidth;
var result = new List<Segment>();
if (width > maxWidth)
{
width = maxWidth;
}
// Top padding
for (var i = 0; i < Padding.GetTopSafe(); i++)
{
result.Add(Segment.Padding(width));
result.Add(Segment.LineBreak);
}
var child = _child.Render(context, maxWidth - paddingWidth);
foreach (var line in Segment.SplitLines(child))
{
// Left padding
if (Padding.GetLeftSafe() != 0)
{
result.Add(Segment.Padding(Padding.GetLeftSafe()));
}
result.AddRange(line);
// Right padding
if (Padding.GetRightSafe() != 0)
{
result.Add(Segment.Padding(Padding.GetRightSafe()));
}
// Missing space on right side?
var lineWidth = line.CellCount();
var diff = width - lineWidth - Padding.GetLeftSafe() - Padding.GetRightSafe();
if (diff > 0)
{
result.Add(Segment.Padding(diff));
}
result.Add(Segment.LineBreak);
}
// Bottom padding
for (var i = 0; i < Padding.GetBottomSafe(); i++)
{
result.Add(Segment.Padding(width));
result.Add(Segment.LineBreak);
}
return result;
}
}

View File

@ -3,200 +3,199 @@ using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// A renderable panel.
/// </summary>
public sealed class Panel : Renderable, IHasBoxBorder, IHasBorder, IExpandable, IPaddable
{
private const int EdgeWidth = 2;
private readonly IRenderable _child;
/// <inheritdoc/>
public BoxBorder Border { get; set; } = BoxBorder.Square;
/// <inheritdoc/>
public bool UseSafeBorder { get; set; } = true;
/// <inheritdoc/>
public Style? BorderStyle { get; set; }
/// <summary>
/// A renderable panel.
/// 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 sealed class Panel : Renderable, IHasBoxBorder, IHasBorder, IExpandable, IPaddable
public bool Expand { get; set; }
/// <summary>
/// Gets or sets the padding.
/// </summary>
public Padding? Padding { get; set; } = new Padding(1, 0, 1, 0);
/// <summary>
/// Gets or sets the header.
/// </summary>
public PanelHeader? Header { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not the panel is inlined.
/// </summary>
internal bool Inline { get; set; }
/// <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))
{
private const int EdgeWidth = 2;
}
private readonly IRenderable _child;
/// <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 ArgumentNullException(nameof(content));
}
/// <inheritdoc/>
public BoxBorder Border { get; set; } = BoxBorder.Square;
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var child = new Padder(_child, Padding);
var childWidth = ((IRenderable)child).Measure(context, maxWidth);
return new Measurement(
childWidth.Min + EdgeWidth,
childWidth.Max + EdgeWidth);
}
/// <inheritdoc/>
public bool UseSafeBorder { get; set; } = true;
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var edgeWidth = EdgeWidth;
/// <inheritdoc/>
public Style? BorderStyle { get; set; }
var border = BoxExtensions.GetSafeBorder(Border, !context.Unicode && UseSafeBorder);
var borderStyle = BorderStyle ?? Style.Plain;
/// <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, 0, 1, 0);
/// <summary>
/// Gets or sets the header.
/// </summary>
public PanelHeader? Header { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not the panel is inlined.
/// </summary>
internal bool Inline { get; set; }
/// <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))
var showBorder = true;
if (border is NoBoxBorder)
{
showBorder = false;
edgeWidth = 0;
}
/// <summary>
/// Initializes a new instance of the <see cref="Panel"/> class.
/// </summary>
/// <param name="content">The panel content.</param>
public Panel(IRenderable content)
var child = new Padder(_child, Padding);
var childWidth = maxWidth - edgeWidth;
if (!Expand)
{
_child = content ?? throw new ArgumentNullException(nameof(content));
var measurement = ((IRenderable)child).Measure(context, maxWidth - edgeWidth);
childWidth = measurement.Max;
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
var panelWidth = childWidth + edgeWidth;
panelWidth = Math.Min(panelWidth, maxWidth);
childWidth = panelWidth - edgeWidth;
var result = new List<Segment>();
if (showBorder)
{
var child = new Padder(_child, Padding);
var childWidth = ((IRenderable)child).Measure(context, maxWidth);
return new Measurement(
childWidth.Min + EdgeWidth,
childWidth.Max + EdgeWidth);
// Panel top
AddTopBorder(result, context, border, borderStyle, panelWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
// Split the child segments into lines.
var childSegments = ((IRenderable)child).Render(context, childWidth);
foreach (var (_, _, last, line) in Segment.SplitLines(childSegments, childWidth).Enumerate())
{
var edgeWidth = EdgeWidth;
var border = BoxExtensions.GetSafeBorder(Border, !context.Unicode && UseSafeBorder);
var borderStyle = BorderStyle ?? Style.Plain;
var showBorder = true;
if (border is NoBoxBorder)
if (line.Count == 1 && line[0].IsWhiteSpace)
{
showBorder = false;
edgeWidth = 0;
// NOTE: This check might impact other things.
// Hopefully not, but there is a chance.
continue;
}
var child = new Padder(_child, Padding);
var childWidth = maxWidth - edgeWidth;
if (!Expand)
{
var measurement = ((IRenderable)child).Measure(context, maxWidth - edgeWidth);
childWidth = measurement.Max;
}
var panelWidth = childWidth + edgeWidth;
panelWidth = Math.Min(panelWidth, maxWidth);
childWidth = panelWidth - edgeWidth;
var result = new List<Segment>();
if (showBorder)
{
// Panel top
AddTopBorder(result, context, border, borderStyle, panelWidth);
result.Add(new Segment(border.GetPart(BoxBorderPart.Left), borderStyle));
}
// Split the child segments into lines.
var childSegments = ((IRenderable)child).Render(context, childWidth);
foreach (var (_, _, last, line) in Segment.SplitLines(childSegments, childWidth).Enumerate())
var content = new List<Segment>();
content.AddRange(line);
// Do we need to pad the panel?
var length = line.Sum(segment => segment.CellCount());
if (length < childWidth)
{
if (line.Count == 1 && line[0].IsWhiteSpace)
{
// NOTE: This check might impact other things.
// Hopefully not, but there is a chance.
continue;
}
if (showBorder)
{
result.Add(new Segment(border.GetPart(BoxBorderPart.Left), borderStyle));
}
var content = new List<Segment>();
content.AddRange(line);
// Do we need to pad the panel?
var length = line.Sum(segment => segment.CellCount());
if (length < childWidth)
{
var diff = childWidth - length;
content.Add(Segment.Padding(diff));
}
result.AddRange(content);
if (showBorder)
{
result.Add(new Segment(border.GetPart(BoxBorderPart.Right), borderStyle));
}
// Don't emit a line break if this is the last
// line, we're not showing the border, and we're
// not rendering this inline.
var emitLinebreak = !(last && !showBorder && !Inline);
if (!emitLinebreak)
{
continue;
}
result.Add(Segment.LineBreak);
var diff = childWidth - length;
content.Add(Segment.Padding(diff));
}
// Panel bottom
result.AddRange(content);
if (showBorder)
{
result.Add(new Segment(border.GetPart(BoxBorderPart.BottomLeft), borderStyle));
result.Add(new Segment(border.GetPart(BoxBorderPart.Bottom).Repeat(panelWidth - EdgeWidth), borderStyle));
result.Add(new Segment(border.GetPart(BoxBorderPart.BottomRight), borderStyle));
result.Add(new Segment(border.GetPart(BoxBorderPart.Right), borderStyle));
}
// TODO: Need a better name for this?
// If we're rendering this as part of an inline parent renderable,
// such as columns, we should not emit the last line break.
if (!Inline)
// Don't emit a line break if this is the last
// line, we're not showing the border, and we're
// not rendering this inline.
var emitLinebreak = !(last && !showBorder && !Inline);
if (!emitLinebreak)
{
result.Add(Segment.LineBreak);
continue;
}
return result;
}
private void AddTopBorder(
List<Segment> result, RenderContext context, BoxBorder border,
Style borderStyle, int panelWidth)
{
var rule = new Rule
{
Style = borderStyle,
Border = border,
TitlePadding = 1,
TitleSpacing = 0,
Title = Header?.Text,
Alignment = Header?.Alignment ?? Justify.Left,
};
// Top left border
result.Add(new Segment(border.GetPart(BoxBorderPart.TopLeft), borderStyle));
// Top border (and header text if specified)
result.AddRange(((IRenderable)rule).Render(context, panelWidth - 2).Where(x => !x.IsLineBreak));
// Top right border
result.Add(new Segment(border.GetPart(BoxBorderPart.TopRight), borderStyle));
result.Add(Segment.LineBreak);
}
// Panel bottom
if (showBorder)
{
result.Add(new Segment(border.GetPart(BoxBorderPart.BottomLeft), borderStyle));
result.Add(new Segment(border.GetPart(BoxBorderPart.Bottom).Repeat(panelWidth - EdgeWidth), borderStyle));
result.Add(new Segment(border.GetPart(BoxBorderPart.BottomRight), borderStyle));
}
// TODO: Need a better name for this?
// If we're rendering this as part of an inline parent renderable,
// such as columns, we should not emit the last line break.
if (!Inline)
{
result.Add(Segment.LineBreak);
}
return result;
}
}
private void AddTopBorder(
List<Segment> result, RenderContext context, BoxBorder border,
Style borderStyle, int panelWidth)
{
var rule = new Rule
{
Style = borderStyle,
Border = border,
TitlePadding = 1,
TitleSpacing = 0,
Title = Header?.Text,
Alignment = Header?.Alignment ?? Justify.Left,
};
// Top left border
result.Add(new Segment(border.GetPart(BoxBorderPart.TopLeft), borderStyle));
// Top border (and header text if specified)
result.AddRange(((IRenderable)rule).Render(context, panelWidth - 2).Where(x => !x.IsLineBreak));
// Top right border
result.Add(new Segment(border.GetPart(BoxBorderPart.TopRight), borderStyle));
result.Add(Segment.LineBreak);
}
}

View File

@ -1,67 +1,66 @@
using System;
using System.ComponentModel;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents a panel header.
/// </summary>
public sealed class PanelHeader : IAlignable
{
/// <summary>
/// Represents a panel header.
/// Gets the panel header text.
/// </summary>
public sealed class PanelHeader : IAlignable
public string Text { get; }
/// <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="alignment">The panel header alignment.</param>
public PanelHeader(string text, Justify? alignment = null)
{
/// <summary>
/// Gets the panel header text.
/// </summary>
public string Text { get; }
/// <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="alignment">The panel header alignment.</param>
public PanelHeader(string text, Justify? alignment = null)
{
Text = text ?? throw new ArgumentNullException(nameof(text));
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>
[Obsolete("Use markup instead.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public PanelHeader SetStyle(Style? style)
{
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>
[Obsolete("Use markup instead.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public PanelHeader SetStyle(string 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;
}
Text = text ?? throw new ArgumentNullException(nameof(text));
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>
[Obsolete("Use markup instead.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public PanelHeader SetStyle(Style? style)
{
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>
[Obsolete("Use markup instead.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public PanelHeader SetStyle(string 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

@ -4,296 +4,295 @@ using System.Diagnostics;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// A paragraph of text where different parts
/// of the paragraph can have individual styling.
/// </summary>
[DebuggerDisplay("{_text,nq}")]
public sealed class Paragraph : Renderable, IAlignable, IOverflowable
{
private readonly List<SegmentLine> _lines;
/// <summary>
/// A paragraph of text where different parts
/// of the paragraph can have individual styling.
/// Gets or sets the alignment of the whole paragraph.
/// </summary>
[DebuggerDisplay("{_text,nq}")]
public sealed class Paragraph : Renderable, IAlignable, IOverflowable
public Justify? Alignment { get; set; }
/// <summary>
/// Gets or sets the text overflow strategy.
/// </summary>
public Overflow? Overflow { get; set; }
/// <summary>
/// Gets the character count of the paragraph.
/// </summary>
public int Length => _lines.Sum(line => line.Length) + Math.Max(0, Lines - 1);
/// <summary>
/// Gets the number of lines in the paragraph.
/// </summary>
public int Lines => _lines.Count;
/// <summary>
/// Initializes a new instance of the <see cref="Paragraph"/> class.
/// </summary>
public Paragraph()
{
private readonly List<SegmentLine> _lines;
_lines = new List<SegmentLine>();
}
/// <summary>
/// Gets or sets the alignment of the whole paragraph.
/// </summary>
public Justify? Alignment { get; set; }
/// <summary>
/// Gets or sets the text overflow strategy.
/// </summary>
public Overflow? Overflow { get; set; }
/// <summary>
/// Gets the character count of the paragraph.
/// </summary>
public int Length => _lines.Sum(line => line.Length) + Math.Max(0, Lines - 1);
/// <summary>
/// Gets the number of lines in the paragraph.
/// </summary>
public int Lines => _lines.Count;
/// <summary>
/// Initializes a new instance of the <see cref="Paragraph"/> class.
/// </summary>
public Paragraph()
/// <summary>
/// Initializes a new instance of the <see cref="Paragraph"/> class.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="style">The style of the text or <see cref="Style.Plain"/> if <see langword="null"/>.</param>
public Paragraph(string text, Style? style = null)
: this()
{
if (text is null)
{
_lines = new List<SegmentLine>();
throw new ArgumentNullException(nameof(text));
}
/// <summary>
/// Initializes a new instance of the <see cref="Paragraph"/> class.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="style">The style of the text or <see cref="Style.Plain"/> if <see langword="null"/>.</param>
public Paragraph(string text, Style? style = null)
: this()
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
Append(text, style);
}
Append(text, style);
/// <summary>
/// Appends some text to this paragraph.
/// </summary>
/// <param name="text">The text to append.</param>
/// <param name="style">The style of the appended text or <see cref="Style.Plain"/> if <see langword="null"/>.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Paragraph Append(string text, Style? style = null)
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
/// <summary>
/// Appends some text to this paragraph.
/// </summary>
/// <param name="text">The text to append.</param>
/// <param name="style">The style of the appended text or <see cref="Style.Plain"/> if <see langword="null"/>.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Paragraph Append(string text, Style? style = null)
foreach (var (_, first, last, part) in text.SplitLines().Enumerate())
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
var current = part;
foreach (var (_, first, last, part) in text.SplitLines().Enumerate())
if (first)
{
var current = part;
if (first)
var line = _lines.LastOrDefault();
if (line == null)
{
var line = _lines.LastOrDefault();
if (line == null)
{
_lines.Add(new SegmentLine());
line = _lines.Last();
}
_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));
}
}
if (string.IsNullOrEmpty(current))
{
line.Add(Segment.Empty);
}
else
{
var line = new SegmentLine();
if (string.IsNullOrEmpty(current))
foreach (var span in current.SplitWords())
{
line.Add(Segment.Empty);
line.Add(new Segment(span, style ?? Style.Plain));
}
else
{
foreach (var span in current.SplitWords())
{
line.Add(new Segment(span, style ?? Style.Plain));
}
}
_lines.Add(line);
}
}
return this;
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
if (_lines.Count == 0)
else
{
return new Measurement(0, 0);
}
var line = new SegmentLine();
var min = _lines.Max(line => line.Max(segment => segment.CellCount()));
var max = _lines.Max(x => x.CellCount());
return new Measurement(min, Math.Min(max, maxWidth));
}
/// <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 = context.SingleLine
? new List<SegmentLine>(_lines)
: SplitLines(maxWidth);
// Justify lines
var justification = context.Justification ?? Alignment ?? Justify.Left;
if (justification != Justify.Left)
{
foreach (var line in lines)
if (string.IsNullOrEmpty(current))
{
Aligner.Align(context, line, justification, maxWidth);
}
}
if (context.SingleLine)
{
// Return the first line
return lines[0].Where(segment => !segment.IsLineBreak);
}
return new SegmentLineEnumerator(lines);
}
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(int maxWidth)
{
if (maxWidth <= 0)
{
// Nothing fits, so return an empty line.
return new List<SegmentLine>();
}
if (_lines.Max(x => x.CellCount()) <= maxWidth)
{
return Clone();
}
var lines = new List<SegmentLine>();
var line = new SegmentLine();
var newLine = true;
using var iterator = new SegmentLineIterator(_lines);
var queue = new Queue<Segment>();
while (true)
{
var current = (Segment?)null;
if (queue.Count == 0)
{
if (!iterator.MoveNext())
{
break;
}
current = iterator.Current;
line.Add(Segment.Empty);
}
else
{
current = queue.Dequeue();
}
if (current == null)
{
throw new InvalidOperationException("Iterator returned empty segment.");
}
newLine = false;
if (current.IsLineBreak)
{
lines.Add(line);
line = new SegmentLine();
newLine = true;
continue;
}
var length = current.CellCount();
if (length > maxWidth)
{
// The current segment is longer than the width of the console,
// so we will need to crop it up, into new segments.
var segments = Segment.SplitOverflow(current, Overflow, maxWidth);
if (segments.Count > 0)
foreach (var span in current.SplitWords())
{
if (line.CellCount() + segments[0].CellCount() > maxWidth)
{
lines.Add(line);
line = new SegmentLine();
newLine = true;
segments.ForEach(s => queue.Enqueue(s));
continue;
}
else
{
// Add the segment and push the rest of them to the queue.
line.Add(segments[0]);
segments.Skip(1).ForEach(s => queue.Enqueue(s));
continue;
}
line.Add(new Segment(span, style ?? Style.Plain));
}
}
else
_lines.Add(line);
}
}
return this;
}
/// <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.CellCount()));
var max = _lines.Max(x => x.CellCount());
return new Measurement(min, Math.Min(max, maxWidth));
}
/// <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 = context.SingleLine
? new List<SegmentLine>(_lines)
: SplitLines(maxWidth);
// Justify lines
var justification = context.Justification ?? Alignment ?? Justify.Left;
if (justification != Justify.Left)
{
foreach (var line in lines)
{
Aligner.Align(context, line, justification, maxWidth);
}
}
if (context.SingleLine)
{
// Return the first line
return lines[0].Where(segment => !segment.IsLineBreak);
}
return new SegmentLineEnumerator(lines);
}
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(int maxWidth)
{
if (maxWidth <= 0)
{
// Nothing fits, so return an empty line.
return new List<SegmentLine>();
}
if (_lines.Max(x => x.CellCount()) <= maxWidth)
{
return Clone();
}
var lines = new List<SegmentLine>();
var line = new SegmentLine();
var newLine = true;
using var iterator = new SegmentLineIterator(_lines);
var queue = new Queue<Segment>();
while (true)
{
var current = (Segment?)null;
if (queue.Count == 0)
{
if (!iterator.MoveNext())
{
if (line.CellCount() + length > maxWidth)
break;
}
current = iterator.Current;
}
else
{
current = queue.Dequeue();
}
if (current == null)
{
throw new InvalidOperationException("Iterator returned empty segment.");
}
newLine = false;
if (current.IsLineBreak)
{
lines.Add(line);
line = new SegmentLine();
newLine = true;
continue;
}
var length = current.CellCount();
if (length > maxWidth)
{
// The current segment is longer than the width of the console,
// so we will need to crop it up, into new segments.
var segments = Segment.SplitOverflow(current, Overflow, maxWidth);
if (segments.Count > 0)
{
if (line.CellCount() + segments[0].CellCount() > maxWidth)
{
line.Add(Segment.Empty);
lines.Add(line);
line = new SegmentLine();
newLine = true;
segments.ForEach(s => queue.Enqueue(s));
continue;
}
else
{
// Add the segment and push the rest of them to the queue.
line.Add(segments[0]);
segments.Skip(1).ForEach(s => queue.Enqueue(s));
continue;
}
}
if (newLine && current.IsWhiteSpace)
{
continue;
}
newLine = false;
line.Add(current);
}
// Flush remaining.
if (line.Count > 0)
else
{
lines.Add(line);
if (line.CellCount() + length > maxWidth)
{
line.Add(Segment.Empty);
lines.Add(line);
line = new SegmentLine();
newLine = true;
}
}
return lines;
if (newLine && current.IsWhiteSpace)
{
continue;
}
newLine = false;
line.Add(current);
}
// Flush remaining.
if (line.Count > 0)
{
lines.Add(line);
}
return lines;
}
}
}

View File

@ -4,46 +4,119 @@ using System.Globalization;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class ProgressBar : Renderable, IHasCulture
{
internal sealed class ProgressBar : Renderable, IHasCulture
private const int PULSESIZE = 20;
private const int PULSESPEED = 15;
public double Value { get; set; }
public double MaxValue { get; set; } = 100;
public int? Width { get; set; }
public bool ShowRemaining { get; set; } = true;
public char UnicodeBar { get; set; } = '━';
public char AsciiBar { get; set; } = '-';
public bool ShowValue { get; set; }
public bool IsIndeterminate { get; set; }
public CultureInfo? Culture { get; set; }
public Style CompletedStyle { get; set; } = new Style(foreground: Color.Yellow);
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);
public Style RemainingStyle { get; set; } = new Style(foreground: Color.Grey);
public Style IndeterminateStyle { get; set; } = DefaultPulseStyle;
internal static Style DefaultPulseStyle { get; } = new Style(foreground: Color.DodgerBlue1, background: Color.Grey23);
protected override Measurement Measure(RenderContext context, int maxWidth)
{
private const int PULSESIZE = 20;
private const int PULSESPEED = 15;
var width = Math.Min(Width ?? maxWidth, maxWidth);
return new Measurement(4, width);
}
public double Value { get; set; }
public double MaxValue { get; set; } = 100;
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
var completedBarCount = Math.Min(MaxValue, Math.Max(0, Value));
var isCompleted = completedBarCount >= MaxValue;
public int? Width { get; set; }
public bool ShowRemaining { get; set; } = true;
public char UnicodeBar { get; set; } = '━';
public char AsciiBar { get; set; } = '-';
public bool ShowValue { get; set; }
public bool IsIndeterminate { get; set; }
public CultureInfo? Culture { get; set; }
public Style CompletedStyle { get; set; } = new Style(foreground: Color.Yellow);
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);
public Style RemainingStyle { get; set; } = new Style(foreground: Color.Grey);
public Style IndeterminateStyle { get; set; } = DefaultPulseStyle;
internal static Style DefaultPulseStyle { get; } = new Style(foreground: Color.DodgerBlue1, background: Color.Grey23);
protected override Measurement Measure(RenderContext context, int maxWidth)
if (IsIndeterminate && !isCompleted)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
return new Measurement(4, width);
foreach (var segment in RenderIndeterminate(context, width))
{
yield return segment;
}
yield break;
}
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
var completedBarCount = Math.Min(MaxValue, Math.Max(0, Value));
var isCompleted = completedBarCount >= MaxValue;
var bar = !context.Unicode ? AsciiBar : UnicodeBar;
var style = isCompleted ? FinishedStyle : CompletedStyle;
var barCount = Math.Max(0, (int)(width * (completedBarCount / MaxValue)));
if (IsIndeterminate && !isCompleted)
// Show value?
var value = completedBarCount.ToString(Culture ?? CultureInfo.InvariantCulture);
if (ShowValue)
{
barCount = barCount - value.Length - 1;
barCount = Math.Max(0, barCount);
}
if (barCount < 0)
{
yield break;
}
yield return new Segment(new string(bar, barCount), style);
if (ShowValue)
{
yield return barCount == 0
? new Segment(value, style)
: new Segment(" " + value, style);
}
// More space available?
if (barCount < width)
{
var diff = width - barCount;
if (ShowValue)
{
foreach (var segment in RenderIndeterminate(context, width))
diff = diff - value.Length - 1;
if (diff <= 0)
{
yield break;
}
}
var legacy = context.ColorSystem == ColorSystem.NoColors || context.ColorSystem == ColorSystem.Legacy;
var remainingToken = ShowRemaining && !legacy ? bar : ' ';
yield return new Segment(new string(remainingToken, diff), RemainingStyle);
}
}
private IEnumerable<Segment> RenderIndeterminate(RenderContext context, int width)
{
var bar = context.Unicode ? UnicodeBar.ToString() : AsciiBar.ToString();
var style = IndeterminateStyle ?? DefaultPulseStyle;
IEnumerable<Segment> GetPulseSegments()
{
// For 1-bit and 3-bit colors, fall back to
// a simpler versions with only two colors.
if (context.ColorSystem == ColorSystem.NoColors ||
context.ColorSystem == ColorSystem.Legacy)
{
// First half of the pulse
var segments = Enumerable.Repeat(new Segment(bar, new Style(style.Foreground)), PULSESIZE / 2);
// Second half of the pulse
var legacy = context.ColorSystem == ColorSystem.NoColors || context.ColorSystem == ColorSystem.Legacy;
var bar2 = legacy ? " " : bar;
segments = segments.Concat(Enumerable.Repeat(new Segment(bar2, new Style(style.Background)), PULSESIZE - (PULSESIZE / 2)));
foreach (var segment in segments)
{
yield return segment;
}
@ -51,98 +124,24 @@ namespace Spectre.Console
yield break;
}
var bar = !context.Unicode ? AsciiBar : UnicodeBar;
var style = isCompleted ? FinishedStyle : CompletedStyle;
var barCount = Math.Max(0, (int)(width * (completedBarCount / MaxValue)));
// Show value?
var value = completedBarCount.ToString(Culture ?? CultureInfo.InvariantCulture);
if (ShowValue)
for (var index = 0; index < PULSESIZE; index++)
{
barCount = barCount - value.Length - 1;
barCount = Math.Max(0, barCount);
}
var position = index / (float)PULSESIZE;
var fade = 0.5f + ((float)Math.Cos(position * Math.PI * 2) / 2.0f);
var color = style.Foreground.Blend(style.Background, fade);
if (barCount < 0)
{
yield break;
}
yield return new Segment(new string(bar, barCount), style);
if (ShowValue)
{
yield return barCount == 0
? new Segment(value, style)
: new Segment(" " + value, style);
}
// More space available?
if (barCount < width)
{
var diff = width - barCount;
if (ShowValue)
{
diff = diff - value.Length - 1;
if (diff <= 0)
{
yield break;
}
}
var legacy = context.ColorSystem == ColorSystem.NoColors || context.ColorSystem == ColorSystem.Legacy;
var remainingToken = ShowRemaining && !legacy ? bar : ' ';
yield return new Segment(new string(remainingToken, diff), RemainingStyle);
yield return new Segment(bar, new Style(foreground: color));
}
}
private IEnumerable<Segment> RenderIndeterminate(RenderContext context, int width)
{
var bar = context.Unicode ? UnicodeBar.ToString() : AsciiBar.ToString();
var style = IndeterminateStyle ?? DefaultPulseStyle;
// Get the pulse segments
var pulseSegments = GetPulseSegments();
pulseSegments = pulseSegments.Repeat((width / PULSESIZE) + 2);
IEnumerable<Segment> GetPulseSegments()
{
// For 1-bit and 3-bit colors, fall back to
// a simpler versions with only two colors.
if (context.ColorSystem == ColorSystem.NoColors ||
context.ColorSystem == ColorSystem.Legacy)
{
// First half of the pulse
var segments = Enumerable.Repeat(new Segment(bar, new Style(style.Foreground)), PULSESIZE / 2);
// Repeat the pulse segments
var currentTime = (DateTime.Now - DateTime.Today).TotalSeconds;
var offset = (int)(currentTime * PULSESPEED) % PULSESIZE;
// Second half of the pulse
var legacy = context.ColorSystem == ColorSystem.NoColors || context.ColorSystem == ColorSystem.Legacy;
var bar2 = legacy ? " " : bar;
segments = segments.Concat(Enumerable.Repeat(new Segment(bar2, new Style(style.Background)), PULSESIZE - (PULSESIZE / 2)));
foreach (var segment in segments)
{
yield return segment;
}
yield break;
}
for (var index = 0; index < PULSESIZE; index++)
{
var position = index / (float)PULSESIZE;
var fade = 0.5f + ((float)Math.Cos(position * Math.PI * 2) / 2.0f);
var color = style.Foreground.Blend(style.Background, fade);
yield return new Segment(bar, new Style(foreground: color));
}
}
// Get the pulse segments
var pulseSegments = GetPulseSegments();
pulseSegments = pulseSegments.Repeat((width / PULSESIZE) + 2);
// Repeat the pulse segments
var currentTime = (DateTime.Now - DateTime.Today).TotalSeconds;
var offset = (int)(currentTime * PULSESPEED) % PULSESIZE;
return pulseSegments.Skip(offset).Take(width);
}
return pulseSegments.Skip(offset).Take(width);
}
}
}

View File

@ -3,75 +3,74 @@ using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Renders things in rows.
/// </summary>
public sealed class Rows : Renderable, IExpandable
{
private readonly List<IRenderable> _children;
/// <inheritdoc/>
public bool Expand { get; set; }
/// <summary>
/// Renders things in rows.
/// Initializes a new instance of the <see cref="Rows"/> class.
/// </summary>
public sealed class Rows : Renderable, IExpandable
/// <param name="items">The items to render as rows.</param>
public Rows(params IRenderable[] items)
: this((IEnumerable<IRenderable>)items)
{
private readonly List<IRenderable> _children;
}
/// <inheritdoc/>
public bool Expand { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Rows"/> class.
/// </summary>
/// <param name="children">The items to render as rows.</param>
public Rows(IEnumerable<IRenderable> children)
{
_children = new List<IRenderable>(children ?? throw new ArgumentNullException(nameof(children)));
}
/// <summary>
/// Initializes a new instance of the <see cref="Rows"/> class.
/// </summary>
/// <param name="items">The items to render as rows.</param>
public Rows(params IRenderable[] items)
: this((IEnumerable<IRenderable>)items)
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
if (Expand)
{
return new Measurement(maxWidth, maxWidth);
}
/// <summary>
/// Initializes a new instance of the <see cref="Rows"/> class.
/// </summary>
/// <param name="children">The items to render as rows.</param>
public Rows(IEnumerable<IRenderable> children)
else
{
_children = new List<IRenderable>(children ?? throw new ArgumentNullException(nameof(children)));
var measurements = _children.Select(c => c.Measure(context, maxWidth));
return new Measurement(
measurements.Min(c => c.Min),
measurements.Min(c => c.Max));
}
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var result = new List<Segment>();
foreach (var child in _children)
{
if (Expand)
var segments = child.Render(context, maxWidth);
foreach (var (_, _, last, segment) in segments.Enumerate())
{
return new Measurement(maxWidth, maxWidth);
}
else
{
var measurements = _children.Select(c => c.Measure(context, maxWidth));
return new Measurement(
measurements.Min(c => c.Min),
measurements.Min(c => c.Max));
}
}
result.Add(segment);
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var result = new List<Segment>();
foreach (var child in _children)
{
var segments = child.Render(context, maxWidth);
foreach (var (_, _, last, segment) in segments.Enumerate())
if (last)
{
result.Add(segment);
if (last)
if (!segment.IsLineBreak)
{
if (!segment.IsLineBreak)
{
result.Add(Segment.LineBreak);
}
result.Add(Segment.LineBreak);
}
}
}
return result;
}
return result;
}
}
}

View File

@ -3,141 +3,140 @@ using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// A renderable horizontal rule.
/// </summary>
public sealed class Rule : Renderable, IAlignable, IHasBoxBorder
{
/// <summary>
/// A renderable horizontal rule.
/// Gets or sets the rule title markup text.
/// </summary>
public sealed class Rule : Renderable, IAlignable, IHasBoxBorder
public string? Title { get; set; }
/// <summary>
/// Gets or sets the rule style.
/// </summary>
public Style? Style { get; set; }
/// <summary>
/// Gets or sets the rule's title alignment.
/// </summary>
public Justify? Alignment { get; set; }
/// <inheritdoc/>
public BoxBorder Border { get; set; } = BoxBorder.Square;
internal int TitlePadding { get; set; } = 2;
internal int TitleSpacing { get; set; } = 1;
/// <summary>
/// Initializes a new instance of the <see cref="Rule"/> class.
/// </summary>
public Rule()
{
/// <summary>
/// Gets or sets the rule title markup text.
/// </summary>
public string? Title { get; set; }
}
/// <summary>
/// Gets or sets the rule style.
/// </summary>
public Style? Style { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Rule"/> class.
/// </summary>
/// <param name="title">The rule title markup text.</param>
public Rule(string title)
{
Title = title ?? throw new ArgumentNullException(nameof(title));
}
/// <summary>
/// Gets or sets the rule's title alignment.
/// </summary>
public Justify? Alignment { get; set; }
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var extraLength = (2 * TitlePadding) + (2 * TitleSpacing);
/// <inheritdoc/>
public BoxBorder Border { get; set; } = BoxBorder.Square;
internal int TitlePadding { get; set; } = 2;
internal int TitleSpacing { get; set; } = 1;
/// <summary>
/// Initializes a new instance of the <see cref="Rule"/> class.
/// </summary>
public Rule()
if (Title == null || maxWidth <= extraLength)
{
return GetLineWithoutTitle(context, maxWidth);
}
/// <summary>
/// Initializes a new instance of the <see cref="Rule"/> class.
/// </summary>
/// <param name="title">The rule title markup text.</param>
public Rule(string title)
// Get the title and make sure it fits.
var title = GetTitleSegments(context, Title, maxWidth - extraLength);
if (Segment.CellCount(title) > maxWidth - extraLength)
{
Title = title ?? throw new ArgumentNullException(nameof(title));
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var extraLength = (2 * TitlePadding) + (2 * TitleSpacing);
if (Title == null || maxWidth <= extraLength)
// Truncate the title
title = Segment.TruncateWithEllipsis(title, maxWidth - extraLength);
if (!title.Any())
{
// We couldn't fit the title at all.
return GetLineWithoutTitle(context, maxWidth);
}
// Get the title and make sure it fits.
var title = GetTitleSegments(context, Title, maxWidth - extraLength);
if (Segment.CellCount(title) > maxWidth - extraLength)
{
// Truncate the title
title = Segment.TruncateWithEllipsis(title, maxWidth - extraLength);
if (!title.Any())
{
// We couldn't fit the title at all.
return GetLineWithoutTitle(context, maxWidth);
}
}
var (left, right) = GetLineSegments(context, maxWidth, title);
var segments = new List<Segment>();
segments.Add(left);
segments.AddRange(title);
segments.Add(right);
segments.Add(Segment.LineBreak);
return segments;
}
private IEnumerable<Segment> GetLineWithoutTitle(RenderContext context, int maxWidth)
{
var border = Border.GetSafeBorder(safe: !context.Unicode);
var text = border.GetPart(BoxBorderPart.Top).Repeat(maxWidth);
var (left, right) = GetLineSegments(context, maxWidth, title);
return new[]
{
new Segment(text, Style ?? Style.Plain),
Segment.LineBreak,
};
}
var segments = new List<Segment>();
segments.Add(left);
segments.AddRange(title);
segments.Add(right);
segments.Add(Segment.LineBreak);
private IEnumerable<Segment> GetTitleSegments(RenderContext context, string title, int width)
{
title = title.NormalizeNewLines().ReplaceExact("\n", " ").Trim();
var markup = new Markup(title, Style);
return ((IRenderable)markup).Render(context.WithSingleLine(), width);
}
private (Segment Left, Segment Right) GetLineSegments(RenderContext context, int width, IEnumerable<Segment> title)
{
var titleLength = Segment.CellCount(title);
var border = Border.GetSafeBorder(safe: !context.Unicode);
var borderPart = border.GetPart(BoxBorderPart.Top);
var alignment = Alignment ?? Justify.Center;
if (alignment == Justify.Left)
{
var left = new Segment(borderPart.Repeat(TitlePadding) + new string(' ', TitleSpacing), Style ?? Style.Plain);
var rightLength = width - titleLength - left.CellCount() - TitleSpacing;
var right = new Segment(new string(' ', TitleSpacing) + borderPart.Repeat(rightLength), Style ?? Style.Plain);
return (left, right);
}
else if (alignment == Justify.Center)
{
var leftLength = ((width - titleLength) / 2) - TitleSpacing;
var left = new Segment(borderPart.Repeat(leftLength) + new string(' ', TitleSpacing), Style ?? Style.Plain);
var rightLength = width - titleLength - left.CellCount() - TitleSpacing;
var right = new Segment(new string(' ', TitleSpacing) + borderPart.Repeat(rightLength), Style ?? Style.Plain);
return (left, right);
}
else if (alignment == Justify.Right)
{
var right = new Segment(new string(' ', TitleSpacing) + borderPart.Repeat(TitlePadding), Style ?? Style.Plain);
var leftLength = width - titleLength - right.CellCount() - TitleSpacing;
var left = new Segment(borderPart.Repeat(leftLength) + new string(' ', TitleSpacing), Style ?? Style.Plain);
return (left, right);
}
throw new NotSupportedException("Unsupported alignment.");
}
return segments;
}
}
private IEnumerable<Segment> GetLineWithoutTitle(RenderContext context, int maxWidth)
{
var border = Border.GetSafeBorder(safe: !context.Unicode);
var text = border.GetPart(BoxBorderPart.Top).Repeat(maxWidth);
return new[]
{
new Segment(text, Style ?? Style.Plain),
Segment.LineBreak,
};
}
private IEnumerable<Segment> GetTitleSegments(RenderContext context, string title, int width)
{
title = title.NormalizeNewLines().ReplaceExact("\n", " ").Trim();
var markup = new Markup(title, Style);
return ((IRenderable)markup).Render(context.WithSingleLine(), width);
}
private (Segment Left, Segment Right) GetLineSegments(RenderContext context, int width, IEnumerable<Segment> title)
{
var titleLength = Segment.CellCount(title);
var border = Border.GetSafeBorder(safe: !context.Unicode);
var borderPart = border.GetPart(BoxBorderPart.Top);
var alignment = Alignment ?? Justify.Center;
if (alignment == Justify.Left)
{
var left = new Segment(borderPart.Repeat(TitlePadding) + new string(' ', TitleSpacing), Style ?? Style.Plain);
var rightLength = width - titleLength - left.CellCount() - TitleSpacing;
var right = new Segment(new string(' ', TitleSpacing) + borderPart.Repeat(rightLength), Style ?? Style.Plain);
return (left, right);
}
else if (alignment == Justify.Center)
{
var leftLength = ((width - titleLength) / 2) - TitleSpacing;
var left = new Segment(borderPart.Repeat(leftLength) + new string(' ', TitleSpacing), Style ?? Style.Plain);
var rightLength = width - titleLength - left.CellCount() - TitleSpacing;
var right = new Segment(new string(' ', TitleSpacing) + borderPart.Repeat(rightLength), Style ?? Style.Plain);
return (left, right);
}
else if (alignment == Justify.Right)
{
var right = new Segment(new string(' ', TitleSpacing) + borderPart.Repeat(TitlePadding), Style ?? Style.Plain);
var leftLength = width - titleLength - right.CellCount() - TitleSpacing;
var left = new Segment(borderPart.Repeat(leftLength) + new string(' ', TitleSpacing), Style ?? Style.Plain);
return (left, right);
}
throw new NotSupportedException("Unsupported alignment.");
}
}

View File

@ -3,171 +3,170 @@ using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// A renderable table.
/// </summary>
public sealed class Table : Renderable, IHasTableBorder, IExpandable, IAlignable
{
private readonly List<TableColumn> _columns;
/// <summary>
/// A renderable table.
/// Gets the table columns.
/// </summary>
public sealed class Table : Renderable, IHasTableBorder, IExpandable, IAlignable
public IReadOnlyList<TableColumn> Columns => _columns;
/// <summary>
/// Gets the table rows.
/// </summary>
public TableRowCollection Rows { get; }
/// <inheritdoc/>
public TableBorder Border { get; set; } = TableBorder.Square;
/// <inheritdoc/>
public Style? BorderStyle { get; set; }
/// <inheritdoc/>
public bool UseSafeBorder { get; set; } = true;
/// <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 table footers should be shown.
/// </summary>
public bool ShowFooters { 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 the table title.
/// </summary>
public TableTitle? Title { get; set; }
/// <summary>
/// Gets or sets the table footnote.
/// </summary>
public TableTitle? Caption { get; set; }
/// <inheritdoc/>
public Justify? Alignment { get; set; }
// 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()
{
private readonly List<TableColumn> _columns;
/// <summary>
/// Gets the table columns.
/// </summary>
public IReadOnlyList<TableColumn> Columns => _columns;
/// <summary>
/// Gets the table rows.
/// </summary>
public TableRowCollection Rows { get; }
/// <inheritdoc/>
public TableBorder Border { get; set; } = TableBorder.Square;
/// <inheritdoc/>
public Style? BorderStyle { get; set; }
/// <inheritdoc/>
public bool UseSafeBorder { get; set; } = true;
/// <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 table footers should be shown.
/// </summary>
public bool ShowFooters { 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 the table title.
/// </summary>
public TableTitle? Title { get; set; }
/// <summary>
/// Gets or sets the table footnote.
/// </summary>
public TableTitle? Caption { get; set; }
/// <inheritdoc/>
public Justify? Alignment { get; set; }
// 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 TableRowCollection(this);
}
/// <summary>
/// Adds a column to the table.
/// </summary>
/// <param name="column">The column to add.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Table 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);
return this;
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var measurer = new TableMeasurer(this, context);
// Calculate the total cell width
var totalCellWidth = measurer.CalculateTotalCellWidth(maxWidth);
// Calculate the minimum and maximum table width
var measurements = _columns.Select(column => measurer.MeasureColumn(column, totalCellWidth));
var minTableWidth = measurements.Sum(x => x.Min) + measurer.GetNonColumnWidth();
var maxTableWidth = Width ?? measurements.Sum(x => x.Max) + measurer.GetNonColumnWidth();
return new Measurement(minTableWidth, maxTableWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var measurer = new TableMeasurer(this, context);
// Calculate the column and table width
var totalCellWidth = measurer.CalculateTotalCellWidth(maxWidth);
var columnWidths = measurer.CalculateColumnWidths(totalCellWidth);
var tableWidth = columnWidths.Sum() + measurer.GetNonColumnWidth();
// Get the rows to render
var rows = GetRenderableRows();
// Render the table
return TableRenderer.Render(
new TableRendererContext(this, context, rows, tableWidth, maxWidth),
columnWidths);
}
private List<TableRow> GetRenderableRows()
{
var rows = new List<TableRow>();
// Show headers?
if (ShowHeaders)
{
rows.Add(TableRow.Header(_columns.Select(c => c.Header)));
}
// Add rows
rows.AddRange(Rows);
// Show footers?
if (ShowFooters && _columns.Any(c => c.Footer != null))
{
rows.Add(TableRow.Footer(_columns.Select(c => c.Footer ?? Text.Empty)));
}
return rows;
}
_columns = new List<TableColumn>();
Rows = new TableRowCollection(this);
}
}
/// <summary>
/// Adds a column to the table.
/// </summary>
/// <param name="column">The column to add.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Table 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);
return this;
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var measurer = new TableMeasurer(this, context);
// Calculate the total cell width
var totalCellWidth = measurer.CalculateTotalCellWidth(maxWidth);
// Calculate the minimum and maximum table width
var measurements = _columns.Select(column => measurer.MeasureColumn(column, totalCellWidth));
var minTableWidth = measurements.Sum(x => x.Min) + measurer.GetNonColumnWidth();
var maxTableWidth = Width ?? measurements.Sum(x => x.Max) + measurer.GetNonColumnWidth();
return new Measurement(minTableWidth, maxTableWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var measurer = new TableMeasurer(this, context);
// Calculate the column and table width
var totalCellWidth = measurer.CalculateTotalCellWidth(maxWidth);
var columnWidths = measurer.CalculateColumnWidths(totalCellWidth);
var tableWidth = columnWidths.Sum() + measurer.GetNonColumnWidth();
// Get the rows to render
var rows = GetRenderableRows();
// Render the table
return TableRenderer.Render(
new TableRendererContext(this, context, rows, tableWidth, maxWidth),
columnWidths);
}
private List<TableRow> GetRenderableRows()
{
var rows = new List<TableRow>();
// Show headers?
if (ShowHeaders)
{
rows.Add(TableRow.Header(_columns.Select(c => c.Header)));
}
// Add rows
rows.AddRange(Rows);
// Show footers?
if (ShowFooters && _columns.Any(c => c.Footer != null))
{
rows.Add(TableRow.Footer(_columns.Select(c => c.Footer ?? Text.Empty)));
}
return rows;
}
}

View File

@ -2,21 +2,20 @@ using System;
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
internal abstract class TableAccessor
{
internal abstract class TableAccessor
private readonly Table _table;
public RenderContext Options { get; }
public IReadOnlyList<TableColumn> Columns => _table.Columns;
public virtual IReadOnlyList<TableRow> Rows => _table.Rows;
public bool Expand => _table.Expand || _table.Width != null;
protected TableAccessor(Table table, RenderContext options)
{
private readonly Table _table;
public RenderContext Options { get; }
public IReadOnlyList<TableColumn> Columns => _table.Columns;
public virtual IReadOnlyList<TableRow> Rows => _table.Rows;
public bool Expand => _table.Expand || _table.Width != null;
protected TableAccessor(Table table, RenderContext options)
{
_table = table ?? throw new ArgumentNullException(nameof(table));
Options = options ?? throw new ArgumentNullException(nameof(options));
}
_table = table ?? throw new ArgumentNullException(nameof(table));
Options = options ?? throw new ArgumentNullException(nameof(options));
}
}
}

View File

@ -1,66 +1,65 @@
using System;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents a table column.
/// </summary>
public sealed class TableColumn : IColumn
{
/// <summary>
/// Represents a table column.
/// Gets or sets the column header.
/// </summary>
public sealed class TableColumn : IColumn
public IRenderable Header { get; set; }
/// <summary>
/// Gets or sets the column footer.
/// </summary>
public IRenderable? Footer { get; set; }
/// <summary>
/// Gets or sets the width of the column.
/// If <c>null</c>, the column will adapt to its contents.
/// </summary>
public int? Width { get; set; }
/// <summary>
/// Gets or sets the padding of the column.
/// Vertical padding (top and bottom) is ignored.
/// </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="header">The table column header.</param>
public TableColumn(string header)
: this(new Markup(header).Overflow(Overflow.Ellipsis))
{
/// <summary>
/// Gets or sets the column header.
/// </summary>
public IRenderable Header { get; set; }
/// <summary>
/// Gets or sets the column footer.
/// </summary>
public IRenderable? Footer { get; set; }
/// <summary>
/// Gets or sets the width of the column.
/// If <c>null</c>, the column will adapt to its contents.
/// </summary>
public int? Width { get; set; }
/// <summary>
/// Gets or sets the padding of the column.
/// Vertical padding (top and bottom) is ignored.
/// </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="header">The table column header.</param>
public TableColumn(string header)
: this(new Markup(header).Overflow(Overflow.Ellipsis))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="TableColumn"/> class.
/// </summary>
/// <param name="header">The <see cref="IRenderable"/> instance to use as the table column header.</param>
public TableColumn(IRenderable header)
{
Header = header ?? throw new ArgumentNullException(nameof(header));
Width = null;
Padding = new Padding(1, 0, 1, 0);
NoWrap = false;
Alignment = null;
}
}
}
/// <summary>
/// Initializes a new instance of the <see cref="TableColumn"/> class.
/// </summary>
/// <param name="header">The <see cref="IRenderable"/> instance to use as the table column header.</param>
public TableColumn(IRenderable header)
{
Header = header ?? throw new ArgumentNullException(nameof(header));
Width = null;
Padding = new Padding(1, 0, 1, 0);
NoWrap = false;
Alignment = null;
}
}

View File

@ -3,158 +3,157 @@ using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class TableMeasurer : TableAccessor
{
internal sealed class TableMeasurer : TableAccessor
private const int EdgeCount = 2;
private readonly int? _explicitWidth;
private readonly TableBorder _border;
private readonly bool _padRightCell;
public TableMeasurer(Table table, RenderContext options)
: base(table, options)
{
private const int EdgeCount = 2;
_explicitWidth = table.Width;
_border = table.Border;
_padRightCell = table.PadRightCell;
}
private readonly int? _explicitWidth;
private readonly TableBorder _border;
private readonly bool _padRightCell;
public TableMeasurer(Table table, RenderContext options)
: base(table, options)
public int CalculateTotalCellWidth(int maxWidth)
{
var totalCellWidth = maxWidth;
if (_explicitWidth != null)
{
_explicitWidth = table.Width;
_border = table.Border;
_padRightCell = table.PadRightCell;
totalCellWidth = Math.Min(_explicitWidth.Value, maxWidth);
}
public int CalculateTotalCellWidth(int maxWidth)
{
var totalCellWidth = maxWidth;
if (_explicitWidth != null)
{
totalCellWidth = Math.Min(_explicitWidth.Value, maxWidth);
}
return totalCellWidth - GetNonColumnWidth();
}
return totalCellWidth - GetNonColumnWidth();
/// <summary>
/// Gets the width of everything that's not a cell.
/// That means separators, edges and padding.
/// </summary>
/// <returns>The width of everything that's not a cell.</returns>
public int GetNonColumnWidth()
{
var hideBorder = !_border.Visible;
var separators = hideBorder ? 0 : Columns.Count - 1;
var edges = hideBorder ? 0 : EdgeCount;
var padding = Columns.Select(x => x.Padding?.GetWidth() ?? 0).Sum();
if (!_padRightCell)
{
padding -= Columns.Last().Padding.GetRightSafe();
}
/// <summary>
/// Gets the width of everything that's not a cell.
/// That means separators, edges and padding.
/// </summary>
/// <returns>The width of everything that's not a cell.</returns>
public int GetNonColumnWidth()
return separators + edges + padding;
}
/// <summary>
/// Calculates the width of all columns minus any padding.
/// </summary>
/// <param name="maxWidth">The maximum width that the columns may occupy.</param>
/// <returns>A list of column widths.</returns>
public List<int> CalculateColumnWidths(int maxWidth)
{
var width_ranges = Columns.Select(column => MeasureColumn(column, maxWidth)).ToArray();
var widths = width_ranges.Select(range => range.Max).ToList();
var tableWidth = widths.Sum();
if (tableWidth > maxWidth)
{
var hideBorder = !_border.Visible;
var separators = hideBorder ? 0 : Columns.Count - 1;
var edges = hideBorder ? 0 : EdgeCount;
var padding = Columns.Select(x => x.Padding?.GetWidth() ?? 0).Sum();
var wrappable = Columns.Select(c => !c.NoWrap).ToList();
widths = CollapseWidths(widths, wrappable, maxWidth);
tableWidth = widths.Sum();
if (!_padRightCell)
{
padding -= Columns.Last().Padding.GetRightSafe();
}
return separators + edges + padding;
}
/// <summary>
/// Calculates the width of all columns minus any padding.
/// </summary>
/// <param name="maxWidth">The maximum width that the columns may occupy.</param>
/// <returns>A list of column widths.</returns>
public List<int> CalculateColumnWidths(int maxWidth)
{
var width_ranges = Columns.Select(column => MeasureColumn(column, maxWidth)).ToArray();
var widths = width_ranges.Select(range => range.Max).ToList();
var tableWidth = widths.Sum();
// last resort, reduce columns evenly
if (tableWidth > maxWidth)
{
var wrappable = Columns.Select(c => !c.NoWrap).ToList();
widths = CollapseWidths(widths, wrappable, maxWidth);
var excessWidth = tableWidth - maxWidth;
widths = Ratio.Reduce(excessWidth, widths.Select(_ => 1).ToList(), widths, widths);
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 && Expand)
{
var padWidths = Ratio.Distribute(maxWidth - tableWidth, widths);
widths = widths.Zip(padWidths, (a, b) => (a, b)).Select(f => f.a + f.b).ToList();
}
return widths;
}
public Measurement MeasureColumn(TableColumn column, int maxWidth)
if (tableWidth < maxWidth && Expand)
{
// Predetermined width?
if (column.Width != null)
{
return new Measurement(column.Width.Value, column.Width.Value);
}
var columnIndex = Columns.IndexOf(column);
var rows = Rows.Select(row => row[columnIndex]);
var minWidths = new List<int>();
var maxWidths = new List<int>();
// Include columns (both header and footer) in measurement
var headerMeasure = column.Header.Measure(Options, maxWidth);
var footerMeasure = column.Footer?.Measure(Options, maxWidth) ?? headerMeasure;
minWidths.Add(Math.Min(headerMeasure.Min, footerMeasure.Min));
maxWidths.Add(Math.Max(headerMeasure.Max, footerMeasure.Max));
foreach (var row in rows)
{
var rowMeasure = row.Measure(Options, maxWidth);
minWidths.Add(rowMeasure.Min);
maxWidths.Add(rowMeasure.Max);
}
var padding = column.Padding?.GetWidth() ?? 0;
return new Measurement(
minWidths.Count > 0 ? minWidths.Max() : padding,
maxWidths.Count > 0 ? maxWidths.Max() : maxWidth);
var padWidths = Ratio.Distribute(maxWidth - tableWidth, widths);
widths = widths.Zip(padWidths, (a, b) => (a, b)).Select(f => f.a + f.b).ToList();
}
// 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;
}
return widths;
}
}
public Measurement MeasureColumn(TableColumn column, int maxWidth)
{
// Predetermined width?
if (column.Width != null)
{
return new Measurement(column.Width.Value, column.Width.Value);
}
var columnIndex = Columns.IndexOf(column);
var rows = Rows.Select(row => row[columnIndex]);
var minWidths = new List<int>();
var maxWidths = new List<int>();
// Include columns (both header and footer) in measurement
var headerMeasure = column.Header.Measure(Options, maxWidth);
var footerMeasure = column.Footer?.Measure(Options, maxWidth) ?? headerMeasure;
minWidths.Add(Math.Min(headerMeasure.Min, footerMeasure.Min));
maxWidths.Add(Math.Max(headerMeasure.Max, footerMeasure.Max));
foreach (var row in rows)
{
var rowMeasure = row.Measure(Options, maxWidth);
minWidths.Add(rowMeasure.Min);
maxWidths.Add(rowMeasure.Max);
}
var padding = column.Padding?.GetWidth() ?? 0;
return new Measurement(
minWidths.Count > 0 ? minWidths.Max() : padding,
maxWidths.Count > 0 ? maxWidths.Max() : maxWidth);
}
// 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;
}
}

View File

@ -3,179 +3,178 @@ using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
internal static class TableRenderer
{
internal static class TableRenderer
private static readonly Style _defaultHeadingStyle = new Style(Color.Silver);
private static readonly Style _defaultCaptionStyle = new Style(Color.Grey);
public static List<Segment> Render(TableRendererContext context, List<int> columnWidths)
{
private static readonly Style _defaultHeadingStyle = new Style(Color.Silver);
private static readonly Style _defaultCaptionStyle = new Style(Color.Grey);
public static List<Segment> Render(TableRendererContext context, List<int> columnWidths)
// Can't render the table?
if (context.TableWidth <= 0 || context.TableWidth > context.MaxWidth || columnWidths.Any(c => c <= 0))
{
// Can't render the table?
if (context.TableWidth <= 0 || context.TableWidth > context.MaxWidth || columnWidths.Any(c => c <= 0))
{
return new List<Segment>(new[] { new Segment("…", context.BorderStyle ?? Style.Plain) });
}
var result = new List<Segment>();
result.AddRange(RenderAnnotation(context, context.Title, _defaultHeadingStyle));
// Iterate all rows
foreach (var (index, isFirstRow, isLastRow, row) in context.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 = context.Columns[columnIndex].Alignment;
var childContext = context.Options.WithJustification(justification);
var lines = Segment.SplitLines(cell.Render(childContext, rowWidth));
cellHeight = Math.Max(cellHeight, lines.Count);
cells.Add(lines);
}
// Show top of header?
if (isFirstRow && context.ShowBorder)
{
var separator = Aligner.Align(context.Border.GetColumnRow(TablePart.Top, columnWidths, context.Columns), context.Alignment, context.MaxWidth);
result.Add(new Segment(separator, context.BorderStyle));
result.Add(Segment.LineBreak);
}
// Show footer separator?
if (context.ShowFooters && isLastRow && context.ShowBorder && context.HasFooters)
{
var textBorder = context.Border.GetColumnRow(TablePart.FooterSeparator, columnWidths, context.Columns);
if (!string.IsNullOrEmpty(textBorder))
{
var separator = Aligner.Align(textBorder, context.Alignment, context.MaxWidth);
result.Add(new Segment(separator, context.BorderStyle));
result.Add(Segment.LineBreak);
}
}
// Make cells the same shape
cells = Segment.MakeSameHeight(cellHeight, cells);
// Iterate through each cell row
foreach (var cellRowIndex in Enumerable.Range(0, cellHeight))
{
var rowResult = new List<Segment>();
foreach (var (cellIndex, isFirstCell, isLastCell, cell) in cells.Enumerate())
{
if (isFirstCell && context.ShowBorder)
{
// Show left column edge
var part = isFirstRow && context.ShowHeaders ? TableBorderPart.HeaderLeft : TableBorderPart.CellLeft;
rowResult.Add(new Segment(context.Border.GetPart(part), context.BorderStyle));
}
// Pad column on left side.
if (context.ShowBorder || context.IsGrid)
{
var leftPadding = context.Columns[cellIndex].Padding.GetLeftSafe();
if (leftPadding > 0)
{
rowResult.Add(new Segment(new string(' ', leftPadding)));
}
}
// Add content
rowResult.AddRange(cell[cellRowIndex]);
// Pad cell content right
var length = cell[cellRowIndex].Sum(segment => segment.CellCount());
if (length < columnWidths[cellIndex])
{
rowResult.Add(new Segment(new string(' ', columnWidths[cellIndex] - length)));
}
// Pad column on the right side
if (context.ShowBorder || (context.HideBorder && !isLastCell) || (context.HideBorder && isLastCell && context.IsGrid && context.PadRightCell))
{
var rightPadding = context.Columns[cellIndex].Padding.GetRightSafe();
if (rightPadding > 0)
{
rowResult.Add(new Segment(new string(' ', rightPadding)));
}
}
if (isLastCell && context.ShowBorder)
{
// Add right column edge
var part = isFirstRow && context.ShowHeaders ? TableBorderPart.HeaderRight : TableBorderPart.CellRight;
rowResult.Add(new Segment(context.Border.GetPart(part), context.BorderStyle));
}
else if (context.ShowBorder)
{
// Add column separator
var part = isFirstRow && context.ShowHeaders ? TableBorderPart.HeaderSeparator : TableBorderPart.CellSeparator;
rowResult.Add(new Segment(context.Border.GetPart(part), context.BorderStyle));
}
}
// Align the row result.
Aligner.Align(context.Options, rowResult, context.Alignment, context.MaxWidth);
// Is the row larger than the allowed max width?
if (Segment.CellCount(rowResult) > context.MaxWidth)
{
result.AddRange(Segment.Truncate(rowResult, context.MaxWidth));
}
else
{
result.AddRange(rowResult);
}
result.Add(Segment.LineBreak);
}
// Show header separator?
if (isFirstRow && context.ShowBorder && context.ShowHeaders && context.HasRows)
{
var separator = Aligner.Align(context.Border.GetColumnRow(TablePart.HeaderSeparator, columnWidths, context.Columns), context.Alignment, context.MaxWidth);
result.Add(new Segment(separator, context.BorderStyle));
result.Add(Segment.LineBreak);
}
// Show bottom of footer?
if (isLastRow && context.ShowBorder)
{
var separator = Aligner.Align(context.Border.GetColumnRow(TablePart.Bottom, columnWidths, context.Columns), context.Alignment, context.MaxWidth);
result.Add(new Segment(separator, context.BorderStyle));
result.Add(Segment.LineBreak);
}
}
result.AddRange(RenderAnnotation(context, context.Caption, _defaultCaptionStyle));
return result;
return new List<Segment>(new[] { new Segment("…", context.BorderStyle ?? Style.Plain) });
}
private static IEnumerable<Segment> RenderAnnotation(TableRendererContext context, TableTitle? header, Style defaultStyle)
var result = new List<Segment>();
result.AddRange(RenderAnnotation(context, context.Title, _defaultHeadingStyle));
// Iterate all rows
foreach (var (index, isFirstRow, isLastRow, row) in context.Rows.Enumerate())
{
if (header == null)
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())
{
return Array.Empty<Segment>();
var justification = context.Columns[columnIndex].Alignment;
var childContext = context.Options.WithJustification(justification);
var lines = Segment.SplitLines(cell.Render(childContext, rowWidth));
cellHeight = Math.Max(cellHeight, lines.Count);
cells.Add(lines);
}
var paragraph = new Markup(header.Text, header.Style ?? defaultStyle)
.Alignment(Justify.Center)
.Overflow(Overflow.Ellipsis);
// Show top of header?
if (isFirstRow && context.ShowBorder)
{
var separator = Aligner.Align(context.Border.GetColumnRow(TablePart.Top, columnWidths, context.Columns), context.Alignment, context.MaxWidth);
result.Add(new Segment(separator, context.BorderStyle));
result.Add(Segment.LineBreak);
}
// Render the paragraphs
var segments = new List<Segment>();
segments.AddRange(((IRenderable)paragraph).Render(context.Options, context.TableWidth));
// Show footer separator?
if (context.ShowFooters && isLastRow && context.ShowBorder && context.HasFooters)
{
var textBorder = context.Border.GetColumnRow(TablePart.FooterSeparator, columnWidths, context.Columns);
if (!string.IsNullOrEmpty(textBorder))
{
var separator = Aligner.Align(textBorder, context.Alignment, context.MaxWidth);
result.Add(new Segment(separator, context.BorderStyle));
result.Add(Segment.LineBreak);
}
}
// Align over the whole buffer area
Aligner.Align(context.Options, segments, context.Alignment, context.MaxWidth);
// Make cells the same shape
cells = Segment.MakeSameHeight(cellHeight, cells);
segments.Add(Segment.LineBreak);
return segments;
// Iterate through each cell row
foreach (var cellRowIndex in Enumerable.Range(0, cellHeight))
{
var rowResult = new List<Segment>();
foreach (var (cellIndex, isFirstCell, isLastCell, cell) in cells.Enumerate())
{
if (isFirstCell && context.ShowBorder)
{
// Show left column edge
var part = isFirstRow && context.ShowHeaders ? TableBorderPart.HeaderLeft : TableBorderPart.CellLeft;
rowResult.Add(new Segment(context.Border.GetPart(part), context.BorderStyle));
}
// Pad column on left side.
if (context.ShowBorder || context.IsGrid)
{
var leftPadding = context.Columns[cellIndex].Padding.GetLeftSafe();
if (leftPadding > 0)
{
rowResult.Add(new Segment(new string(' ', leftPadding)));
}
}
// Add content
rowResult.AddRange(cell[cellRowIndex]);
// Pad cell content right
var length = cell[cellRowIndex].Sum(segment => segment.CellCount());
if (length < columnWidths[cellIndex])
{
rowResult.Add(new Segment(new string(' ', columnWidths[cellIndex] - length)));
}
// Pad column on the right side
if (context.ShowBorder || (context.HideBorder && !isLastCell) || (context.HideBorder && isLastCell && context.IsGrid && context.PadRightCell))
{
var rightPadding = context.Columns[cellIndex].Padding.GetRightSafe();
if (rightPadding > 0)
{
rowResult.Add(new Segment(new string(' ', rightPadding)));
}
}
if (isLastCell && context.ShowBorder)
{
// Add right column edge
var part = isFirstRow && context.ShowHeaders ? TableBorderPart.HeaderRight : TableBorderPart.CellRight;
rowResult.Add(new Segment(context.Border.GetPart(part), context.BorderStyle));
}
else if (context.ShowBorder)
{
// Add column separator
var part = isFirstRow && context.ShowHeaders ? TableBorderPart.HeaderSeparator : TableBorderPart.CellSeparator;
rowResult.Add(new Segment(context.Border.GetPart(part), context.BorderStyle));
}
}
// Align the row result.
Aligner.Align(context.Options, rowResult, context.Alignment, context.MaxWidth);
// Is the row larger than the allowed max width?
if (Segment.CellCount(rowResult) > context.MaxWidth)
{
result.AddRange(Segment.Truncate(rowResult, context.MaxWidth));
}
else
{
result.AddRange(rowResult);
}
result.Add(Segment.LineBreak);
}
// Show header separator?
if (isFirstRow && context.ShowBorder && context.ShowHeaders && context.HasRows)
{
var separator = Aligner.Align(context.Border.GetColumnRow(TablePart.HeaderSeparator, columnWidths, context.Columns), context.Alignment, context.MaxWidth);
result.Add(new Segment(separator, context.BorderStyle));
result.Add(Segment.LineBreak);
}
// Show bottom of footer?
if (isLastRow && context.ShowBorder)
{
var separator = Aligner.Align(context.Border.GetColumnRow(TablePart.Bottom, columnWidths, context.Columns), context.Alignment, context.MaxWidth);
result.Add(new Segment(separator, context.BorderStyle));
result.Add(Segment.LineBreak);
}
}
result.AddRange(RenderAnnotation(context, context.Caption, _defaultCaptionStyle));
return result;
}
}
private static IEnumerable<Segment> RenderAnnotation(TableRendererContext context, TableTitle? header, Style defaultStyle)
{
if (header == null)
{
return Array.Empty<Segment>();
}
var paragraph = new Markup(header.Text, header.Style ?? defaultStyle)
.Alignment(Justify.Center)
.Overflow(Overflow.Ellipsis);
// Render the paragraphs
var segments = new List<Segment>();
segments.AddRange(((IRenderable)paragraph).Render(context.Options, context.TableWidth));
// Align over the whole buffer area
Aligner.Align(context.Options, segments, context.Alignment, context.MaxWidth);
segments.Add(Segment.LineBreak);
return segments;
}
}

View File

@ -3,55 +3,54 @@ using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class TableRendererContext : TableAccessor
{
internal sealed class TableRendererContext : TableAccessor
private readonly Table _table;
private readonly List<TableRow> _rows;
public override IReadOnlyList<TableRow> Rows => _rows;
public TableBorder Border { get; }
public Style BorderStyle { get; }
public bool ShowBorder { get; }
public bool HasRows { get; }
public bool HasFooters { get; }
/// <summary>
/// Gets the max width of the destination area.
/// The table might take up less than this.
/// </summary>
public int MaxWidth { get; }
/// <summary>
/// Gets the width of the table.
/// </summary>
public int TableWidth { get; }
public bool HideBorder => !ShowBorder;
public bool ShowHeaders => _table.ShowHeaders;
public bool ShowFooters => _table.ShowFooters;
public bool IsGrid => _table.IsGrid;
public bool PadRightCell => _table.PadRightCell;
public TableTitle? Title => _table.Title;
public TableTitle? Caption => _table.Caption;
public Justify? Alignment => _table.Alignment;
public TableRendererContext(Table table, RenderContext options, IEnumerable<TableRow> rows, int tableWidth, int maxWidth)
: base(table, options)
{
private readonly Table _table;
private readonly List<TableRow> _rows;
_table = table ?? throw new ArgumentNullException(nameof(table));
_rows = new List<TableRow>(rows ?? Enumerable.Empty<TableRow>());
public override IReadOnlyList<TableRow> Rows => _rows;
ShowBorder = _table.Border.Visible;
HasRows = Rows.Any(row => !row.IsHeader && !row.IsFooter);
HasFooters = Rows.Any(column => column.IsFooter);
Border = table.Border.GetSafeBorder(!options.Unicode && table.UseSafeBorder);
BorderStyle = table.BorderStyle ?? Style.Plain;
public TableBorder Border { get; }
public Style BorderStyle { get; }
public bool ShowBorder { get; }
public bool HasRows { get; }
public bool HasFooters { get; }
/// <summary>
/// Gets the max width of the destination area.
/// The table might take up less than this.
/// </summary>
public int MaxWidth { get; }
/// <summary>
/// Gets the width of the table.
/// </summary>
public int TableWidth { get; }
public bool HideBorder => !ShowBorder;
public bool ShowHeaders => _table.ShowHeaders;
public bool ShowFooters => _table.ShowFooters;
public bool IsGrid => _table.IsGrid;
public bool PadRightCell => _table.PadRightCell;
public TableTitle? Title => _table.Title;
public TableTitle? Caption => _table.Caption;
public Justify? Alignment => _table.Alignment;
public TableRendererContext(Table table, RenderContext options, IEnumerable<TableRow> rows, int tableWidth, int maxWidth)
: base(table, options)
{
_table = table ?? throw new ArgumentNullException(nameof(table));
_rows = new List<TableRow>(rows ?? Enumerable.Empty<TableRow>());
ShowBorder = _table.Border.Visible;
HasRows = Rows.Any(row => !row.IsHeader && !row.IsFooter);
HasFooters = Rows.Any(column => column.IsFooter);
Border = table.Border.GetSafeBorder(!options.Unicode && table.UseSafeBorder);
BorderStyle = table.BorderStyle ?? Style.Plain;
TableWidth = tableWidth;
MaxWidth = maxWidth;
}
TableWidth = tableWidth;
MaxWidth = maxWidth;
}
}
}

View File

@ -3,80 +3,79 @@ using System.Collections;
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents a table row.
/// </summary>
public sealed class TableRow : IEnumerable<IRenderable>
{
private readonly List<IRenderable> _items;
/// <summary>
/// Represents a table row.
/// Gets the number of columns in the row.
/// </summary>
public sealed class TableRow : IEnumerable<IRenderable>
public int Count => _items.Count;
internal bool IsHeader { get; }
internal bool IsFooter { get; }
/// <summary>
/// Gets a row item at the specified table column index.
/// </summary>
/// <param name="index">The table column index.</param>
/// <returns>The row item at the specified table column index.</returns>
public IRenderable this[int index]
{
private readonly List<IRenderable> _items;
/// <summary>
/// Gets the number of columns in the row.
/// </summary>
public int Count => _items.Count;
internal bool IsHeader { get; }
internal bool IsFooter { get; }
/// <summary>
/// Gets a row item at the specified table column index.
/// </summary>
/// <param name="index">The table column index.</param>
/// <returns>The row item at the specified table column index.</returns>
public IRenderable this[int index]
{
get => _items[index];
}
/// <summary>
/// Initializes a new instance of the <see cref="TableRow"/> class.
/// </summary>
/// <param name="items">The row items.</param>
public TableRow(IEnumerable<IRenderable> items)
: this(items, false, false)
{
}
private TableRow(IEnumerable<IRenderable> items, bool isHeader, bool isFooter)
{
_items = new List<IRenderable>(items ?? Array.Empty<IRenderable>());
IsHeader = isHeader;
IsFooter = isFooter;
}
internal static TableRow Header(IEnumerable<IRenderable> items)
{
return new TableRow(items, true, false);
}
internal static TableRow Footer(IEnumerable<IRenderable> items)
{
return new TableRow(items, false, true);
}
internal void Add(IRenderable item)
{
if (item is null)
{
throw new ArgumentNullException(nameof(item));
}
_items.Add(item);
}
/// <inheritdoc/>
public IEnumerator<IRenderable> GetEnumerator()
{
return _items.GetEnumerator();
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
get => _items[index];
}
}
/// <summary>
/// Initializes a new instance of the <see cref="TableRow"/> class.
/// </summary>
/// <param name="items">The row items.</param>
public TableRow(IEnumerable<IRenderable> items)
: this(items, false, false)
{
}
private TableRow(IEnumerable<IRenderable> items, bool isHeader, bool isFooter)
{
_items = new List<IRenderable>(items ?? Array.Empty<IRenderable>());
IsHeader = isHeader;
IsFooter = isFooter;
}
internal static TableRow Header(IEnumerable<IRenderable> items)
{
return new TableRow(items, true, false);
}
internal static TableRow Footer(IEnumerable<IRenderable> items)
{
return new TableRow(items, false, true);
}
internal void Add(IRenderable item)
{
if (item is null)
{
throw new ArgumentNullException(nameof(item));
}
_items.Add(item);
}
/// <inheritdoc/>
public IEnumerator<IRenderable> GetEnumerator()
{
return _items.GetEnumerator();
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

View File

@ -4,206 +4,205 @@ using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents a collection holding table rows.
/// </summary>
public sealed class TableRowCollection : IReadOnlyList<TableRow>
{
/// <summary>
/// Represents a collection holding table rows.
/// </summary>
public sealed class TableRowCollection : IReadOnlyList<TableRow>
private readonly Table _table;
private readonly IList<TableRow> _list;
private readonly object _lock;
/// <inheritdoc/>
TableRow IReadOnlyList<TableRow>.this[int index]
{
private readonly Table _table;
private readonly IList<TableRow> _list;
private readonly object _lock;
/// <inheritdoc/>
TableRow IReadOnlyList<TableRow>.this[int index]
{
get
{
lock (_lock)
{
return _list[index];
}
}
}
/// <summary>
/// Gets the number of rows in the collection.
/// </summary>
public int Count
{
get
{
lock (_lock)
{
return _list.Count;
}
}
}
internal TableRowCollection(Table table)
{
_table = table ?? throw new ArgumentNullException(nameof(table));
_list = new List<TableRow>();
_lock = new object();
}
/// <summary>
/// Adds a new row.
/// </summary>
/// <param name="columns">The columns that are part of the row to add.</param>
/// <returns>The index of the added item.</returns>
public int Add(IEnumerable<IRenderable> columns)
{
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
lock (_lock)
{
var row = CreateRow(columns);
_list.Add(row);
return _list.IndexOf(row);
}
}
/// <summary>
/// Inserts a new row at the specified index.
/// </summary>
/// <param name="index">The index to insert the row at.</param>
/// <param name="columns">The columns that are part of the row to insert.</param>
/// <returns>The index of the inserted item.</returns>
public int Insert(int index, IEnumerable<IRenderable> columns)
{
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
lock (_lock)
{
var row = CreateRow(columns);
_list.Insert(index, row);
return _list.IndexOf(row);
}
}
/// <summary>
/// Update a table cell at the specified index.
/// </summary>
/// <param name="row">Index of cell row.</param>
/// <param name="column">index of cell column.</param>
/// <param name="cellData">The new cells details.</param>
public void Update(int row, int column, IRenderable cellData)
{
if (cellData is null)
{
throw new ArgumentNullException(nameof(cellData));
}
lock (_lock)
{
if (row < 0)
{
throw new IndexOutOfRangeException("Table row index cannot be negative.");
}
else if (row >= _list.Count)
{
throw new IndexOutOfRangeException("Table row index cannot exceed the number of rows in the table.");
}
var tableRow = _list.ElementAtOrDefault(row);
var currentRenderables = tableRow.ToList();
if (column < 0)
{
throw new IndexOutOfRangeException("Table column index cannot be negative.");
}
else if (column >= currentRenderables.Count)
{
throw new IndexOutOfRangeException("Table column index cannot exceed the number of rows in the table.");
}
currentRenderables.RemoveAt(column);
currentRenderables.Insert(column, cellData);
var newTableRow = new TableRow(currentRenderables);
_list.RemoveAt(row);
_list.Insert(row, newTableRow);
}
}
/// <summary>
/// Removes a row at the specified index.
/// </summary>
/// <param name="index">The index to remove a row at.</param>
public void RemoveAt(int index)
get
{
lock (_lock)
{
if (index < 0)
{
throw new IndexOutOfRangeException("Table row index cannot be negative.");
}
else if (index >= _list.Count)
{
throw new IndexOutOfRangeException("Table row index cannot exceed the number of rows in the table.");
}
_list.RemoveAt(index);
return _list[index];
}
}
/// <summary>
/// Clears all rows.
/// </summary>
public void Clear()
{
lock (_lock)
{
_list.Clear();
}
}
/// <inheritdoc/>
public IEnumerator<TableRow> GetEnumerator()
{
lock (_lock)
{
var items = new TableRow[_list.Count];
_list.CopyTo(items, 0);
return new TableRowEnumerator(items);
}
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private TableRow CreateRow(IEnumerable<IRenderable> columns)
{
var row = new TableRow(columns);
if (row.Count > _table.Columns.Count)
{
throw new InvalidOperationException("The number of row columns are greater than the number of table columns.");
}
// Need to add missing columns
if (row.Count < _table.Columns.Count)
{
var diff = _table.Columns.Count - row.Count;
Enumerable.Range(0, diff).ForEach(_ => row.Add(Text.Empty));
}
return row;
}
}
}
/// <summary>
/// Gets the number of rows in the collection.
/// </summary>
public int Count
{
get
{
lock (_lock)
{
return _list.Count;
}
}
}
internal TableRowCollection(Table table)
{
_table = table ?? throw new ArgumentNullException(nameof(table));
_list = new List<TableRow>();
_lock = new object();
}
/// <summary>
/// Adds a new row.
/// </summary>
/// <param name="columns">The columns that are part of the row to add.</param>
/// <returns>The index of the added item.</returns>
public int Add(IEnumerable<IRenderable> columns)
{
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
lock (_lock)
{
var row = CreateRow(columns);
_list.Add(row);
return _list.IndexOf(row);
}
}
/// <summary>
/// Inserts a new row at the specified index.
/// </summary>
/// <param name="index">The index to insert the row at.</param>
/// <param name="columns">The columns that are part of the row to insert.</param>
/// <returns>The index of the inserted item.</returns>
public int Insert(int index, IEnumerable<IRenderable> columns)
{
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
lock (_lock)
{
var row = CreateRow(columns);
_list.Insert(index, row);
return _list.IndexOf(row);
}
}
/// <summary>
/// Update a table cell at the specified index.
/// </summary>
/// <param name="row">Index of cell row.</param>
/// <param name="column">index of cell column.</param>
/// <param name="cellData">The new cells details.</param>
public void Update(int row, int column, IRenderable cellData)
{
if (cellData is null)
{
throw new ArgumentNullException(nameof(cellData));
}
lock (_lock)
{
if (row < 0)
{
throw new IndexOutOfRangeException("Table row index cannot be negative.");
}
else if (row >= _list.Count)
{
throw new IndexOutOfRangeException("Table row index cannot exceed the number of rows in the table.");
}
var tableRow = _list.ElementAtOrDefault(row);
var currentRenderables = tableRow.ToList();
if (column < 0)
{
throw new IndexOutOfRangeException("Table column index cannot be negative.");
}
else if (column >= currentRenderables.Count)
{
throw new IndexOutOfRangeException("Table column index cannot exceed the number of rows in the table.");
}
currentRenderables.RemoveAt(column);
currentRenderables.Insert(column, cellData);
var newTableRow = new TableRow(currentRenderables);
_list.RemoveAt(row);
_list.Insert(row, newTableRow);
}
}
/// <summary>
/// Removes a row at the specified index.
/// </summary>
/// <param name="index">The index to remove a row at.</param>
public void RemoveAt(int index)
{
lock (_lock)
{
if (index < 0)
{
throw new IndexOutOfRangeException("Table row index cannot be negative.");
}
else if (index >= _list.Count)
{
throw new IndexOutOfRangeException("Table row index cannot exceed the number of rows in the table.");
}
_list.RemoveAt(index);
}
}
/// <summary>
/// Clears all rows.
/// </summary>
public void Clear()
{
lock (_lock)
{
_list.Clear();
}
}
/// <inheritdoc/>
public IEnumerator<TableRow> GetEnumerator()
{
lock (_lock)
{
var items = new TableRow[_list.Count];
_list.CopyTo(items, 0);
return new TableRowEnumerator(items);
}
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private TableRow CreateRow(IEnumerable<IRenderable> columns)
{
var row = new TableRow(columns);
if (row.Count > _table.Columns.Count)
{
throw new InvalidOperationException("The number of row columns are greater than the number of table columns.");
}
// Need to add missing columns
if (row.Count < _table.Columns.Count)
{
var diff = _table.Columns.Count - row.Count;
Enumerable.Range(0, diff).ForEach(_ => row.Add(Text.Empty));
}
return row;
}
}

View File

@ -2,35 +2,34 @@ using System;
using System.Collections;
using System.Collections.Generic;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class TableRowEnumerator : IEnumerator<TableRow>
{
internal sealed class TableRowEnumerator : IEnumerator<TableRow>
private readonly TableRow[] _items;
private int _index;
public TableRow Current => _items[_index];
object? IEnumerator.Current => _items[_index];
public TableRowEnumerator(TableRow[] items)
{
private readonly TableRow[] _items;
private int _index;
public TableRow Current => _items[_index];
object? IEnumerator.Current => _items[_index];
public TableRowEnumerator(TableRow[] items)
{
_items = items ?? throw new ArgumentNullException(nameof(items));
_index = -1;
}
public void Dispose()
{
}
public bool MoveNext()
{
_index++;
return _index < _items.Length;
}
public void Reset()
{
_index = -1;
}
_items = items ?? throw new ArgumentNullException(nameof(items));
_index = -1;
}
}
public void Dispose()
{
}
public bool MoveNext()
{
_index++;
return _index < _items.Length;
}
public void Reset()
{
_index = -1;
}
}

View File

@ -1,58 +1,57 @@
using System;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents a table title such as a heading or footnote.
/// </summary>
public sealed class TableTitle
{
/// <summary>
/// Represents a table title such as a heading or footnote.
/// Gets the title text.
/// </summary>
public sealed class TableTitle
public string Text { get; }
/// <summary>
/// Gets or sets the title style.
/// </summary>
public Style? Style { get; set; }
/// <summary>
/// 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>
public TableTitle(string text, Style? style = null)
{
/// <summary>
/// Gets the title text.
/// </summary>
public string Text { get; }
/// <summary>
/// Gets or sets the title style.
/// </summary>
public Style? Style { get; set; }
/// <summary>
/// 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>
public TableTitle(string text, Style? style = null)
{
Text = text ?? throw new ArgumentNullException(nameof(text));
Style = style;
}
/// <summary>
/// Sets the title style.
/// </summary>
/// <param name="style">The title style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public TableTitle SetStyle(Style? style)
{
Style = style ?? Style.Plain;
return this;
}
/// <summary>
/// Sets the title style.
/// </summary>
/// <param name="style">The title style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public TableTitle SetStyle(string style)
{
if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
Style = Style.Parse(style);
return this;
}
Text = text ?? throw new ArgumentNullException(nameof(text));
Style = style;
}
}
/// <summary>
/// Sets the title style.
/// </summary>
/// <param name="style">The title style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public TableTitle SetStyle(Style? style)
{
Style = style ?? Style.Plain;
return this;
}
/// <summary>
/// Sets the title style.
/// </summary>
/// <param name="style">The title style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public TableTitle SetStyle(string style)
{
if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
Style = Style.Parse(style);
return this;
}
}

View File

@ -4,75 +4,74 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Spectre.Console.Rendering;
namespace Spectre.Console
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, IOverflowable
{
private readonly Paragraph _paragraph;
/// <summary>
/// A renderable piece of text.
/// Gets an empty <see cref="Text"/> instance.
/// </summary>
[DebuggerDisplay("{_text,nq}")]
[SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
public sealed class Text : Renderable, IAlignable, IOverflowable
public static Text Empty { get; } = new Text(string.Empty);
/// <summary>
/// Gets an instance of <see cref="Text"/> containing a new line.
/// </summary>
public static Text NewLine { get; } = new Text(Environment.NewLine, Style.Plain);
/// <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 or <see cref="Style.Plain"/> if <see langword="null"/>.</param>
public Text(string text, Style? style = null)
{
private readonly Paragraph _paragraph;
/// <summary>
/// Gets an empty <see cref="Text"/> instance.
/// </summary>
public static Text Empty { get; } = new Text(string.Empty);
/// <summary>
/// Gets an instance of <see cref="Text"/> containing a new line.
/// </summary>
public static Text NewLine { get; } = new Text(Environment.NewLine, Style.Plain);
/// <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 or <see cref="Style.Plain"/> if <see langword="null"/>.</param>
public Text(string text, Style? style = null)
{
_paragraph = new Paragraph(text, style);
}
/// <summary>
/// Gets or sets the text alignment.
/// </summary>
public Justify? Alignment
{
get => _paragraph.Alignment;
set => _paragraph.Alignment = value;
}
/// <summary>
/// Gets or sets the text overflow strategy.
/// </summary>
public Overflow? Overflow
{
get => _paragraph.Overflow;
set => _paragraph.Overflow = value;
}
/// <summary>
/// Gets the character count.
/// </summary>
public int Length => _paragraph.Length;
/// <summary>
/// Gets the number of lines in the text.
/// </summary>
public int Lines => _paragraph.Lines;
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
return ((IRenderable)_paragraph).Measure(context, maxWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
return ((IRenderable)_paragraph).Render(context, maxWidth);
}
_paragraph = new Paragraph(text, style);
}
}
/// <summary>
/// Gets or sets the text alignment.
/// </summary>
public Justify? Alignment
{
get => _paragraph.Alignment;
set => _paragraph.Alignment = value;
}
/// <summary>
/// Gets or sets the text overflow strategy.
/// </summary>
public Overflow? Overflow
{
get => _paragraph.Overflow;
set => _paragraph.Overflow = value;
}
/// <summary>
/// Gets the character count.
/// </summary>
public int Length => _paragraph.Length;
/// <summary>
/// Gets the number of lines in the text.
/// </summary>
public int Lines => _paragraph.Lines;
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
return ((IRenderable)_paragraph).Measure(context, maxWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
return ((IRenderable)_paragraph).Render(context, maxWidth);
}
}

View File

@ -2,130 +2,129 @@ using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Representation of non-circular tree data.
/// Each node added to the tree may only be present in it a single time, in order to facilitate cycle detection.
/// </summary>
public sealed class Tree : Renderable, IHasTreeNodes
{
private readonly TreeNode _root;
/// <summary>
/// Representation of non-circular tree data.
/// Each node added to the tree may only be present in it a single time, in order to facilitate cycle detection.
/// Gets or sets the tree style.
/// </summary>
public sealed class Tree : Renderable, IHasTreeNodes
public Style? Style { get; set; }
/// <summary>
/// Gets or sets the tree guide lines.
/// </summary>
public TreeGuide Guide { get; set; } = TreeGuide.Line;
/// <summary>
/// Gets the tree's child nodes.
/// </summary>
public List<TreeNode> Nodes => _root.Nodes;
/// <summary>
/// Gets or sets a value indicating whether or not the tree is expanded or not.
/// </summary>
public bool Expanded { get; set; } = true;
/// <summary>
/// Initializes a new instance of the <see cref="Tree"/> class.
/// </summary>
/// <param name="renderable">The tree label.</param>
public Tree(IRenderable renderable)
{
private readonly TreeNode _root;
_root = new TreeNode(renderable);
}
/// <summary>
/// Gets or sets the tree style.
/// </summary>
public Style? Style { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Tree"/> class.
/// </summary>
/// <param name="label">The tree label.</param>
public Tree(string label)
{
_root = new TreeNode(new Markup(label));
}
/// <summary>
/// Gets or sets the tree guide lines.
/// </summary>
public TreeGuide Guide { get; set; } = TreeGuide.Line;
/// <inheritdoc />
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var result = new List<Segment>();
var visitedNodes = new HashSet<TreeNode>();
/// <summary>
/// Gets the tree's child nodes.
/// </summary>
public List<TreeNode> Nodes => _root.Nodes;
var stack = new Stack<Queue<TreeNode>>();
stack.Push(new Queue<TreeNode>(new[] { _root }));
/// <summary>
/// Gets or sets a value indicating whether or not the tree is expanded or not.
/// </summary>
public bool Expanded { get; set; } = true;
var levels = new List<Segment>();
levels.Add(GetGuide(context, TreeGuidePart.Continue));
/// <summary>
/// Initializes a new instance of the <see cref="Tree"/> class.
/// </summary>
/// <param name="renderable">The tree label.</param>
public Tree(IRenderable renderable)
while (stack.Count > 0)
{
_root = new TreeNode(renderable);
}
/// <summary>
/// Initializes a new instance of the <see cref="Tree"/> class.
/// </summary>
/// <param name="label">The tree label.</param>
public Tree(string label)
{
_root = new TreeNode(new Markup(label));
}
/// <inheritdoc />
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var result = new List<Segment>();
var visitedNodes = new HashSet<TreeNode>();
var stack = new Stack<Queue<TreeNode>>();
stack.Push(new Queue<TreeNode>(new[] { _root }));
var levels = new List<Segment>();
levels.Add(GetGuide(context, TreeGuidePart.Continue));
while (stack.Count > 0)
var stackNode = stack.Pop();
if (stackNode.Count == 0)
{
var stackNode = stack.Pop();
if (stackNode.Count == 0)
levels.RemoveLast();
if (levels.Count > 0)
{
levels.RemoveLast();
if (levels.Count > 0)
{
levels.AddOrReplaceLast(GetGuide(context, TreeGuidePart.Fork));
}
continue;
levels.AddOrReplaceLast(GetGuide(context, TreeGuidePart.Fork));
}
var isLastChild = stackNode.Count == 1;
var current = stackNode.Dequeue();
if (!visitedNodes.Add(current))
continue;
}
var isLastChild = stackNode.Count == 1;
var current = stackNode.Dequeue();
if (!visitedNodes.Add(current))
{
throw new CircularTreeException("Cycle detected in tree - unable to render.");
}
stack.Push(stackNode);
if (isLastChild)
{
levels.AddOrReplaceLast(GetGuide(context, TreeGuidePart.End));
}
var prefix = levels.Skip(1).ToList();
var renderableLines = Segment.SplitLines(current.Renderable.Render(context, maxWidth - Segment.CellCount(prefix)));
foreach (var (_, isFirstLine, _, line) in renderableLines.Enumerate())
{
if (prefix.Count > 0)
{
throw new CircularTreeException("Cycle detected in tree - unable to render.");
result.AddRange(prefix.ToList());
}
stack.Push(stackNode);
result.AddRange(line);
result.Add(Segment.LineBreak);
if (isLastChild)
if (isFirstLine && prefix.Count > 0)
{
levels.AddOrReplaceLast(GetGuide(context, TreeGuidePart.End));
}
var prefix = levels.Skip(1).ToList();
var renderableLines = Segment.SplitLines(current.Renderable.Render(context, maxWidth - Segment.CellCount(prefix)));
foreach (var (_, isFirstLine, _, line) in renderableLines.Enumerate())
{
if (prefix.Count > 0)
{
result.AddRange(prefix.ToList());
}
result.AddRange(line);
result.Add(Segment.LineBreak);
if (isFirstLine && prefix.Count > 0)
{
var part = isLastChild ? TreeGuidePart.Space : TreeGuidePart.Continue;
prefix.AddOrReplaceLast(GetGuide(context, part));
}
}
if (current.Expanded && current.Nodes.Count > 0)
{
levels.AddOrReplaceLast(GetGuide(context, isLastChild ? TreeGuidePart.Space : TreeGuidePart.Continue));
levels.Add(GetGuide(context, current.Nodes.Count == 1 ? TreeGuidePart.End : TreeGuidePart.Fork));
stack.Push(new Queue<TreeNode>(current.Nodes));
var part = isLastChild ? TreeGuidePart.Space : TreeGuidePart.Continue;
prefix.AddOrReplaceLast(GetGuide(context, part));
}
}
return result;
if (current.Expanded && current.Nodes.Count > 0)
{
levels.AddOrReplaceLast(GetGuide(context, isLastChild ? TreeGuidePart.Space : TreeGuidePart.Continue));
levels.Add(GetGuide(context, current.Nodes.Count == 1 ? TreeGuidePart.End : TreeGuidePart.Fork));
stack.Push(new Queue<TreeNode>(current.Nodes));
}
}
private Segment GetGuide(RenderContext context, TreeGuidePart part)
{
var guide = Guide.GetSafeTreeGuide(safe: !context.Unicode);
return new Segment(guide.GetPart(part), Style ?? Style.Plain);
}
return result;
}
private Segment GetGuide(RenderContext context, TreeGuidePart part)
{
var guide = Guide.GetSafeTreeGuide(safe: !context.Unicode);
return new Segment(guide.GetPart(part), Style ?? Style.Plain);
}
}

View File

@ -1,32 +1,31 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents a tree node.
/// </summary>
public sealed class TreeNode : IHasTreeNodes
{
internal IRenderable Renderable { get; }
/// <summary>
/// Represents a tree node.
/// Gets the tree node's child nodes.
/// </summary>
public sealed class TreeNode : IHasTreeNodes
public List<TreeNode> Nodes { get; } = new List<TreeNode>();
/// <summary>
/// Gets or sets a value indicating whether or not the tree node is expanded or not.
/// </summary>
public bool Expanded { get; set; } = true;
/// <summary>
/// Initializes a new instance of the <see cref="TreeNode"/> class.
/// </summary>
/// <param name="renderable">The tree node label.</param>
public TreeNode(IRenderable renderable)
{
internal IRenderable Renderable { get; }
/// <summary>
/// Gets the tree node's child nodes.
/// </summary>
public List<TreeNode> Nodes { get; } = new List<TreeNode>();
/// <summary>
/// Gets or sets a value indicating whether or not the tree node is expanded or not.
/// </summary>
public bool Expanded { get; set; } = true;
/// <summary>
/// Initializes a new instance of the <see cref="TreeNode"/> class.
/// </summary>
/// <param name="renderable">The tree node label.</param>
public TreeNode(IRenderable renderable)
{
Renderable = renderable;
}
Renderable = renderable;
}
}