diff --git a/src/Spectre.Console.Tests/Unit/TextTests.cs b/src/Spectre.Console.Tests/Unit/TextTests.cs
index ef9803c..09f1141 100644
--- a/src/Spectre.Console.Tests/Unit/TextTests.cs
+++ b/src/Spectre.Console.Tests/Unit/TextTests.cs
@@ -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);
+ }
}
}
diff --git a/src/Spectre.Console/Rendering/Markup.cs b/src/Spectre.Console/Rendering/Markup.cs
index 9dcc60e..c798016 100644
--- a/src/Spectre.Console/Rendering/Markup.cs
+++ b/src/Spectre.Console/Rendering/Markup.cs
@@ -7,7 +7,7 @@ namespace Spectre.Console
///
/// A renderable piece of markup text.
///
- 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;
}
+ ///
+ public Overflow? Overflow
+ {
+ get => _paragraph.Overflow;
+ set => _paragraph.Overflow = value;
+ }
+
///
/// Initializes a new instance of the class.
///
diff --git a/src/Spectre.Console/Rendering/Overflow.cs b/src/Spectre.Console/Rendering/Overflow.cs
new file mode 100644
index 0000000..0621d2d
--- /dev/null
+++ b/src/Spectre.Console/Rendering/Overflow.cs
@@ -0,0 +1,24 @@
+namespace Spectre.Console
+{
+ ///
+ /// Represents text overflow.
+ ///
+ public enum Overflow
+ {
+ ///
+ /// Put any excess characters on the next line.
+ ///
+ Fold = 0,
+
+ ///
+ /// Truncates the text at the end of the line.
+ ///
+ Crop = 1,
+
+ ///
+ /// Truncates the text at the end of the line and
+ /// also inserts an ellipsis character.
+ ///
+ Ellipsis = 2,
+ }
+}
diff --git a/src/Spectre.Console/Rendering/Paragraph.cs b/src/Spectre.Console/Rendering/Paragraph.cs
index aa97487..8167a6f 100644
--- a/src/Spectre.Console/Rendering/Paragraph.cs
+++ b/src/Spectre.Console/Rendering/Paragraph.cs
@@ -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.
///
[DebuggerDisplay("{_text,nq}")]
- public sealed class Paragraph : Renderable, IAlignable
+ public sealed class Paragraph : Renderable, IAlignable, IOverflowable
{
private readonly List _lines;
@@ -21,6 +21,11 @@ namespace Spectre.Console
///
public Justify? Alignment { get; set; }
+ ///
+ /// Gets or sets the text overflow strategy.
+ ///
+ public Overflow? Overflow { get; set; }
+
///
/// Initializes a new instance of the class.
///
@@ -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();
+ 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.
diff --git a/src/Spectre.Console/Rendering/Segment.cs b/src/Spectre.Console/Rendering/Segment.cs
index 2d93867..b398595 100644
--- a/src/Spectre.Console/Rendering/Segment.cs
+++ b/src/Spectre.Console/Rendering/Segment.cs
@@ -261,6 +261,57 @@ namespace Spectre.Console.Rendering
return result;
}
+ ///
+ /// Splits an overflowing segment into several new segments.
+ ///
+ /// The segment to split.
+ /// The overflow strategy to use.
+ /// The encodign to use.
+ /// The maxiumum width.
+ /// A list of segments that has been split.
+ public static List 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(1) { segment };
+ }
+
+ // Default to folding
+ overflow ??= Overflow.Fold;
+
+ var result = new List();
+
+ 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> MakeSameHeight(int cellHeight, List> cells)
{
foreach (var cell in cells)
diff --git a/src/Spectre.Console/Rendering/Text.cs b/src/Spectre.Console/Rendering/Text.cs
index 9a7edaa..bb60f5e 100644
--- a/src/Spectre.Console/Rendering/Text.cs
+++ b/src/Spectre.Console/Rendering/Text.cs
@@ -10,7 +10,7 @@ namespace Spectre.Console
///
[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;
}
+ ///
+ /// Gets or sets the text overflow strategy.
+ ///
+ public Overflow? Overflow
+ {
+ get => _paragraph.Overflow;
+ set => _paragraph.Overflow = value;
+ }
+
///
protected override Measurement Measure(RenderContext context, int maxWidth)
{
diff --git a/src/Spectre.Console/Rendering/Traits/Extensions/OverflowableExtensions.cs b/src/Spectre.Console/Rendering/Traits/Extensions/OverflowableExtensions.cs
new file mode 100644
index 0000000..2efc0b7
--- /dev/null
+++ b/src/Spectre.Console/Rendering/Traits/Extensions/OverflowableExtensions.cs
@@ -0,0 +1,80 @@
+using System;
+
+namespace Spectre.Console
+{
+ ///
+ /// Contains extension methods for .
+ ///
+ public static class OverflowableExtensions
+ {
+ ///
+ /// Folds any overflowing text.
+ ///
+ /// An object implementing .
+ /// The overflowable object instance.
+ /// The same instance so that multiple calls can be chained.
+ public static T Fold(this T obj)
+ where T : class, IOverflowable
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ return SetOverflow(obj, Overflow.Fold);
+ }
+
+ ///
+ /// Crops any overflowing text.
+ ///
+ /// An object implementing .
+ /// The overflowable object instance.
+ /// The same instance so that multiple calls can be chained.
+ public static T Crop(this T obj)
+ where T : class, IOverflowable
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ return SetOverflow(obj, Overflow.Crop);
+ }
+
+ ///
+ /// Crops any overflowing text and adds an ellipsis to the end.
+ ///
+ /// An object implementing .
+ /// The overflowable object instance.
+ /// The same instance so that multiple calls can be chained.
+ public static T Ellipsis(this T obj)
+ where T : class, IOverflowable
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ return SetOverflow(obj, Overflow.Ellipsis);
+ }
+
+ ///
+ /// Sets the overflow strategy.
+ ///
+ /// An object implementing .
+ /// The overflowable object instance.
+ /// The overflow strategy to use.
+ /// The same instance so that multiple calls can be chained.
+ public static T SetOverflow(this T obj, Overflow overflow)
+ where T : class, IOverflowable
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ obj.Overflow = overflow;
+ return obj;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Rendering/Traits/IOverflowable.cs b/src/Spectre.Console/Rendering/Traits/IOverflowable.cs
new file mode 100644
index 0000000..1807a62
--- /dev/null
+++ b/src/Spectre.Console/Rendering/Traits/IOverflowable.cs
@@ -0,0 +1,13 @@
+namespace Spectre.Console
+{
+ ///
+ /// Represents something that can overflow.
+ ///
+ public interface IOverflowable
+ {
+ ///
+ /// Gets or sets the text overflow strategy.
+ ///
+ Overflow? Overflow { get; set; }
+ }
+}