mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-16 00:42:51 +08:00
parent
88edfe68ec
commit
9f8ca6d648
@ -83,5 +83,25 @@ namespace Spectre.Console.Tests.Unit
|
||||
.NormalizeLineEndings()
|
||||
.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>
|
||||
/// A renderable piece of markup text.
|
||||
/// </summary>
|
||||
public sealed class Markup : Renderable, IAlignable
|
||||
public sealed class Markup : Renderable, IAlignable, IOverflowable
|
||||
{
|
||||
private readonly Paragraph _paragraph;
|
||||
|
||||
@ -18,6 +18,13 @@ namespace Spectre.Console
|
||||
set => _paragraph.Alignment = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Overflow? Overflow
|
||||
{
|
||||
get => _paragraph.Overflow;
|
||||
set => _paragraph.Overflow = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Markup"/> class.
|
||||
/// </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.Diagnostics;
|
||||
using System.Linq;
|
||||
@ -12,7 +12,7 @@ namespace Spectre.Console
|
||||
/// of the paragraph can have individual styling.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{_text,nq}")]
|
||||
public sealed class Paragraph : Renderable, IAlignable
|
||||
public sealed class Paragraph : Renderable, IAlignable, IOverflowable
|
||||
{
|
||||
private readonly List<SegmentLine> _lines;
|
||||
|
||||
@ -21,6 +21,11 @@ namespace Spectre.Console
|
||||
/// </summary>
|
||||
public Justify? Alignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text overflow strategy.
|
||||
/// </summary>
|
||||
public Overflow? Overflow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Paragraph"/> class.
|
||||
/// </summary>
|
||||
@ -197,34 +202,76 @@ namespace Spectre.Console
|
||||
var line = new SegmentLine();
|
||||
|
||||
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 (current == null)
|
||||
if (!iterator.MoveNext())
|
||||
{
|
||||
throw new InvalidOperationException("Iterator returned empty segment.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (newLine && current.IsWhiteSpace && !current.IsLineBreak)
|
||||
{
|
||||
newLine = false;
|
||||
continue;
|
||||
}
|
||||
current = iterator.Current;
|
||||
}
|
||||
else
|
||||
{
|
||||
current = queue.Dequeue();
|
||||
}
|
||||
|
||||
if (current == null)
|
||||
{
|
||||
throw new InvalidOperationException("Iterator returned empty segment.");
|
||||
}
|
||||
|
||||
if (newLine && current.IsWhiteSpace && !current.IsLineBreak)
|
||||
{
|
||||
newLine = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current.IsLineBreak)
|
||||
newLine = false;
|
||||
|
||||
if (current.IsLineBreak)
|
||||
{
|
||||
line.Add(current);
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
newLine = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
line.Add(current);
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
newLine = true;
|
||||
continue;
|
||||
}
|
||||
if (line.CellWidth(context.Encoding) + segments[0].CellLength(context.Encoding) > maxWidth)
|
||||
{
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
newLine = true;
|
||||
|
||||
var length = current.CellLength(context.Encoding);
|
||||
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)
|
||||
{
|
||||
line.Add(Segment.Empty);
|
||||
@ -232,16 +279,16 @@ namespace Spectre.Console
|
||||
line = new SegmentLine();
|
||||
newLine = true;
|
||||
}
|
||||
|
||||
if (newLine && current.IsWhiteSpace)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
newLine = false;
|
||||
|
||||
line.Add(current);
|
||||
}
|
||||
|
||||
if (newLine && current.IsWhiteSpace)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
newLine = false;
|
||||
|
||||
line.Add(current);
|
||||
}
|
||||
|
||||
// Flush remaining.
|
||||
|
@ -261,6 +261,57 @@ namespace Spectre.Console.Rendering
|
||||
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)
|
||||
{
|
||||
foreach (var cell in cells)
|
||||
|
@ -10,7 +10,7 @@ namespace Spectre.Console
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{_text,nq}")]
|
||||
[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;
|
||||
|
||||
@ -38,6 +38,15 @@ namespace Spectre.Console
|
||||
set => _paragraph.Alignment = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text overflow strategy.
|
||||
/// </summary>
|
||||
public Overflow? Overflow
|
||||
{
|
||||
get => _paragraph.Overflow;
|
||||
set => _paragraph.Overflow = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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