Add text overflow support

Closes #61
This commit is contained in:
Patrik Svensson 2020-09-03 14:34:13 +02:00 committed by Patrik Svensson
parent 88edfe68ec
commit 9f8ca6d648
8 changed files with 282 additions and 31 deletions

View File

@ -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);
}
}
}

View File

@ -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>

View 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,
}
}

View File

@ -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.

View File

@ -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)

View File

@ -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)
{

View File

@ -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;
}
}
}

View 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; }
}
}