Skip to content

Commit b98614c

Browse files
Merge pull request #445 from SixLabors/js/additional-linebreak-fixes
Fix Linebreak issues #443 and #444
2 parents a82862f + 23fda1d commit b98614c

12 files changed

+265
-141
lines changed

src/SixLabors.Fonts/TextLayout.cs

+116-141
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Six Labors Split License.
33

44
using System.Diagnostics;
5+
using System.Diagnostics.CodeAnalysis;
56
using System.Numerics;
67
using SixLabors.Fonts.Tables.AdvancedTypographic;
78
using SixLabors.Fonts.Unicode;
@@ -1181,149 +1182,117 @@ VerticalOrientationType.Rotate or
11811182
lineBreaks.Add(lineBreakEnumerator.Current);
11821183
}
11831184

1184-
// Then split the line at the line breaks.
1185-
int lineBreakIndex = 0;
1186-
int maxLineBreakIndex = lineBreaks.Count - 1;
1187-
LineBreak lastLineBreak = lineBreaks[lineBreakIndex];
1188-
LineBreak currentLineBreak = lineBreaks[lineBreakIndex];
1189-
float lineAdvance = 0;
1190-
1191-
for (int i = 0; i < textLine.Count; i++)
1185+
int usedOffset = 0;
1186+
while (textLine.Count > 0)
11921187
{
1193-
int max = textLine.Count - 1;
1194-
TextLine.GlyphLayoutData glyph = textLine[i];
1195-
codePointIndex = glyph.CodePointIndex;
1196-
int graphemeCodePointIndex = glyph.GraphemeCodePointIndex;
1197-
1198-
if (graphemeCodePointIndex == 0 && textLine.Count > 0)
1188+
LineBreak? bestBreak = null;
1189+
foreach (LineBreak lineBreak in lineBreaks)
11991190
{
1200-
lineAdvance += glyph.ScaledAdvance;
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)
1196+
{
1197+
continue;
1198+
}
12011199

1202-
if (codePointIndex == currentLineBreak.PositionWrap && currentLineBreak.Required)
1200+
// Measure the text up to the adjusted break point
1201+
float measure = textLine.MeasureAt(measureAt);
1202+
if (measure > wrappingLength)
12031203
{
1204-
// Mandatory line break at index.
1205-
TextLine remaining = textLine.SplitAt(i);
1204+
// Stop and use the best break so far
1205+
bestBreak ??= lineBreak;
1206+
break;
1207+
}
1208+
1209+
// Update the best break
1210+
bestBreak = lineBreak;
12061211

1207-
if (shouldWrap && textLine.ScaledLineAdvance - glyph.ScaledAdvance > wrappingLength)
1212+
// If it's a mandatory break, stop immediately
1213+
if (lineBreak.Required)
1214+
{
1215+
break;
1216+
}
1217+
}
1218+
1219+
if (bestBreak != null)
1220+
{
1221+
if (breakAll)
1222+
{
1223+
// Break-all works differently to the other modes.
1224+
// It will break at any character so we simply toggle the breaking operation depending
1225+
// on whether the break is required.
1226+
TextLine? remaining;
1227+
if (bestBreak.Value.Required)
12081228
{
1209-
// We've overshot the wrapping length so we need to split the line
1210-
// at the previous break and add both lines.
1211-
TextLine overflow = textLine.SplitAt(lastLineBreak, keepAll);
1212-
if (overflow != textLine)
1229+
if (textLine.TrySplitAt(bestBreak.Value, keepAll, out remaining))
12131230
{
1231+
usedOffset += textLine.Count;
12141232
textLines.Add(textLine.Finalize(options));
1215-
textLine = overflow;
1233+
textLine = remaining;
12161234
}
1217-
1235+
}
1236+
else if (textLine.TrySplitAt(wrappingLength, out remaining))
1237+
{
1238+
usedOffset += textLine.Count;
12181239
textLines.Add(textLine.Finalize(options));
12191240
textLine = remaining;
1220-
i = -1;
1221-
lineAdvance = 0;
12221241
}
12231242
else
12241243
{
1225-
textLines.Add(textLine.Finalize(options));
1226-
textLine = remaining;
1227-
i = -1;
1228-
lineAdvance = 0;
1244+
usedOffset += textLine.Count;
12291245
}
12301246
}
1231-
else if (shouldWrap)
1247+
else
12321248
{
1233-
if (lineAdvance >= wrappingLength)
1249+
// Split the current line at the adjusted break index
1250+
if (textLine.TrySplitAt(bestBreak.Value, keepAll, out TextLine? remaining))
12341251
{
1235-
if (breakAll)
1252+
usedOffset += textLine.Count;
1253+
if (breakWord)
12361254
{
1237-
// Insert a forced break.
1238-
TextLine remaining = textLine.SplitAt(i);
1239-
if (remaining != textLine)
1255+
// A break was found, but we need to check if the line is too long
1256+
// and break if required.
1257+
if (textLine.ScaledLineAdvance > wrappingLength &&
1258+
textLine.TrySplitAt(wrappingLength, out TextLine? overflow))
12401259
{
1241-
textLines.Add(textLine.Finalize(options));
1242-
textLine = remaining;
1243-
i = -1;
1244-
lineAdvance = 0;
1260+
// Reinsert the overflow at the beginning of the remaining line
1261+
usedOffset -= overflow.Count;
1262+
remaining.InsertAt(0, overflow);
12451263
}
12461264
}
1247-
else if (codePointIndex == currentLineBreak.PositionWrap || i == max)
1248-
{
1249-
LineBreak lineBreak = lineAdvance == wrappingLength
1250-
? currentLineBreak
1251-
: lastLineBreak;
12521265

1253-
if (i > 0)
1254-
{
1255-
// If the current break is a space, and the line minus the space
1256-
// is less than the wrapping length, we can break using the current break.
1257-
float previousAdvance = lineAdvance - glyph.ScaledAdvance;
1258-
TextLine.GlyphLayoutData lastGlyph = textLine[i - 1];
1259-
if (CodePoint.IsWhiteSpace(lastGlyph.CodePoint))
1260-
{
1261-
previousAdvance -= lastGlyph.ScaledAdvance;
1262-
if (previousAdvance <= wrappingLength)
1263-
{
1264-
lineBreak = currentLineBreak;
1265-
}
1266-
}
1267-
}
1268-
1269-
// If we are at the position wrap we can break here.
1270-
// Split the line at the appropriate break.
1271-
// CJK characters will not be split if 'keepAll' is true.
1272-
TextLine remaining = textLine.SplitAt(lineBreak, keepAll);
1273-
1274-
if (remaining != textLine)
1275-
{
1276-
if (breakWord)
1277-
{
1278-
// If the line is too long, insert a forced break.
1279-
if (textLine.ScaledLineAdvance > wrappingLength)
1280-
{
1281-
TextLine overflow = textLine.SplitAt(wrappingLength);
1282-
if (overflow != textLine)
1283-
{
1284-
remaining.InsertAt(0, overflow);
1285-
}
1286-
}
1287-
}
1288-
1289-
textLines.Add(textLine.Finalize(options));
1290-
textLine = remaining;
1291-
i = -1;
1292-
lineAdvance = 0;
1293-
}
1294-
}
1266+
// Add the split part to the list and continue processing.
1267+
textLines.Add(textLine.Finalize(options));
1268+
textLine = remaining;
1269+
}
1270+
else
1271+
{
1272+
usedOffset += textLine.Count;
12951273
}
12961274
}
12971275
}
1298-
1299-
// Find the next line break.
1300-
if (lineBreakIndex < maxLineBreakIndex &&
1301-
(currentLineBreak.PositionWrap == codePointIndex))
1302-
{
1303-
lastLineBreak = currentLineBreak;
1304-
currentLineBreak = lineBreaks[++lineBreakIndex];
1305-
}
1306-
}
1307-
1308-
// Add the final line.
1309-
if (textLine.Count > 0)
1310-
{
1311-
if (shouldWrap && (breakWord || breakAll))
1276+
else
13121277
{
1313-
while (textLine.ScaledLineAdvance > wrappingLength)
1278+
// If no valid break is found, add the remaining line and exit
1279+
if (breakWord || breakAll)
13141280
{
1315-
TextLine overflow = textLine.SplitAt(wrappingLength);
1316-
if (overflow == textLine)
1281+
while (textLine.ScaledLineAdvance > wrappingLength)
13171282
{
1318-
break;
1319-
}
1283+
if (!textLine.TrySplitAt(wrappingLength, out TextLine? overflow))
1284+
{
1285+
break;
1286+
}
13201287

1321-
textLines.Add(textLine.Finalize(options));
1322-
textLine = overflow;
1288+
textLines.Add(textLine.Finalize(options));
1289+
textLine = overflow;
1290+
}
13231291
}
1324-
}
13251292

