mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-24 04:02:50 +08:00
parent
88edfe68ec
commit
9f8ca6d648
@ -83,5 +83,25 @@ namespace Spectre.Console.Tests.Unit
|
|||||||
.NormalizeLineEndings()
|
.NormalizeLineEndings()
|
||||||
.ShouldBe(expected);
|
.ShouldBe(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(Overflow.Fold, "foo \npneumonoultram\nicroscopicsili\ncovolcanoconio\nsis bar qux")]
|
||||||
|
[InlineData(Overflow.Crop, "foo \npneumonoultram\nbar qux")]
|
||||||
|
[InlineData(Overflow.Ellipsis, "foo \npneumonoultra…\nbar qux")]
|
||||||
|
public void Should_Overflow_Text_Correctly(Overflow overflow, string expected)
|
||||||
|
{
|
||||||
|
// Given
|
||||||
|
var fixture = new PlainConsole(14);
|
||||||
|
var text = new Text("foo pneumonoultramicroscopicsilicovolcanoconiosis bar qux")
|
||||||
|
.SetOverflow(overflow);
|
||||||
|
|
||||||
|
// When
|
||||||
|
fixture.Render(text);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
fixture.Output
|
||||||
|
.NormalizeLineEndings()
|
||||||
|
.ShouldBe(expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ namespace Spectre.Console
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A renderable piece of markup text.
|
/// A renderable piece of markup text.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class Markup : Renderable, IAlignable
|
public sealed class Markup : Renderable, IAlignable, IOverflowable
|
||||||
{
|
{
|
||||||
private readonly Paragraph _paragraph;
|
private readonly Paragraph _paragraph;
|
||||||
|
|
||||||
@ -18,6 +18,13 @@ namespace Spectre.Console
|
|||||||
set => _paragraph.Alignment = value;
|
set => _paragraph.Alignment = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public Overflow? Overflow
|
||||||
|
{
|
||||||
|
get => _paragraph.Overflow;
|
||||||
|
set => _paragraph.Overflow = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Markup"/> class.
|
/// Initializes a new instance of the <see cref="Markup"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
24
src/Spectre.Console/Rendering/Overflow.cs
Normal file
24
src/Spectre.Console/Rendering/Overflow.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
namespace Spectre.Console
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents text overflow.
|
||||||
|
/// </summary>
|
||||||
|
public enum Overflow
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Put any excess characters on the next line.
|
||||||
|
/// </summary>
|
||||||
|
Fold = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Truncates the text at the end of the line.
|
||||||
|
/// </summary>
|
||||||
|
Crop = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Truncates the text at the end of the line and
|
||||||
|
/// also inserts an ellipsis character.
|
||||||
|
/// </summary>
|
||||||
|
Ellipsis = 2,
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -12,7 +12,7 @@ namespace Spectre.Console
|
|||||||
/// of the paragraph can have individual styling.
|
/// of the paragraph can have individual styling.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DebuggerDisplay("{_text,nq}")]
|
[DebuggerDisplay("{_text,nq}")]
|
||||||
public sealed class Paragraph : Renderable, IAlignable
|
public sealed class Paragraph : Renderable, IAlignable, IOverflowable
|
||||||
{
|
{
|
||||||
private readonly List<SegmentLine> _lines;
|
private readonly List<SegmentLine> _lines;
|
||||||
|
|
||||||
@ -21,6 +21,11 @@ namespace Spectre.Console
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Justify? Alignment { get; set; }
|
public Justify? Alignment { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the text overflow strategy.
|
||||||
|
/// </summary>
|
||||||
|
public Overflow? Overflow { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Paragraph"/> class.
|
/// Initializes a new instance of the <see cref="Paragraph"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -197,11 +202,26 @@ namespace Spectre.Console
|
|||||||
var line = new SegmentLine();
|
var line = new SegmentLine();
|
||||||
|
|
||||||
var newLine = true;
|
var newLine = true;
|
||||||
using (var iterator = new SegmentLineIterator(_lines))
|
|
||||||
|
using var iterator = new SegmentLineIterator(_lines);
|
||||||
|
var queue = new Queue<Segment>();
|
||||||
|
while (true)
|
||||||
{
|
{
|
||||||
while (iterator.MoveNext())
|
var current = (Segment?)null;
|
||||||
|
if (queue.Count == 0)
|
||||||
{
|
{
|
||||||
var current = iterator.Current;
|
if (!iterator.MoveNext())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = iterator.Current;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
current = queue.Dequeue();
|
||||||
|
}
|
||||||
|
|
||||||
if (current == null)
|
if (current == null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Iterator returned empty segment.");
|
throw new InvalidOperationException("Iterator returned empty segment.");
|
||||||
@ -225,6 +245,33 @@ namespace Spectre.Console
|
|||||||
}
|
}
|
||||||
|
|
||||||
var length = current.CellLength(context.Encoding);
|
var length = current.CellLength(context.Encoding);
|
||||||
|
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, context.Encoding, maxWidth);
|
||||||
|
if (segments.Count > 0)
|
||||||
|
{
|
||||||
|
if (line.CellWidth(context.Encoding) + segments[0].CellLength(context.Encoding) > 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
if (line.CellWidth(context.Encoding) + length > maxWidth)
|
if (line.CellWidth(context.Encoding) + length > maxWidth)
|
||||||
{
|
{
|
||||||
line.Add(Segment.Empty);
|
line.Add(Segment.Empty);
|
||||||
@ -232,6 +279,7 @@ namespace Spectre.Console
|
|||||||
line = new SegmentLine();
|
line = new SegmentLine();
|
||||||
newLine = true;
|
newLine = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (newLine && current.IsWhiteSpace)
|
if (newLine && current.IsWhiteSpace)
|
||||||
{
|
{
|
||||||
@ -242,7 +290,6 @@ namespace Spectre.Console
|
|||||||
|
|
||||||
line.Add(current);
|
line.Add(current);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Flush remaining.
|
// Flush remaining.
|
||||||
if (line.Count > 0)
|
if (line.Count > 0)
|
||||||
|
@ -261,6 +261,57 @@ namespace Spectre.Console.Rendering
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Splits an overflowing segment into several new segments.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="segment">The segment to split.</param>
|
||||||
|
/// <param name="overflow">The overflow strategy to use.</param>
|
||||||
|
/// <param name="encoding">The encodign to use.</param>
|
||||||
|
/// <param name="width">The maxiumum width.</param>
|
||||||
|
/// <returns>A list of segments that has been split.</returns>
|
||||||
|
public static List<Segment> SplitOverflow(Segment segment, Overflow? overflow, Encoding encoding, int width)
|
||||||
|
{
|
||||||
|
if (segment is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(segment));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment.CellLength(encoding) <= width)
|
||||||
|
{
|
||||||
|
return new List<Segment>(1) { segment };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to folding
|
||||||
|
overflow ??= Overflow.Fold;
|
||||||
|
|
||||||
|
var result = new List<Segment>();
|
||||||
|
|
||||||
|
if (overflow == Overflow.Fold)
|
||||||
|
{
|
||||||
|
var totalLength = segment.Text.CellLength(encoding);
|
||||||
|
var lengthLeft = totalLength;
|
||||||
|
while (lengthLeft > 0)
|
||||||
|
{
|
||||||
|
var index = totalLength - lengthLeft;
|
||||||
|
var take = Math.Min(width, totalLength - index);
|
||||||
|
|
||||||
|
result.Add(new Segment(segment.Text.Substring(index, take), segment.Style));
|
||||||
|
lengthLeft -= take;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (overflow == Overflow.Crop)
|
||||||
|
{
|
||||||
|
result.Add(new Segment(segment.Text.Substring(0, width), segment.Style));
|
||||||
|
}
|
||||||
|
else if (overflow == Overflow.Ellipsis)
|
||||||
|
{
|
||||||
|
result.Add(new Segment(segment.Text.Substring(0, width - 1), segment.Style));
|
||||||
|
result.Add(new Segment("…", segment.Style));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
internal static List<List<SegmentLine>> MakeSameHeight(int cellHeight, List<List<SegmentLine>> cells)
|
internal static List<List<SegmentLine>> MakeSameHeight(int cellHeight, List<List<SegmentLine>> cells)
|
||||||
{
|
{
|
||||||
foreach (var cell in cells)
|
foreach (var cell in cells)
|
||||||
|
@ -10,7 +10,7 @@ namespace Spectre.Console
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[DebuggerDisplay("{_text,nq}")]
|
[DebuggerDisplay("{_text,nq}")]
|
||||||
[SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
|
[SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
|
||||||
public sealed class Text : Renderable, IAlignable
|
public sealed class Text : Renderable, IAlignable, IOverflowable
|
||||||
{
|
{
|
||||||
private readonly Paragraph _paragraph;
|
private readonly Paragraph _paragraph;
|
||||||
|
|
||||||
@ -38,6 +38,15 @@ namespace Spectre.Console
|
|||||||
set => _paragraph.Alignment = value;
|
set => _paragraph.Alignment = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the text overflow strategy.
|
||||||
|
/// </summary>
|
||||||
|
public Overflow? Overflow
|
||||||
|
{
|
||||||
|
get => _paragraph.Overflow;
|
||||||
|
set => _paragraph.Overflow = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected override Measurement Measure(RenderContext context, int maxWidth)
|
protected override Measurement Measure(RenderContext context, int maxWidth)
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Spectre.Console
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains extension methods for <see cref="IOverflowable"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static class OverflowableExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Folds any overflowing text.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">An object implementing <see cref="IOverflowable"/>.</typeparam>
|
||||||
|
/// <param name="obj">The overflowable object instance.</param>
|
||||||
|
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||||
|
public static T Fold<T>(this T obj)
|
||||||
|
where T : class, IOverflowable
|
||||||
|
{
|
||||||
|
if (obj is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetOverflow(obj, Overflow.Fold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crops any overflowing text.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">An object implementing <see cref="IOverflowable"/>.</typeparam>
|
||||||
|
/// <param name="obj">The overflowable object instance.</param>
|
||||||
|
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||||
|
public static T Crop<T>(this T obj)
|
||||||
|
where T : class, IOverflowable
|
||||||
|
{
|
||||||
|
if (obj is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetOverflow(obj, Overflow.Crop);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crops any overflowing text and adds an ellipsis to the end.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">An object implementing <see cref="IOverflowable"/>.</typeparam>
|
||||||
|
/// <param name="obj">The overflowable object instance.</param>
|
||||||
|
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||||
|
public static T Ellipsis<T>(this T obj)
|
||||||
|
where T : class, IOverflowable
|
||||||
|
{
|
||||||
|
if (obj is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetOverflow(obj, Overflow.Ellipsis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the overflow strategy.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">An object implementing <see cref="IOverflowable"/>.</typeparam>
|
||||||
|
/// <param name="obj">The overflowable object instance.</param>
|
||||||
|
/// <param name="overflow">The overflow strategy to use.</param>
|
||||||
|
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||||
|
public static T SetOverflow<T>(this T obj, Overflow overflow)
|
||||||
|
where T : class, IOverflowable
|
||||||
|
{
|
||||||
|
if (obj is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.Overflow = overflow;
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
src/Spectre.Console/Rendering/Traits/IOverflowable.cs
Normal file
13
src/Spectre.Console/Rendering/Traits/IOverflowable.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace Spectre.Console
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents something that can overflow.
|
||||||
|
/// </summary>
|
||||||
|
public interface IOverflowable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the text overflow strategy.
|
||||||
|
/// </summary>
|
||||||
|
Overflow? Overflow { get; set; }
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user