diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 46137fcb..f137cd81 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -1223,7 +1223,11 @@ VerticalOrientationType.Rotate or // If the line is too long, insert a forced line break. if (textLine.ScaledLineAdvance > wrappingLength) { - remaining.InsertAt(0, textLine.SplitAt(wrappingLength)); + TextLine overflow = textLine.SplitAt(wrappingLength); + if (overflow != textLine) + { + remaining.InsertAt(0, overflow); + } } } @@ -1249,6 +1253,21 @@ VerticalOrientationType.Rotate or // Add the final line. if (textLine.Count > 0) { + if (shouldWrap && (breakWord || breakAll)) + { + while (textLine.ScaledLineAdvance > wrappingLength) + { + TextLine overflow = textLine.SplitAt(wrappingLength); + if (overflow == textLine) + { + break; + } + + textLines.Add(textLine.Finalize(options)); + textLine = overflow; + } + } + textLines.Add(textLine.Finalize(options)); } @@ -1342,29 +1361,40 @@ public TextLine SplitAt(int index) return this; } - TextLine result = new(); - result.data.AddRange(this.data.GetRange(index, this.data.Count - index)); + int count = this.data.Count - index; + TextLine result = new(count); + result.data.AddRange(this.data.GetRange(index, count)); RecalculateLineMetrics(result); - this.data.RemoveRange(index, this.data.Count - index); + this.data.RemoveRange(index, count); RecalculateLineMetrics(this); return result; } public TextLine SplitAt(float length) { - TextLine result = new(); - float advance = 0; - for (int i = 0; i < this.data.Count; i++) + float advance = this.data[0].ScaledAdvance; + + // Ensure at least one glyph is in the line. + // trailing whitespace should be ignored as it is trimmed + // on finalization. + for (int i = 1; i < this.data.Count; i++) { GlyphLayoutData glyph = this.data[i]; advance += glyph.ScaledAdvance; + if (CodePoint.IsWhiteSpace(glyph.CodePoint)) + { + continue; + } + if (advance >= length) { - result.data.AddRange(this.data.GetRange(i, this.data.Count - i)); + int count = this.data.Count - i; + TextLine result = new(count); + result.data.AddRange(this.data.GetRange(i, count)); RecalculateLineMetrics(result); - this.data.RemoveRange(i, this.data.Count - i); + this.data.RemoveRange(i, count); RecalculateLineMetrics(this); return result; } diff --git a/tests/Images/ReferenceOutput/BreakWordEnsuresSingleCharacterPerLine__WrappingLength_1_.png b/tests/Images/ReferenceOutput/BreakWordEnsuresSingleCharacterPerLine__WrappingLength_1_.png new file mode 100644 index 00000000..4cc9c33e --- /dev/null +++ b/tests/Images/ReferenceOutput/BreakWordEnsuresSingleCharacterPerLine__WrappingLength_1_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a55c654f4e0748d3fb1f47d1cec8d243774d38257a8dd5db407a2a2a96a33e69 +size 1277 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png index d0834260..22aa8d7c 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a5ec93c6ece40f6c3577970cd0fa1f97d74d019ed52cf3ce9812b4d805624b0 -size 13613 +oid sha256:65da62dd5894e1bdda14fd9d469205c0cdfecb679617660a48c4ea4b4e5faa1b +size 13587 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png index bc6d223a..2ad66506 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d714d870453515f516af0fe8267795df3b36059a9c8c91b0946889985cd089e -size 13705 +oid sha256:6fe3ad7e51ad713b36ab89c186c1e32c14467ba3320b31f3ce99bf7d4e603050 +size 13669 diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 340aef0a..4e4d9cff 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -1206,6 +1206,23 @@ public void IsMeasureCharacterBoundsSameAsRenderBounds() } } + [Fact] + public void BreakWordEnsuresSingleCharacterPerLine() + { + Font font = CreateRenderingFont(20); + TextOptions options = new(font) + { + WordBreaking = WordBreaking.BreakWord, + WrappingLength = 1 + }; + + const string text = "Hello World!"; + int lineCount = TextMeasurer.CountLines(text, options); + Assert.Equal(text.Length - 1, lineCount); + + TextLayoutTestUtilities.TestLayout(text, options); + } + private class CaptureGlyphBoundBuilder : IGlyphRenderer { public static List GenerateGlyphsBoxes(string text, TextOptions options)