1326-
textLines.Add(textLine.Finalize(options));
1293+
textLines.Add(textLine.Finalize(options));
1294+
break;
1295+
}
13271296
}
13281297

13291298
return new TextBox(textLines);
@@ -1381,7 +1350,7 @@ public void Add(
13811350
{
13821351
// Reset metrics.
13831352
// We track the maximum metrics for each line to ensure glyphs can be aligned.
1384-
if (graphemeIndex == 0)
1353+
if (graphemeCodePointIndex == 0)
13851354
{
13861355
this.ScaledLineAdvance += scaledAdvance;
13871356
}
@@ -1406,31 +1375,36 @@ public void Add(
14061375
stringIndex));
14071376
}
14081377

1409-
public TextLine InsertAt(int index, TextLine textLine)
1378+
public void InsertAt(int index, TextLine textLine)
14101379
{
14111380
this.data.InsertRange(index, textLine.data);
14121381
RecalculateLineMetrics(this);
1413-
return this;
14141382
}
14151383

1416-
public TextLine SplitAt(int index)
1384+
public float MeasureAt(int index)
14171385
{
1418-
if (index == 0 || index >= this.Count)
1386+
if (index >= this.data.Count)
14191387
{
1420-
return this;
1388+
index = this.data.Count - 1;
14211389
}
14221390

1423-
int count = this.data.Count - index;
1424-
TextLine result = new(count);
1425-
result.data.AddRange(this.data.GetRange(index, count));
1426-
RecalculateLineMetrics(result);
1391+
while (index >= 0 && CodePoint.IsWhiteSpace(this.data[index].CodePoint))
1392+
{
1393+
// If the index is whitespace, we need to measure at the previous
1394+
// non-whitespace glyph to ensure we don't break too early.
1395+
index--;
1396+
}
14271397

1428-
this.data.RemoveRange(index, count);
1429-
RecalculateLineMetrics(this);
1430-
return result;
1398+
float advance = 0;
1399+
for (int i = 0; i <= index; i++)
1400+
{
1401+
advance += this.data[i].ScaledAdvance;
1402+
}
1403+
1404+
return advance;
14311405
}
14321406

