mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-06-19 21:38:16 +08:00
Use file scoped namespace declarations
This commit is contained in:

committed by
Phil Scott

parent
1dbaf50935
commit
ec1188b837
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
@ -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; }
|
||||
}
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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 won’t 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 won’t 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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user