mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-19 10:12:50 +08:00
Clean up public API
* Make things a bit more consistent * Add extension methods to configure things like tables, panels and grids.
This commit is contained in:
parent
c111c7d463
commit
31f117aed0
@ -6,42 +6,38 @@ namespace PanelExample
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
var content = Text.Markup(
|
||||
var content = new Markup(
|
||||
"[underline]I[/] heard [underline on blue]you[/] like 📦\n\n\n\n" +
|
||||
"So I put a 📦 in a 📦\n\n" +
|
||||
"😅");
|
||||
"😅").Centered();
|
||||
|
||||
AnsiConsole.Render(
|
||||
new Panel(
|
||||
new Panel(content)
|
||||
{
|
||||
Alignment = Justify.Center,
|
||||
Border = BorderKind.Rounded
|
||||
}));
|
||||
|
||||
// Left adjusted panel with text
|
||||
AnsiConsole.Render(new Panel(
|
||||
new Text("Left adjusted\nLeft"))
|
||||
new Text("Left adjusted\nLeft").LeftAligned())
|
||||
{
|
||||
Expand = true,
|
||||
Alignment = Justify.Left,
|
||||
});
|
||||
|
||||
// Centered ASCII panel with text
|
||||
AnsiConsole.Render(new Panel(
|
||||
new Text("Centered\nCenter"))
|
||||
new Text("Centered\nCenter").Centered())
|
||||
{
|
||||
Expand = true,
|
||||
Alignment = Justify.Center,
|
||||
Border = BorderKind.Ascii,
|
||||
});
|
||||
|
||||
// Right adjusted, rounded panel with text
|
||||
AnsiConsole.Render(new Panel(
|
||||
new Text("Right adjusted\nRight"))
|
||||
new Text("Right adjusted\nRight").RightAligned())
|
||||
{
|
||||
Expand = true,
|
||||
Alignment = Justify.Right,
|
||||
Border = BorderKind.Rounded,
|
||||
});
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
// Given
|
||||
var console = new PlainConsole(width: 80);
|
||||
var text = new Panel(
|
||||
Text.Markup("I heard [underline on blue]you[/] like 📦\n\n\n\nSo I put a 📦 in a 📦"));
|
||||
new Markup("I heard [underline on blue]you[/] like 📦\n\n\n\nSo I put a 📦 in a 📦"));
|
||||
|
||||
// When
|
||||
console.Render(text);
|
||||
|
@ -10,20 +10,26 @@ namespace Spectre.Console.Tests.Unit
|
||||
[Fact]
|
||||
public void Should_Consider_The_Longest_Word_As_Minimum_Width()
|
||||
{
|
||||
// Given
|
||||
var text = new Text("Foo Bar Baz\nQux\nLol mobile");
|
||||
|
||||
// When
|
||||
var result = ((IRenderable)text).Measure(new RenderContext(Encoding.Unicode, false), 80);
|
||||
|
||||
// Then
|
||||
result.Min.ShouldBe(6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Consider_The_Longest_Line_As_Maximum_Width()
|
||||
{
|
||||
// Given
|
||||
var text = new Text("Foo Bar Baz\nQux\nLol mobile");
|
||||
|
||||
// When
|
||||
var result = ((IRenderable)text).Measure(new RenderContext(Encoding.Unicode, false), 80);
|
||||
|
||||
// Then
|
||||
result.Max.ShouldBe(11);
|
||||
}
|
||||
|
||||
|
@ -6,11 +6,11 @@ namespace Spectre.Console.Internal
|
||||
{
|
||||
internal static class MarkupParser
|
||||
{
|
||||
public static Text Parse(string text, Style? style = null)
|
||||
public static Paragraph Parse(string text, Style? style = null)
|
||||
{
|
||||
style ??= Style.Plain;
|
||||
|
||||
var result = new Text();
|
||||
var result = new Paragraph();
|
||||
using var tokenizer = new MarkupTokenizer(text);
|
||||
|
||||
var stack = new Stack<Style>();
|
||||
|
@ -64,13 +64,13 @@ namespace Spectre.Console
|
||||
}
|
||||
|
||||
// Only pad the most right cell if we've explicitly set a padding.
|
||||
_table.PadRightCell = column.Padding != null;
|
||||
_table.PadRightCell = column.HasExplicitPadding;
|
||||
|
||||
_table.AddColumn(new TableColumn(string.Empty)
|
||||
{
|
||||
Width = column.Width,
|
||||
NoWrap = column.NoWrap,
|
||||
Padding = column.Padding ?? new Padding(0, 2),
|
||||
Padding = column.Padding,
|
||||
Alignment = column.Alignment,
|
||||
});
|
||||
}
|
||||
|
@ -3,8 +3,10 @@ namespace Spectre.Console
|
||||
/// <summary>
|
||||
/// Represents a grid column.
|
||||
/// </summary>
|
||||
public sealed class GridColumn : IAlignable
|
||||
public sealed class GridColumn : IColumn
|
||||
{
|
||||
private Padding _padding;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width of the column.
|
||||
/// If <c>null</c>, the column will adapt to it's contents.
|
||||
@ -20,11 +22,33 @@ namespace Spectre.Console
|
||||
/// <summary>
|
||||
/// Gets or sets the padding of the column.
|
||||
/// </summary>
|
||||
public Padding? Padding { get; set; }
|
||||
public Padding Padding
|
||||
{
|
||||
get => _padding;
|
||||
set
|
||||
{
|
||||
HasExplicitPadding = true;
|
||||
_padding = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alignment of the column.
|
||||
/// </summary>
|
||||
public Justify? Alignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the user
|
||||
/// has set an explicit padding for this column.
|
||||
/// </summary>
|
||||
internal bool HasExplicitPadding { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GridColumn"/> class.
|
||||
/// </summary>
|
||||
public GridColumn()
|
||||
{
|
||||
_padding = new Padding(0, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,13 +9,13 @@ namespace Spectre.Console
|
||||
/// </summary>
|
||||
public sealed class Markup : Renderable, IAlignable
|
||||
{
|
||||
private readonly Text _text;
|
||||
private readonly Paragraph _paragraph;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Justify? Alignment
|
||||
{
|
||||
get => _text.Alignment;
|
||||
set => _text.Alignment = value;
|
||||
get => _paragraph.Alignment;
|
||||
set => _paragraph.Alignment = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -25,19 +25,19 @@ namespace Spectre.Console
|
||||
/// <param name="style">The style of the text.</param>
|
||||
public Markup(string text, Style? style = null)
|
||||
{
|
||||
_text = MarkupParser.Parse(text, style);
|
||||
_paragraph = MarkupParser.Parse(text, style);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Measurement Measure(RenderContext context, int maxWidth)
|
||||
{
|
||||
return ((IRenderable)_text).Measure(context, maxWidth);
|
||||
return ((IRenderable)_paragraph).Measure(context, maxWidth);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
|
||||
{
|
||||
return ((IRenderable)_text).Render(context, maxWidth);
|
||||
return ((IRenderable)_paragraph).Render(context, maxWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ namespace Spectre.Console
|
||||
/// <summary>
|
||||
/// A renderable panel.
|
||||
/// </summary>
|
||||
public sealed class Panel : Renderable, IHasBorder
|
||||
public sealed class Panel : Renderable, IHasBorder, IExpandable, IPaddable
|
||||
{
|
||||
private const int EdgeWidth = 2;
|
||||
|
||||
@ -23,11 +23,6 @@ namespace Spectre.Console
|
||||
/// <inheritdoc/>
|
||||
public Color? BorderColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alignment of the panel contents.
|
||||
/// </summary>
|
||||
public Justify? Alignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not the panel should
|
||||
/// fit the available space. If <c>false</c>, the panel width will be
|
||||
@ -94,8 +89,7 @@ namespace Spectre.Console
|
||||
};
|
||||
|
||||
// Render the child.
|
||||
var childContext = context.WithJustification(Alignment);
|
||||
var childSegments = _child.Render(childContext, childWidth);
|
||||
var childSegments = _child.Render(context, childWidth);
|
||||
|
||||
// Split the child segments into lines.
|
||||
foreach (var line in Segment.SplitLines(childSegments, panelWidth))
|
||||
|
256
src/Spectre.Console/Rendering/Paragraph.cs
Normal file
256
src/Spectre.Console/Rendering/Paragraph.cs
Normal file
@ -0,0 +1,256 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Internal;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
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
|
||||
{
|
||||
private readonly List<SegmentLine> _lines;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alignment of the whole paragraph.
|
||||
/// </summary>
|
||||
public Justify? Alignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Paragraph"/> class.
|
||||
/// </summary>
|
||||
public Paragraph()
|
||||
{
|
||||
_lines = new List<SegmentLine>();
|
||||
}
|
||||
|
||||
/// <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.</param>
|
||||
public Paragraph(string text, Style? style = null)
|
||||
: this()
|
||||
{
|
||||
if (text is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
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.</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));
|
||||
}
|
||||
|
||||
foreach (var (_, first, last, part) in text.SplitLines().Enumerate())
|
||||
{
|
||||
var current = part;
|
||||
|
||||
if (first)
|
||||
{
|
||||
var line = _lines.LastOrDefault();
|
||||
if (line == null)
|
||||
{
|
||||
_lines.Add(new SegmentLine());
|
||||
line = _lines.Last();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(current))
|
||||
{
|
||||
line.Add(Segment.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var span in current.SplitWords())
|
||||
{
|
||||
line.Add(new Segment(span, style ?? Style.Plain));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var line = new SegmentLine();
|
||||
|
||||
if (string.IsNullOrEmpty(current))
|
||||
{
|
||||
line.Add(Segment.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var span in current.SplitWords())
|
||||
{
|
||||
line.Add(new Segment(span, style ?? Style.Plain));
|
||||
}
|
||||
}
|
||||
|
||||
_lines.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
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.CellLength(context.Encoding)));
|
||||
var max = _lines.Max(x => x.CellWidth(context.Encoding));
|
||||
|
||||
return new Measurement(min, max);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (_lines.Count == 0)
|
||||
{
|
||||
return Array.Empty<Segment>();
|
||||
}
|
||||
|
||||
var lines = SplitLines(context, maxWidth);
|
||||
|
||||
// Justify lines
|
||||
var justification = context.Justification ?? Alignment ?? Justify.Left;
|
||||
foreach (var (_, _, last, line) in lines.Enumerate())
|
||||
{
|
||||
var length = line.Sum(l => l.StripLineEndings().CellLength(context.Encoding));
|
||||
if (length < maxWidth)
|
||||
{
|
||||
// Justify right side
|
||||
if (justification == Justify.Right)
|
||||
{
|
||||
var diff = maxWidth - length;
|
||||
line.Prepend(new Segment(new string(' ', diff)));
|
||||
}
|
||||
else if (justification == Justify.Center)
|
||||
{
|
||||
// Left side.
|
||||
var diff = (maxWidth - length) / 2;
|
||||
line.Prepend(new Segment(new string(' ', diff)));
|
||||
|
||||
// Right side
|
||||
line.Add(new Segment(new string(' ', diff)));
|
||||
var remainder = (maxWidth - length) % 2;
|
||||
if (remainder != 0)
|
||||
{
|
||||
line.Add(new Segment(new string(' ', remainder)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new SegmentLineEnumerator(lines);
|
||||
}
|
||||
|
||||
private List<SegmentLine> Clone()
|
||||
{
|
||||
var result = new List<SegmentLine>();
|
||||
|
||||
foreach (var line in _lines)
|
||||
{
|
||||
var newLine = new SegmentLine();
|
||||
foreach (var segment in line)
|
||||
{
|
||||
newLine.Add(segment);
|
||||
}
|
||||
|
||||
result.Add(newLine);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<SegmentLine> SplitLines(RenderContext context, int maxWidth)
|
||||
{
|
||||
if (_lines.Max(x => x.CellWidth(context.Encoding)) <= maxWidth)
|
||||
{
|
||||
return Clone();
|
||||
}
|
||||
|
||||
var lines = new List<SegmentLine>();
|
||||
var line = new SegmentLine();
|
||||
|
||||
var newLine = true;
|
||||
using (var iterator = new SegmentLineIterator(_lines))
|
||||
{
|
||||
while (iterator.MoveNext())
|
||||
{
|
||||
var current = iterator.Current;
|
||||
if (current == null)
|
||||
{
|
||||
throw new InvalidOperationException("Iterator returned empty segment.");
|
||||
}
|
||||
|
||||
if (newLine && current.IsWhiteSpace && !current.IsLineBreak)
|
||||
{
|
||||
newLine = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
newLine = false;
|
||||
|
||||
if (current.IsLineBreak)
|
||||
{
|
||||
line.Add(current);
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
newLine = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
var length = current.CellLength(context.Encoding);
|
||||
if (line.CellWidth(context.Encoding) + length > maxWidth)
|
||||
{
|
||||
line.Add(Segment.Empty);
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
newLine = true;
|
||||
}
|
||||
|
||||
if (newLine && current.IsWhiteSpace)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
newLine = false;
|
||||
|
||||
line.Add(current);
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining.
|
||||
if (line.Count > 0)
|
||||
{
|
||||
lines.Add(line);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ namespace Spectre.Console
|
||||
/// <summary>
|
||||
/// A renderable table.
|
||||
/// </summary>
|
||||
public sealed partial class Table : Renderable, IHasBorder
|
||||
public sealed partial class Table : Renderable, IHasBorder, IExpandable
|
||||
{
|
||||
private readonly List<TableColumn> _columns;
|
||||
private readonly List<List<IRenderable>> _rows;
|
||||
|
@ -6,7 +6,7 @@ namespace Spectre.Console
|
||||
/// <summary>
|
||||
/// Represents a table column.
|
||||
/// </summary>
|
||||
public sealed class TableColumn : IAlignable
|
||||
public sealed class TableColumn : IColumn
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the text associated with the column.
|
||||
|
@ -74,5 +74,54 @@ namespace Spectre.Console
|
||||
|
||||
table.AddRow(columns.Select(column => new Markup(column)).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the table width.
|
||||
/// </summary>
|
||||
/// <param name="table">The table.</param>
|
||||
/// <param name="width">The width.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static Table SetWidth(this Table table, int width)
|
||||
{
|
||||
if (table is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(table));
|
||||
}
|
||||
|
||||
table.Width = width;
|
||||
return table;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows table headers.
|
||||
/// </summary>
|
||||
/// <param name="table">The table.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static Table ShowHeaders(this Table table)
|
||||
{
|
||||
if (table is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(table));
|
||||
}
|
||||
|
||||
table.ShowHeaders = true;
|
||||
return table;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides table headers.
|
||||
/// </summary>
|
||||
/// <param name="table">The table.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static Table HideHeaders(this Table table)
|
||||
{
|
||||
if (table is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(table));
|
||||
}
|
||||
|
||||
table.ShowHeaders = false;
|
||||
return table;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Internal;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
@ -15,258 +12,42 @@ namespace Spectre.Console
|
||||
[SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
|
||||
public sealed class Text : Renderable, IAlignable
|
||||
{
|
||||
private readonly List<SegmentLine> _lines;
|
||||
private readonly Paragraph _paragraph;
|
||||
|
||||
/// <summary>
|
||||
/// Gets an empty <see cref="Text"/> instance.
|
||||
/// </summary>
|
||||
public static Text Empty { get; } = new Text(string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text alignment.
|
||||
/// </summary>
|
||||
public Justify? Alignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Text"/> class.
|
||||
/// </summary>
|
||||
public Text()
|
||||
{
|
||||
_lines = new List<SegmentLine>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Text"/> class.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="style">The style of the text.</param>
|
||||
public Text(string text, Style? style = null)
|
||||
: this()
|
||||
{
|
||||
if (text is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
Append(text, style);
|
||||
_paragraph = new Paragraph(text, style);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Text"/> instance representing
|
||||
/// the specified markup text.
|
||||
/// Gets or sets the text alignment.
|
||||
/// </summary>
|
||||
/// <param name="text">The markup text.</param>
|
||||
/// <param name="style">The text style.</param>
|
||||
/// <returns>a <see cref="Text"/> instance representing the specified markup text.</returns>
|
||||
public static Text Markup(string text, Style? style = null)
|
||||
public Justify? Alignment
|
||||
{
|
||||
var result = MarkupParser.Parse(text, style ?? Style.Plain);
|
||||
return result;
|
||||
get => _paragraph.Alignment;
|
||||
set => _paragraph.Alignment = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Measurement Measure(RenderContext context, int maxWidth)
|
||||
{
|
||||
if (_lines.Count == 0)
|
||||
{
|
||||
return new Measurement(0, 0);
|
||||
}
|
||||
|
||||
var min = _lines.Max(line => line.Max(segment => segment.CellLength(context.Encoding)));
|
||||
var max = _lines.Max(x => x.CellWidth(context.Encoding));
|
||||
|
||||
return new Measurement(min, max);
|
||||
return ((IRenderable)_paragraph).Measure(context, 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 = SplitLines(context, maxWidth);
|
||||
|
||||
// Justify lines
|
||||
var justification = context.Justification ?? Alignment ?? Justify.Left;
|
||||
foreach (var (_, _, last, line) in lines.Enumerate())
|
||||
{
|
||||
var length = line.Sum(l => l.StripLineEndings().CellLength(context.Encoding));
|
||||
if (length < maxWidth)
|
||||
{
|
||||
// Justify right side
|
||||
if (justification == Justify.Right)
|
||||
{
|
||||
var diff = maxWidth - length;
|
||||
line.Prepend(new Segment(new string(' ', diff)));
|
||||
}
|
||||
else if (justification == Justify.Center)
|
||||
{
|
||||
// Left side.
|
||||
var diff = (maxWidth - length) / 2;
|
||||
line.Prepend(new Segment(new string(' ', diff)));
|
||||
|
||||
// Right side
|
||||
line.Add(new Segment(new string(' ', diff)));
|
||||
var remainder = (maxWidth - length) % 2;
|
||||
if (remainder != 0)
|
||||
{
|
||||
line.Add(new Segment(new string(' ', remainder)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new SegmentLineEnumerator(lines);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a piece of text.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to append.</param>
|
||||
/// <param name="style">The style of the appended text.</param>
|
||||
public void Append(string text, Style? style = null)
|
||||
{
|
||||
if (text is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
foreach (var (_, first, last, part) in text.SplitLines().Enumerate())
|
||||
{
|
||||
var current = part;
|
||||
|
||||
if (first)
|
||||
{
|
||||
var line = _lines.LastOrDefault();
|
||||
if (line == null)
|
||||
{
|
||||
_lines.Add(new SegmentLine());
|
||||
line = _lines.Last();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(current))
|
||||
{
|
||||
line.Add(Segment.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var span in current.SplitWords())
|
||||
{
|
||||
line.Add(new Segment(span, style ?? Style.Plain));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var line = new SegmentLine();
|
||||
|
||||
if (string.IsNullOrEmpty(current))
|
||||
{
|
||||
line.Add(Segment.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var span in current.SplitWords())
|
||||
{
|
||||
line.Add(new Segment(span, style ?? Style.Plain));
|
||||
}
|
||||
}
|
||||
|
||||
_lines.Add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<SegmentLine> Clone()
|
||||
{
|
||||
var result = new List<SegmentLine>();
|
||||
|
||||
foreach (var line in _lines)
|
||||
{
|
||||
var newLine = new SegmentLine();
|
||||
foreach (var segment in line)
|
||||
{
|
||||
newLine.Add(segment);
|
||||
}
|
||||
|
||||
result.Add(newLine);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<SegmentLine> SplitLines(RenderContext context, int maxWidth)
|
||||
{
|
||||
if (_lines.Max(x => x.CellWidth(context.Encoding)) <= maxWidth)
|
||||
{
|
||||
return Clone();
|
||||
}
|
||||
|
||||
var lines = new List<SegmentLine>();
|
||||
var line = new SegmentLine();
|
||||
|
||||
var newLine = true;
|
||||
using (var iterator = new SegmentLineIterator(_lines))
|
||||
{
|
||||
while (iterator.MoveNext())
|
||||
{
|
||||
var current = iterator.Current;
|
||||
if (current == null)
|
||||
{
|
||||
throw new InvalidOperationException("Iterator returned empty segment.");
|
||||
}
|
||||
|
||||
if (newLine && current.IsWhiteSpace && !current.IsLineBreak)
|
||||
{
|
||||
newLine = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
newLine = false;
|
||||
|
||||
if (current.IsLineBreak)
|
||||
{
|
||||
line.Add(current);
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
newLine = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
var length = current.CellLength(context.Encoding);
|
||||
if (line.CellWidth(context.Encoding) + length > maxWidth)
|
||||
{
|
||||
line.Add(Segment.Empty);
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
newLine = true;
|
||||
}
|
||||
|
||||
if (newLine && current.IsWhiteSpace)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
newLine = false;
|
||||
|
||||
line.Add(current);
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining.
|
||||
if (line.Count > 0)
|
||||
{
|
||||
lines.Add(line);
|
||||
}
|
||||
|
||||
return lines;
|
||||
return ((IRenderable)_paragraph).Render(context, maxWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ namespace Spectre.Console
|
||||
/// <typeparam name="T">The alignable object type.</typeparam>
|
||||
/// <param name="obj">The alignable object.</param>
|
||||
/// <param name="alignment">The alignment.</param>
|
||||
/// <returns>The same <see cref="IAlignable"/> instance.</returns>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static T SetAlignment<T>(this T obj, Justify alignment)
|
||||
where T : class, IAlignable
|
||||
{
|
||||
@ -29,7 +29,7 @@ namespace Spectre.Console
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The alignable type.</typeparam>
|
||||
/// <param name="obj">The alignable object.</param>
|
||||
/// <returns>The same <see cref="IAlignable"/> instance.</returns>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static T LeftAligned<T>(this T obj)
|
||||
where T : class, IAlignable
|
||||
{
|
||||
@ -47,7 +47,7 @@ namespace Spectre.Console
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The alignable type.</typeparam>
|
||||
/// <param name="obj">The alignable object.</param>
|
||||
/// <returns>The same <see cref="IAlignable"/> instance.</returns>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static T Centered<T>(this T obj)
|
||||
where T : class, IAlignable
|
||||
{
|
||||
@ -65,7 +65,7 @@ namespace Spectre.Console
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The alignable type.</typeparam>
|
||||
/// <param name="obj">The alignable object.</param>
|
||||
/// <returns>The same <see cref="IAlignable"/> instance.</returns>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static T RightAligned<T>(this T obj)
|
||||
where T : class, IAlignable
|
||||
{
|
@ -13,7 +13,7 @@ namespace Spectre.Console.Rendering
|
||||
/// <typeparam name="T">The object that has a border.</typeparam>
|
||||
/// <param name="obj">The object to set the border for.</param>
|
||||
/// <param name="border">The border to use.</param>
|
||||
/// <returns>The same <see cref="IHasBorder"/> instance.</returns>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static T SetBorder<T>(this T obj, BorderKind border)
|
||||
where T : class, IHasBorder
|
||||
{
|
||||
@ -31,7 +31,7 @@ namespace Spectre.Console.Rendering
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The object that has a border.</typeparam>
|
||||
/// <param name="obj">The object to set the border for.</param>
|
||||
/// <returns>The same <see cref="IHasBorder"/> instance.</returns>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static T NoSafeBorder<T>(this T obj)
|
||||
where T : class, IHasBorder
|
||||
{
|
||||
@ -50,7 +50,7 @@ namespace Spectre.Console.Rendering
|
||||
/// <typeparam name="T">The object that has a border.</typeparam>
|
||||
/// <param name="obj">The object to set the border color for.</param>
|
||||
/// <param name="color">The color to set.</param>
|
||||
/// <returns>The same <see cref="IHasBorder"/> instance.</returns>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static T SetBorderColor<T>(this T obj, Color color)
|
||||
where T : class, IHasBorder
|
||||
{
|
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="IColumn"/>.
|
||||
/// </summary>
|
||||
public static class ColumnExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Prevents a column from wrapping.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">An object implementing <see cref="IColumn"/>.</typeparam>
|
||||
/// <param name="obj">The column.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static T NoWrap<T>(this T obj)
|
||||
where T : class, IColumn
|
||||
{
|
||||
if (obj is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
}
|
||||
|
||||
obj.NoWrap = true;
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
using System;
|
||||
|
||||
namespace Spectre.Console.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="IExpandable"/>.
|
||||
/// </summary>
|
||||
public static class ExpandableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Tells the specified object to not expand to the available area
|
||||
/// but take as little space as possible.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The expandable object.</typeparam>
|
||||
/// <param name="obj">The object to collapse.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static T Collapse<T>(this T obj)
|
||||
where T : class, IExpandable
|
||||
{
|
||||
SetExpand<T>(obj, false);
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tells the specified object to expand to the available area.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The expandable object.</typeparam>
|
||||
/// <param name="obj">The object to expand.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static T Expand<T>(this T obj)
|
||||
where T : class, IExpandable
|
||||
{
|
||||
SetExpand<T>(obj, true);
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static void SetExpand<T>(T obj, bool value)
|
||||
where T : class, IExpandable
|
||||
{
|
||||
if (obj is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
}
|
||||
|
||||
obj.Expand = value;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
using System;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="IPaddable"/>.
|
||||
/// </summary>
|
||||
public static class PaddableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the left padding.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">An object implementing <see cref="IPaddable"/>.</typeparam>
|
||||
/// <param name="obj">The paddable object instance.</param>
|
||||
/// <param name="padding">The left padding to apply.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static T PadLeft<T>(this T obj, int padding)
|
||||
where T : class, IPaddable
|
||||
{
|
||||
if (obj is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
}
|
||||
|
||||
return SetPadding(obj, new Padding(padding, obj.Padding.Right));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the right padding.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">An object implementing <see cref="IPaddable"/>.</typeparam>
|
||||
/// <param name="obj">The paddable object instance.</param>
|
||||
/// <param name="padding">The right padding to apply.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static T PadRight<T>(this T obj, int padding)
|
||||
where T : class, IPaddable
|
||||
{
|
||||
if (obj is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
}
|
||||
|
||||
return SetPadding(obj, new Padding(obj.Padding.Left, padding));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the left and right padding.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">An object implementing <see cref="IPaddable"/>.</typeparam>
|
||||
/// <param name="obj">The paddable object instance.</param>
|
||||
/// <param name="left">The left padding to apply.</param>
|
||||
/// <param name="right">The right padding to apply.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static T SetPadding<T>(this T obj, int left, int right)
|
||||
where T : class, IPaddable
|
||||
{
|
||||
return SetPadding(obj, new Padding(left, right));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the padding.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">An object implementing <see cref="IPaddable"/>.</typeparam>
|
||||
/// <param name="obj">The paddable object instance.</param>
|
||||
/// <param name="padding">The padding to apply.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static T SetPadding<T>(this T obj, Padding padding)
|
||||
where T : class, IPaddable
|
||||
{
|
||||
if (obj is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
}
|
||||
|
||||
obj.Padding = padding;
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
14
src/Spectre.Console/Rendering/Traits/IColumn.cs
Normal file
14
src/Spectre.Console/Rendering/Traits/IColumn.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a column.
|
||||
/// </summary>
|
||||
public interface IColumn : IAlignable, IPaddable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether
|
||||
/// or not wrapping should be prevented.
|
||||
/// </summary>
|
||||
bool NoWrap { get; set; }
|
||||
}
|
||||
}
|
15
src/Spectre.Console/Rendering/Traits/IExpandable.cs
Normal file
15
src/Spectre.Console/Rendering/Traits/IExpandable.cs
Normal file
@ -0,0 +1,15 @@
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents something that is expandable.
|
||||
/// </summary>
|
||||
public interface IExpandable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not the object should
|
||||
/// expand to the available space. If <c>false</c>, the object's
|
||||
/// width will be auto calculated.
|
||||
/// </summary>
|
||||
bool Expand { get; set; }
|
||||
}
|
||||
}
|
13
src/Spectre.Console/Rendering/Traits/IPaddable.cs
Normal file
13
src/Spectre.Console/Rendering/Traits/IPaddable.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents something that is paddable.
|
||||
/// </summary>
|
||||
public interface IPaddable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the padding.
|
||||
/// </summary>
|
||||
public Padding Padding { get; set; }
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user