1433-
public TextLine SplitAt(float length)
1407+
public bool TrySplitAt(float length, [NotNullWhen(true)] out TextLine? result)
14341408
{
14351409
float advance = this.data[0].ScaledAdvance;
14361410

@@ -1449,20 +1423,21 @@ public TextLine SplitAt(float length)
14491423
if (advance >= length)
14501424
{
14511425
int count = this.data.Count - i;
1452-
TextLine result = new(count);
1426+
result = new(count);
14531427
result.data.AddRange(this.data.GetRange(i, count));
14541428
RecalculateLineMetrics(result);
14551429

14561430
this.data.RemoveRange(i, count);
14571431
RecalculateLineMetrics(this);
1458-
return result;
1432+
return true;
14591433
}
14601434
}
14611435

1462-
return this;
1436+
result = null;
1437+
return false;
14631438
}
14641439

1465-
public TextLine SplitAt(LineBreak lineBreak, bool keepAll)
1440+
public bool TrySplitAt(LineBreak lineBreak, bool keepAll, [NotNullWhen(true)] out TextLine? result)
14661441
{
14671442
int index = this.data.Count;
14681443
GlyphLayoutData glyphWrap = default;
@@ -1475,14 +1450,12 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll)
14751450
}
14761451
}
14771452

1478-
if (index == 0)
1479-
{
1480-
return this;
1481-
}
1482-
14831453
// Word breaks should not be used for Chinese/Japanese/Korean (CJK) text
14841454
// when word-breaking mode is keep-all.
1485-
if (!lineBreak.Required && keepAll && UnicodeUtility.IsCJKCodePoint((uint)glyphWrap.CodePoint.Value))
1455+
if (index > 0
1456+
&& !lineBreak.Required
1457+
&& keepAll
1458+
&& UnicodeUtility.IsCJKCodePoint((uint)glyphWrap.CodePoint.Value))
14861459
{
14871460
// Loop through previous glyphs to see if there is
14881461
// a non CJK codepoint we can break at.
@@ -1495,23 +1468,25 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll)
14951468
break;
14961469
}
14971470
}
1471+
}
14981472

1499-
if (index == 0)
1500-
{
1501-
return this;
1502-
}
1473+
if (index == 0)
1474+
{
1475+
result = null;
1476+
return false;
15031477
}
15041478

15051479
// Create a new line ensuring we capture the initial metrics.
15061480
int count = this.data.Count - index;
1507-
TextLine result = new(count);
1481+
result = new(count);
15081482
result.data.AddRange(this.data.GetRange(index, count));
15091483
RecalculateLineMetrics(result);
15101484

15111485
// Remove those items from this line.
15121486
this.data.RemoveRange(index, count);
15131487
RecalculateLineMetrics(this);
1514-
return result;
1488+
1489+
return true;
15151490
}
15161491

15171492
private void TrimTrailingWhitespace()
Loading
Loading
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)