Skip to content

Commit 23c9a43

Browse files
Fix #446
1 parent b98614c commit 23c9a43

File tree

4 files changed

+90
-28
lines changed

4 files changed

+90
-28
lines changed

src/SixLabors.Fonts/TextLayout.cs

+47-28
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,13 +1319,16 @@ 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

13221326
public TextLine(int capacity) => this.data = new(capacity);
13231327

13241328
public int Count => this.data.Count;
13251329

1330+
public int LeadCodePointIndex { get; private set; } = -1;
1331+
13261332
public float ScaledLineAdvance { get; private set; }
13271333

13281334
public float ScaledMaxLineHeight { get; private set; } = -1;
@@ -1355,6 +1361,11 @@ public void Add(
13551361
this.ScaledLineAdvance += scaledAdvance;
13561362
}
13571363

1364+
if (this.LeadCodePointIndex == -1)
1365+
{
1366+
this.LeadCodePointIndex = codePointIndex;
1367+
}
1368+
13581369
this.ScaledMaxLineHeight = MathF.Max(this.ScaledMaxLineHeight, scaledLineHeight);
13591370
this.ScaledMaxAscender = MathF.Max(this.ScaledMaxAscender, scaledAscender);
13601371
this.ScaledMaxDescender = MathF.Max(this.ScaledMaxDescender, scaledDescender);
@@ -1383,6 +1394,11 @@ public void InsertAt(int index, TextLine textLine)
13831394

13841395
public float MeasureAt(int index)
13851396
{
1397+
if (this.advances.TryGetValue(index, out float advance))
1398+
{
1399+
return advance;
1400+
}
1401+
13861402
if (index >= this.data.Count)
13871403
{
13881404
index = this.data.Count - 1;
@@ -1395,12 +1411,13 @@ public float MeasureAt(int index)
13951411
index--;
13961412
}
13971413

1398-
float advance = 0;
1414+
advance = 0;
13991415
for (int i = 0; i <= index; i++)
14001416
{
14011417
advance += this.data[i].ScaledAdvance;
14021418
}
14031419

1420+
this.advances[index] = advance;
14041421
return advance;
14051422
}
14061423

@@ -1440,11 +1457,11 @@ public bool TrySplitAt(float length, [NotNullWhen(true)] out TextLine? result)
14401457
public bool TrySplitAt(LineBreak lineBreak, bool keepAll, [NotNullWhen(true)] out TextLine? result)
14411458
{
14421459
int index = this.data.Count;
1443-
GlyphLayoutData glyphWrap = default;
1460+
GlyphLayoutData glyphData = default;
14441461
while (index > 0)
14451462
{
1446-
glyphWrap = this.data[--index];
1447-
if (glyphWrap.CodePointIndex == lineBreak.PositionWrap)
1463+
glyphData = this.data[--index];
1464+
if (glyphData.CodePointIndex == lineBreak.PositionWrap)
14481465
{
14491466
break;
14501467
}
@@ -1455,14 +1472,14 @@ public bool TrySplitAt(LineBreak lineBreak, bool keepAll, [NotNullWhen(true)] ou
14551472
if (index > 0
14561473
&& !lineBreak.Required
14571474
&& keepAll
1458-
&& UnicodeUtility.IsCJKCodePoint((uint)glyphWrap.CodePoint.Value))
1475+
&& UnicodeUtility.IsCJKCodePoint((uint)glyphData.CodePoint.Value))
14591476
{
14601477
// Loop through previous glyphs to see if there is
14611478
// a non CJK codepoint we can break at.
14621479
while (index > 0)
14631480
{
1464-
glyphWrap = this.data[--index];
1465-
if (!UnicodeUtility.IsCJKCodePoint((uint)glyphWrap.CodePoint.Value))
1481+
glyphData = this.data[--index];
1482+
if (!UnicodeUtility.IsCJKCodePoint((uint)glyphData.CodePoint.Value))
14661483
{
14671484
index++;
14681485
break;
@@ -1694,6 +1711,8 @@ private static void RecalculateLineMetrics(TextLine textLine)
16941711
textLine.ScaledMaxAscender = ascender;
16951712
textLine.ScaledMaxDescender = descender;
16961713
textLine.ScaledMaxLineHeight = lineHeight;
1714+
textLine.LeadCodePointIndex = textLine[0].CodePointIndex;
1715+
textLine.advances.Clear();
16971716
}
16981717

16991718
/// <summary>
Loading
Loading
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)