Skip to content

Commit d74f3fa

Browse files
Merge pull request #447 from SixLabors/js/additional-linebreak-fixes
Fix Tracking of Last Line Break
2 parents b98614c + fb83c12 commit d74f3fa

File tree

4 files changed

+82
-28
lines changed

4 files changed

+82
-28
lines changed

src/SixLabors.Fonts/TextLayout.cs

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,42 +1182,40 @@ VerticalOrientationType.Rotate or
11821182
lineBreaks.Add(lineBreakEnumerator.Current);
11831183
}
11841184

1185-
int usedOffset = 0;
1185+
int processed = 0;
11861186
while (textLine.Count > 0)
11871187
{
11881188
LineBreak? bestBreak = null;
11891189
foreach (LineBreak lineBreak in lineBreaks)
11901190
{
1191-
// Adjust the break index relative to the current position in the original line
1192-
int measureAt = lineBreak.PositionMeasure - usedOffset;
1193-
1194-
// Skip breaks that are already behind the trimmed portion
1195-
if (measureAt < 0)
1191+
// Skip breaks that are already behind the processed portion
1192+
if (lineBreak.PositionWrap <= processed)
11961193
{
11971194
continue;
11981195
}
11991196

12001197
// Measure the text up to the adjusted break point
1201-
float measure = textLine.MeasureAt(measureAt);
1202-
if (measure > wrappingLength)
1198+
float advance = textLine.MeasureAt(lineBreak.PositionMeasure - processed);
1199+
if (advance >= wrappingLength)
12031200
{
1204-
// Stop and use the best break so far
12051201
bestBreak ??= lineBreak;
12061202
break;
12071203
}
12081204

1209-
// Update the best break
1210-
bestBreak = lineBreak;
1211-
12121205
// If it's a mandatory break, stop immediately
12131206
if (lineBreak.Required)
12141207
{
1208+
bestBreak = lineBreak;
12151209
break;
12161210
}
1211+
1212+
// Update the best break
1213+
bestBreak = lineBreak;
12171214
}
12181215

12191216
if (bestBreak != null)
12201217
{
1218+
LineBreak breakAt = bestBreak.Value;
12211219
if (breakAll)
12221220
{
12231221
// Break-all works differently to the other modes.
@@ -1226,30 +1224,34 @@ VerticalOrientationType.Rotate or
12261224
TextLine? remaining;
12271225
if (bestBreak.Value.Required)
12281226
{
1229-
if (textLine.TrySplitAt(bestBreak.Value, keepAll, out remaining))
1227+
if (textLine.TrySplitAt(breakAt, keepAll, out remaining))
12301228
{
1231-
usedOffset += textLine.Count;
1229+
processed = breakAt.PositionWrap;
12321230
textLines.Add(textLine.Finalize(options));
12331231
textLine = remaining;
12341232
}
12351233
}
12361234
else if (textLine.TrySplitAt(wrappingLength, out remaining))
12371235
{
1238-
usedOffset += textLine.Count;
1236+
processed += textLine.Count;
12391237
textLines.Add(textLine.Finalize(options));
12401238
textLine = remaining;
12411239
}
12421240
else
12431241
{
1244-
usedOffset += textLine.Count;
1242+
processed += textLine.Count;
12451243
}
12461244
}
12471245
else
12481246
{
12491247
// Split the current line at the adjusted break index
1250-
if (textLine.TrySplitAt(bestBreak.Value, keepAll, out TextLine? remaining))
1248+
if (textLine.TrySplitAt(breakAt, keepAll, out TextLine? remaining))
12511249
{
1252-
usedOffset += textLine.Count;
1250+
// If 'keepAll' is true then the break could be later than expected.
1251+
processed = keepAll
1252+
? processed + Math.Max(textLine.Count, breakAt.PositionWrap - processed)
1253+
: breakAt.PositionWrap;
1254+
12531255
if (breakWord)
12541256
{
12551257
// A break was found, but we need to check if the line is too long
@@ -1258,7 +1260,7 @@ VerticalOrientationType.Rotate or
12581260
textLine.TrySplitAt(wrappingLength, out TextLine? overflow))
12591261
{
12601262
// Reinsert the overflow at the beginning of the remaining line
1261-
usedOffset -= overflow.Count;
1263+
processed -= overflow.Count;
12621264
remaining.InsertAt(0, overflow);
12631265
}
12641266
}
@@ -1269,13 +1271,14 @@ VerticalOrientationType.Rotate or
12691271
}
12701272
else
12711273
{
1272-
usedOffset += textLine.Count;
1274+
processed += textLine.Count;
12731275
}
12741276
}
12751277
}
12761278
else
12771279
{
1278-
// If no valid break is found, add the remaining line and exit
1280+
// We're at the last line break which should be at the end of the
1281+
// text. We can break here and finalize the line.
12791282
if (breakWord || breakAll)
12801283
{
12811284
while (textLine.ScaledLineAdvance > wrappingLength)
@@ -1316,6 +1319,7 @@ public float ScaledMaxAdvance()
13161319
internal sealed class TextLine
13171320
{
13181321
private readonly List<GlyphLayoutData> data;
1322+
private readonly Dictionary<int, float> advances = new();
13191323

13201324
public TextLine() => this.data = new(16);
13211325

@@ -1383,6 +1387,11 @@ public void InsertAt(int index, TextLine textLine)
13831387

13841388
public float MeasureAt(int index)
13851389
{
1390+
if (this.advances.TryGetValue(index, out float advance))
1391+
{
1392+
return advance;
1393+
}
1394+
13861395
if (index >= this.data.Count)
13871396
{
13881397
index = this.data.Count - 1;
@@ -1395,12 +1404,13 @@ public float MeasureAt(int index)
13951404
index--;
13961405
}
13971406

1398-
float advance = 0;
1407+
advance = 0;
13991408
for (int i = 0; i <= index; i++)
14001409
{
14011410
advance += this.data[i].ScaledAdvance;
14021411
}
14031412

1413+
this.advances[index] = advance;
14041414
return advance;
14051415
}
14061416

@@ -1440,11 +1450,11 @@ public bool TrySplitAt(float length, [NotNullWhen(true)] out TextLine? result)
14401450
public bool TrySplitAt(LineBreak lineBreak, bool keepAll, [NotNullWhen(true)] out TextLine? result)
14411451
{
14421452
int index = this.data.Count;
1443-
GlyphLayoutData glyphWrap = default;
1453+
GlyphLayoutData glyphData = default;
14441454
while (index > 0)
14451455
{
1446-
glyphWrap = this.data[--index];
1447-
if (glyphWrap.CodePointIndex == lineBreak.PositionWrap)
1456+
glyphData = this.data[--index];
1457+
if (glyphData.CodePointIndex == lineBreak.PositionWrap)
14481458
{
14491459
break;
14501460
}
@@ -1455,14 +1465,14 @@ public bool TrySplitAt(LineBreak lineBreak, bool keepAll, [NotNullWhen(true)] ou
14551465
if (index > 0
14561466
&& !lineBreak.Required
14571467
&& keepAll
1458-
&& UnicodeUtility.IsCJKCodePoint((uint)glyphWrap.CodePoint.Value))
1468+
&& UnicodeUtility.IsCJKCodePoint((uint)glyphData.CodePoint.Value))
14591469
{
14601470
// Loop through previous glyphs to see if there is
14611471
// a non CJK codepoint we can break at.
14621472
while (index > 0)
14631473
{
1464-
glyphWrap = this.data[--index];
1465-
if (!UnicodeUtility.IsCJKCodePoint((uint)glyphWrap.CodePoint.Value))
1474+
glyphData = this.data[--index];
1475+
if (!UnicodeUtility.IsCJKCodePoint((uint)glyphData.CodePoint.Value))
14661476
{
14671477
index++;
14681478
break;
@@ -1694,6 +1704,7 @@ private static void RecalculateLineMetrics(TextLine textLine)
16941704
textLine.ScaledMaxAscender = ascender;
16951705
textLine.ScaledMaxDescender = descender;
16961706
textLine.ScaledMaxLineHeight = lineHeight;
1707+
textLine.advances.Clear();
16971708
}
16981709

16991710
/// <summary>
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Numerics;
5+
6+
namespace SixLabors.Fonts.Tests.Issues;
7+
8+
public class Issues_446
9+
{
10+
private readonly FontFamily charisSIL = new FontCollection().Add(TestFonts.CharisSILRegular);
11+
12+
[Fact]
13+
public void Issue_446_A()
14+
{
15+
Font font = this.charisSIL.CreateFont(85);
16+
TextLayoutTestUtilities.TestLayout(
17+
"⇒ Tim Cook\n⇒ Jef Bezos\n⇒ Steve Jobs\n⇒ Mark Zuckerberg",
18+
new TextOptions(font)
19+
{
20+
Origin = new Vector2(50, 20),
21+
WrappingLength = 860,
22+
});
23+
}
24+
25+
[Fact]
26+
public void Issue_446_B()
27+
{
28+
Font font = this.charisSIL.CreateFont(85);
29+
TextLayoutTestUtilities.TestLayout(
30+
"⇒ Tim Cook\n⇒ Jeff Bezos\n⇒ Steve Jobs\n⇒ Mark Zuckerberg",
31+
new TextOptions(font)
32+
{
33+
Origin = new Vector2(50, 20),
34+
WrappingLength = 860,
35+
});
36+
}
37+
}

0 commit comments

Comments
 (0)