diff --git a/src/Spectre.Console.Tests/Unit/SegmentTests.cs b/src/Spectre.Console.Tests/Unit/SegmentTests.cs index ca8141c..f3cf5f9 100644 --- a/src/Spectre.Console.Tests/Unit/SegmentTests.cs +++ b/src/Spectre.Console.Tests/Unit/SegmentTests.cs @@ -1,3 +1,4 @@ +using System.Text; using Shouldly; using Spectre.Console.Rendering; using Xunit; @@ -6,6 +7,16 @@ namespace Spectre.Console.Tests.Unit { public sealed class SegmentTests { + [Fact] + public void Lol() + { + var context = new RenderContext(Encoding.UTF8, false); + + var result = new Segment(" ").CellCount(context); + + result.ShouldBe(4); + } + public sealed class TheSplitMethod { [Fact] @@ -31,7 +42,10 @@ namespace Spectre.Console.Tests.Unit [Fact] public void Should_Split_Segment() { + var context = new RenderContext(Encoding.UTF8, false); + var lines = Segment.SplitLines( + context, new[] { new Segment("Foo"), @@ -61,7 +75,9 @@ namespace Spectre.Console.Tests.Unit [Fact] public void Should_Split_Segments_With_Linebreak_In_Text() { + var context = new RenderContext(Encoding.UTF8, false); var lines = Segment.SplitLines( + context, new[] { new Segment("Foo\n"), diff --git a/src/Spectre.Console/Internal/Aligner.cs b/src/Spectre.Console/Internal/Aligner.cs index f51e208..4ff8432 100644 --- a/src/Spectre.Console/Internal/Aligner.cs +++ b/src/Spectre.Console/Internal/Aligner.cs @@ -57,7 +57,7 @@ namespace Spectre.Console.Internal return; } - var width = Segment.CellLength(context, segments); + var width = Segment.CellCount(context, segments); if (width >= maxWidth) { return; diff --git a/src/Spectre.Console/Internal/Extensions/StringExtensions.cs b/src/Spectre.Console/Internal/Extensions/StringExtensions.cs index 7e22b9a..28e9b43 100644 --- a/src/Spectre.Console/Internal/Extensions/StringExtensions.cs +++ b/src/Spectre.Console/Internal/Extensions/StringExtensions.cs @@ -16,6 +16,11 @@ namespace Spectre.Console.Internal public static int CellLength(this string text, RenderContext context) { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + return Cell.GetCellLength(context, text); } diff --git a/src/Spectre.Console/Internal/Text/Cell.cs b/src/Spectre.Console/Internal/Text/Cell.cs index dad3a70..2f0d3d8 100644 --- a/src/Spectre.Console/Internal/Text/Cell.cs +++ b/src/Spectre.Console/Internal/Text/Cell.cs @@ -21,6 +21,17 @@ namespace Spectre.Console.Internal } } + // TODO: We need to figure out why Segment.SplitLines fails + // if we let wcwidth (which returns -1 instead of 1) + // calculate the size for new line characters. + // That is correct from a Unicode perspective, but the + // algorithm was written before wcwidth was added and used + // to work with string length and not cell length. + if (rune == '\n') + { + return 1; + } + return UnicodeCalculator.GetWidth(rune); }); } diff --git a/src/Spectre.Console/Rendering/Segment.cs b/src/Spectre.Console/Rendering/Segment.cs index 7b42b20..84b8e95 100644 --- a/src/Spectre.Console/Rendering/Segment.cs +++ b/src/Spectre.Console/Rendering/Segment.cs @@ -79,11 +79,37 @@ namespace Spectre.Console.Rendering /// /// The render context. /// The number of cells that this segment occupies in the console. - public int CellLength(RenderContext context) + public int CellCount(RenderContext context) { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + return Text.CellLength(context); } + /// + /// Gets the number of cells that the segments occupies in the console. + /// + /// The render context. + /// The segments to measure. + /// The number of cells that the segments occupies in the console. + public static int CellCount(RenderContext context, IEnumerable segments) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (segments is null) + { + throw new ArgumentNullException(nameof(segments)); + } + + return segments.Sum(segment => segment.CellCount(context)); + } + /// /// Returns a new segment without any trailing line endings. /// @@ -124,35 +150,41 @@ namespace Spectre.Console.Rendering return new Segment(Text, Style); } - /// - /// Gets the number of cells that the segments occupies in the console. - /// - /// The render context. - /// The segments to measure. - /// The number of cells that the segments occupies in the console. - public static int CellLength(RenderContext context, IEnumerable segments) - { - return segments.Sum(segment => segment.CellLength(context)); - } - /// /// Splits the provided segments into lines. /// + /// The render context. /// The segments to split. /// A collection of lines. - public static List SplitLines(IEnumerable segments) + public static List SplitLines(RenderContext context, IEnumerable segments) { - return SplitLines(segments, int.MaxValue); + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (segments is null) + { + throw new ArgumentNullException(nameof(segments)); + } + + return SplitLines(context, segments, int.MaxValue); } /// /// Splits the provided segments into lines with a maximum width. /// + /// The render context. /// The segments to split into lines. /// The maximum width. /// A list of lines. - public static List SplitLines(IEnumerable segments, int maxWidth) + public static List SplitLines(RenderContext context, IEnumerable segments, int maxWidth) { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + if (segments is null) { throw new ArgumentNullException(nameof(segments)); @@ -167,9 +199,10 @@ namespace Spectre.Console.Rendering { var segment = stack.Pop(); - if (line.Width + segment.Text.Length > maxWidth) + // Does this segment make the line exceed the max width? + if (line.CellCount(context) + segment.CellCount(context) > maxWidth) { - var diff = -(maxWidth - (line.Width + segment.Text.Length)); + var diff = -(maxWidth - (line.Length + segment.Text.Length)); var offset = segment.Text.Length - diff; var (first, second) = segment.Split(offset); @@ -186,11 +219,13 @@ namespace Spectre.Console.Rendering continue; } + // Does the segment contain a newline? if (segment.Text.Contains("\n")) { + // Is it a new line? if (segment.Text == "\n") { - if (line.Width > 0 || segment.IsLineBreak) + if (line.Length != 0 || segment.IsLineBreak) { lines.Add(line); line = new SegmentLine(); @@ -213,7 +248,7 @@ namespace Spectre.Console.Rendering if (parts.Length > 1) { - if (line.Width > 0) + if (line.Length > 0) { lines.Add(line); line = new SegmentLine(); @@ -247,16 +282,21 @@ namespace Spectre.Console.Rendering /// The segment to split. /// The overflow strategy to use. /// The render context. - /// The maximum width. + /// The maximum width. /// A list of segments that has been split. - public static List SplitOverflow(Segment segment, Overflow? overflow, RenderContext context, int width) + public static List SplitOverflow(Segment segment, Overflow? overflow, RenderContext context, int maxWidth) { if (segment is null) { throw new ArgumentNullException(nameof(segment)); } - if (segment.CellLength(context) <= width) + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (segment.CellCount(context) <= maxWidth) { return new List(1) { segment }; } @@ -275,7 +315,7 @@ namespace Spectre.Console.Rendering var index = totalLength - lengthLeft; // How many characters should we take? - var take = Math.Min(width, totalLength - index); + var take = Math.Min(maxWidth, totalLength - index); if (take <= 0) { // This shouldn't really occur, but I don't like @@ -289,24 +329,24 @@ namespace Spectre.Console.Rendering } else if (overflow == Overflow.Crop) { - if (Math.Max(0, width - 1) == 0) + if (Math.Max(0, maxWidth - 1) == 0) { result.Add(new Segment(string.Empty, segment.Style)); } else { - result.Add(new Segment(segment.Text.Substring(0, width), segment.Style)); + result.Add(new Segment(segment.Text.Substring(0, maxWidth), segment.Style)); } } else if (overflow == Overflow.Ellipsis) { - if (Math.Max(0, width - 1) == 0) + if (Math.Max(0, maxWidth - 1) == 0) { result.Add(new Segment("…", segment.Style)); } else { - result.Add(new Segment(segment.Text.Substring(0, width - 1) + "…", segment.Style)); + result.Add(new Segment(segment.Text.Substring(0, maxWidth - 1) + "…", segment.Style)); } } @@ -337,14 +377,14 @@ namespace Spectre.Console.Rendering var totalWidth = 0; foreach (var segment in segments) { - var segmentWidth = segment.CellLength(context); - if (totalWidth + segmentWidth > maxWidth) + var segmentCellWidth = segment.CellCount(context); + if (totalWidth + segmentCellWidth > maxWidth) { break; } result.Add(segment); - totalWidth += segmentWidth; + totalWidth += segmentCellWidth; } if (result.Count == 0 && segments.Any()) @@ -368,12 +408,17 @@ namespace Spectre.Console.Rendering /// A new truncated segment, or null. public static Segment? Truncate(RenderContext context, Segment segment, int maxWidth) { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + if (segment is null) { return null; } - if (segment.CellLength(context) <= maxWidth) + if (segment.CellCount(context) <= maxWidth) { return segment; } @@ -381,7 +426,8 @@ namespace Spectre.Console.Rendering var builder = new StringBuilder(); foreach (var character in segment.Text) { - if (Cell.GetCellLength(context, builder.ToString()) >= maxWidth) + var accumulatedCellWidth = builder.ToString().CellLength(context); + if (accumulatedCellWidth >= maxWidth) { break; } @@ -399,6 +445,11 @@ namespace Spectre.Console.Rendering internal static IEnumerable Merge(IEnumerable segments) { + if (segments is null) + { + throw new ArgumentNullException(nameof(segments)); + } + var result = new List(); var previous = (Segment?)null; @@ -432,6 +483,33 @@ namespace Spectre.Console.Rendering internal static Segment TruncateWithEllipsis(string text, Style style, RenderContext context, int maxWidth) { + if (text is null) + { + throw new ArgumentNullException(nameof(text)); + } + + if (style is null) + { + throw new ArgumentNullException(nameof(style)); + } + + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var overflow = SplitOverflow(new Segment(text, style), Overflow.Ellipsis, context, maxWidth); + if (overflow.Count == 0) + { + if (maxWidth > 0) + { + return new Segment(text, style); + } + + // We got space for an ellipsis + return new Segment("…", style); + } + return SplitOverflow( new Segment(text, style), Overflow.Ellipsis, @@ -441,7 +519,17 @@ namespace Spectre.Console.Rendering internal static List TruncateWithEllipsis(IEnumerable segments, RenderContext context, int maxWidth) { - if (CellLength(context, segments) <= maxWidth) + if (segments is null) + { + throw new ArgumentNullException(nameof(segments)); + } + + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (CellCount(context, segments) <= maxWidth) { return new List(segments); } @@ -459,6 +547,11 @@ namespace Spectre.Console.Rendering internal static List TrimEnd(IEnumerable segments) { + if (segments is null) + { + throw new ArgumentNullException(nameof(segments)); + } + var stack = new Stack(); var checkForWhitespace = true; foreach (var segment in segments.Reverse()) @@ -481,6 +574,11 @@ namespace Spectre.Console.Rendering internal static List> MakeSameHeight(int cellHeight, List> cells) { + if (cells is null) + { + throw new ArgumentNullException(nameof(cells)); + } + foreach (var cell in cells) { if (cell.Count < cellHeight) diff --git a/src/Spectre.Console/Rendering/SegmentLine.cs b/src/Spectre.Console/Rendering/SegmentLine.cs index f1483d6..f643e9d 100644 --- a/src/Spectre.Console/Rendering/SegmentLine.cs +++ b/src/Spectre.Console/Rendering/SegmentLine.cs @@ -13,16 +13,21 @@ namespace Spectre.Console.Rendering /// /// Gets the width of the line. /// - public int Width => this.Sum(line => line.Text.Length); + public int Length => this.Sum(line => line.Text.Length); /// - /// Gets the cell width of the segment line. + /// Gets the number of cells the segment line occupies. /// /// The render context. /// The cell width of the segment line. - public int CellWidth(RenderContext context) + public int CellCount(RenderContext context) { - return this.Sum(line => line.CellLength(context)); + if (context is null) + { + throw new System.ArgumentNullException(nameof(context)); + } + + return Segment.CellCount(context, this); } /// @@ -31,6 +36,11 @@ namespace Spectre.Console.Rendering /// The segment to prepend. public void Prepend(Segment segment) { + if (segment is null) + { + throw new System.ArgumentNullException(nameof(segment)); + } + Insert(0, segment); } } diff --git a/src/Spectre.Console/Widgets/Padder.cs b/src/Spectre.Console/Widgets/Padder.cs index 6e6dff4..bba0a4b 100644 --- a/src/Spectre.Console/Widgets/Padder.cs +++ b/src/Spectre.Console/Widgets/Padder.cs @@ -66,7 +66,7 @@ namespace Spectre.Console } var child = _child.Render(context, maxWidth - paddingWidth); - foreach (var (_, _, _, line) in Segment.SplitLines(child).Enumerate()) + foreach (var (_, _, _, line) in Segment.SplitLines(context, child).Enumerate()) { // Left padding if (Padding.Left != 0) @@ -83,7 +83,7 @@ namespace Spectre.Console } // Missing space on right side? - var lineWidth = line.CellWidth(context); + var lineWidth = line.CellCount(context); var diff = width - lineWidth - Padding.Left - Padding.Right; if (diff > 0) { diff --git a/src/Spectre.Console/Widgets/Panel.cs b/src/Spectre.Console/Widgets/Panel.cs index b5bbe59..1755085 100644 --- a/src/Spectre.Console/Widgets/Panel.cs +++ b/src/Spectre.Console/Widgets/Panel.cs @@ -94,7 +94,7 @@ namespace Spectre.Console // Split the child segments into lines. var childSegments = ((IRenderable)child).Render(context, childWidth); - foreach (var line in Segment.SplitLines(childSegments, childWidth)) + foreach (var line in Segment.SplitLines(context, childSegments, childWidth)) { if (line.Count == 1 && line[0].IsWhiteSpace) { @@ -109,7 +109,7 @@ namespace Spectre.Console content.AddRange(line); // Do we need to pad the panel? - var length = line.Sum(segment => segment.CellLength(context)); + var length = line.Sum(segment => segment.CellCount(context)); if (length < childWidth) { var diff = childWidth - length; @@ -148,7 +148,7 @@ namespace Spectre.Console var headerWidth = panelWidth - (EdgeWidth * 2); var header = Segment.TruncateWithEllipsis(Header.Text, Header.Style ?? borderStyle, context, headerWidth); - var excessWidth = headerWidth - header.CellLength(context); + var excessWidth = headerWidth - header.CellCount(context); if (excessWidth > 0) { switch (Header.Alignment ?? Justify.Left) diff --git a/src/Spectre.Console/Widgets/Paragraph.cs b/src/Spectre.Console/Widgets/Paragraph.cs index a78ed3a..4a6c447 100644 --- a/src/Spectre.Console/Widgets/Paragraph.cs +++ b/src/Spectre.Console/Widgets/Paragraph.cs @@ -119,8 +119,8 @@ namespace Spectre.Console return new Measurement(0, 0); } - var min = _lines.Max(line => line.Max(segment => segment.CellLength(context))); - var max = _lines.Max(x => x.CellWidth(context)); + var min = _lines.Max(line => line.Max(segment => segment.CellCount(context))); + var max = _lines.Max(x => x.CellCount(context)); return new Measurement(min, Math.Min(max, maxWidth)); } @@ -187,7 +187,7 @@ namespace Spectre.Console return new List(); } - if (_lines.Max(x => x.CellWidth(context)) <= maxWidth) + if (_lines.Max(x => x.CellCount(context)) <= maxWidth) { return Clone(); } @@ -231,7 +231,7 @@ namespace Spectre.Console continue; } - var length = current.CellLength(context); + var length = current.CellCount(context); if (length > maxWidth) { // The current segment is longer than the width of the console, @@ -239,7 +239,7 @@ namespace Spectre.Console var segments = Segment.SplitOverflow(current, Overflow, context, maxWidth); if (segments.Count > 0) { - if (line.CellWidth(context) + segments[0].CellLength(context) > maxWidth) + if (line.CellCount(context) + segments[0].CellCount(context) > maxWidth) { lines.Add(line); line = new SegmentLine(); @@ -259,7 +259,7 @@ namespace Spectre.Console } else { - if (line.CellWidth(context) + length > maxWidth) + if (line.CellCount(context) + length > maxWidth) { line.Add(Segment.Empty); lines.Add(line); diff --git a/src/Spectre.Console/Widgets/Rule.cs b/src/Spectre.Console/Widgets/Rule.cs index 1ac04d3..168cbb0 100644 --- a/src/Spectre.Console/Widgets/Rule.cs +++ b/src/Spectre.Console/Widgets/Rule.cs @@ -52,7 +52,7 @@ namespace Spectre.Console // Get the title and make sure it fits. var title = GetTitleSegments(context, Title, maxWidth - 6); - if (Segment.CellLength(context, title) > maxWidth - 6) + if (Segment.CellCount(context, title) > maxWidth - 6) { // Truncate the title title = Segment.TruncateWithEllipsis(title, context, maxWidth - 6); @@ -88,13 +88,13 @@ namespace Spectre.Console { var alignment = Alignment ?? Justify.Center; - var titleLength = Segment.CellLength(context, title); + var titleLength = Segment.CellCount(context, title); if (alignment == Justify.Left) { var left = new Segment(new string('─', 2) + " ", Style ?? Style.Plain); - var rightLength = maxWidth - titleLength - left.CellLength(context) - 1; + var rightLength = maxWidth - titleLength - left.CellCount(context) - 1; var right = new Segment(" " + new string('─', rightLength), Style ?? Style.Plain); return (left, right); @@ -104,7 +104,7 @@ namespace Spectre.Console var leftLength = ((maxWidth - titleLength) / 2) - 1; var left = new Segment(new string('─', leftLength) + " ", Style ?? Style.Plain); - var rightLength = maxWidth - titleLength - left.CellLength(context) - 1; + var rightLength = maxWidth - titleLength - left.CellCount(context) - 1; var right = new Segment(" " + new string('─', rightLength), Style ?? Style.Plain); return (left, right); @@ -113,7 +113,7 @@ namespace Spectre.Console { var right = new Segment(" " + new string('─', 2), Style ?? Style.Plain); - var leftLength = maxWidth - titleLength - right.CellLength(context) - 1; + var leftLength = maxWidth - titleLength - right.CellCount(context) - 1; var left = new Segment(new string('─', leftLength) + " ", Style ?? Style.Plain); return (left, right); diff --git a/src/Spectre.Console/Widgets/Table.cs b/src/Spectre.Console/Widgets/Table.cs index 39d29b1..03417ee 100644 --- a/src/Spectre.Console/Widgets/Table.cs +++ b/src/Spectre.Console/Widgets/Table.cs @@ -217,7 +217,7 @@ namespace Spectre.Console var justification = _columns[columnIndex].Alignment; var childContext = context.WithJustification(justification); - var lines = Segment.SplitLines(cell.Render(childContext, rowWidth)); + var lines = Segment.SplitLines(context, cell.Render(childContext, rowWidth)); cellHeight = Math.Max(cellHeight, lines.Count); cells.Add(lines); } @@ -261,7 +261,7 @@ namespace Spectre.Console rowResult.AddRange(cell[cellRowIndex]); // Pad cell content right - var length = cell[cellRowIndex].Sum(segment => segment.CellLength(context)); + var length = cell[cellRowIndex].Sum(segment => segment.CellCount(context)); if (length < columnWidths[cellIndex]) { rowResult.Add(new Segment(new string(' ', columnWidths[cellIndex] - length))); @@ -295,7 +295,7 @@ namespace Spectre.Console Aligner.Align(context, rowResult, Alignment, actualMaxWidth); // Is the row larger than the allowed max width? - if (Segment.CellLength(context, rowResult) > actualMaxWidth) + if (Segment.CellCount(context, rowResult) > actualMaxWidth) { result.AddRange(Segment.Truncate(context, rowResult, actualMaxWidth)); }