2
2
// Licensed under the Six Labors Split License.
3
3
4
4
using System . Diagnostics ;
5
+ using System . Diagnostics . CodeAnalysis ;
5
6
using System . Numerics ;
6
7
using SixLabors . Fonts . Tables . AdvancedTypographic ;
7
8
using SixLabors . Fonts . Unicode ;
@@ -1181,149 +1182,117 @@ VerticalOrientationType.Rotate or
1181
1182
lineBreaks . Add ( lineBreakEnumerator . Current ) ;
1182
1183
}
1183
1184
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 )
1192
1187
{
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 )
1199
1190
{
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
+ }
1201
1199
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 )
1203
1203
{
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 ;
1206
1211
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 )
1208
1228
{
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 ) )
1213
1230
{
1231
+ usedOffset += textLine . Count ;
1214
1232
textLines . Add ( textLine . Finalize ( options ) ) ;
1215
- textLine = overflow ;
1233
+ textLine = remaining ;
1216
1234
}
1217
-
1235
+ }
1236
+ else if ( textLine . TrySplitAt ( wrappingLength , out remaining ) )
1237
+ {
1238
+ usedOffset += textLine . Count ;
1218
1239
textLines . Add ( textLine . Finalize ( options ) ) ;
1219
1240
textLine = remaining ;
1220
- i = - 1 ;
1221
- lineAdvance = 0 ;
1222
1241
}
1223
1242
else
1224
1243
{
1225
- textLines . Add ( textLine . Finalize ( options ) ) ;
1226
- textLine = remaining ;
1227
- i = - 1 ;
1228
- lineAdvance = 0 ;
1244
+ usedOffset += textLine . Count ;
1229
1245
}
1230
1246
}
1231
- else if ( shouldWrap )
1247
+ else
1232
1248
{
1233
- if ( lineAdvance >= wrappingLength )
1249
+ // Split the current line at the adjusted break index
1250
+ if ( textLine . TrySplitAt ( bestBreak . Value , keepAll , out TextLine ? remaining ) )
1234
1251
{
1235
- if ( breakAll )
1252
+ usedOffset += textLine . Count ;
1253
+ if ( breakWord )
1236
1254
{
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 ) )
1240
1259
{
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 ) ;
1245
1263
}
1246
1264
}
1247
- else if ( codePointIndex == currentLineBreak . PositionWrap || i == max )
1248
- {
1249
- LineBreak lineBreak = lineAdvance == wrappingLength
1250
- ? currentLineBreak
1251
- : lastLineBreak ;
1252
1265
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 ;
1295
1273
}
1296
1274
}
1297
1275
}
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
1312
1277
{
1313
- while ( textLine . ScaledLineAdvance > wrappingLength )
1278
+ // If no valid break is found, add the remaining line and exit
1279
+ if ( breakWord || breakAll )
1314
1280
{
1315
- TextLine overflow = textLine . SplitAt ( wrappingLength ) ;
1316
- if ( overflow == textLine )
1281
+ while ( textLine . ScaledLineAdvance > wrappingLength )
1317
1282
{
1318
- break ;
1319
- }
1283
+ if ( ! textLine . TrySplitAt ( wrappingLength , out TextLine ? overflow ) )
1284
+ {
1285
+ break ;
1286
+ }
1320
1287
1321
- textLines . Add ( textLine . Finalize ( options ) ) ;
1322
- textLine = overflow ;
1288
+ textLines . Add ( textLine . Finalize ( options ) ) ;
1289
+ textLine = overflow ;
1290
+ }
1323
1291
}
1324
- }
1325
1292
1326
- textLines . Add ( textLine . Finalize ( options ) ) ;
1293
+ textLines . Add ( textLine . Finalize ( options ) ) ;
1294
+ break ;
1295
+ }
1327
1296
}
1328
1297
1329
1298
return new TextBox ( textLines ) ;
@@ -1381,7 +1350,7 @@ public void Add(
1381
1350
{
1382
1351
// Reset metrics.
1383
1352
// We track the maximum metrics for each line to ensure glyphs can be aligned.
1384
- if ( graphemeIndex == 0 )
1353
+ if ( graphemeCodePointIndex == 0 )
1385
1354
{
1386
1355
this . ScaledLineAdvance += scaledAdvance ;
1387
1356
}
@@ -1406,31 +1375,36 @@ public void Add(
1406
1375
stringIndex ) ) ;
1407
1376
}
1408
1377
1409
- public TextLine InsertAt ( int index , TextLine textLine )
1378
+ public void InsertAt ( int index , TextLine textLine )
1410
1379
{
1411
1380
this . data . InsertRange ( index , textLine . data ) ;
1412
1381
RecalculateLineMetrics ( this ) ;
1413
- return this ;
1414
1382
}
1415
1383
1416
- public TextLine SplitAt ( int index )
1384
+ public float MeasureAt ( int index )
1417
1385
{
1418
- if ( index == 0 || index >= this . Count )
1386
+ if ( index >= this . data . Count )
1419
1387
{
1420
- return this ;
1388
+ index = this . data . Count - 1 ;
1421
1389
}
1422
1390
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
+ }
1427
1397
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 ;
1431
1405
}
1432
1406
1433
- public TextLine SplitAt ( float length )
1407
+ public bool TrySplitAt ( float length , [ NotNullWhen ( true ) ] out TextLine ? result )
1434
1408
{
1435
1409
float advance = this . data [ 0 ] . ScaledAdvance ;
1436
1410
@@ -1449,20 +1423,21 @@ public TextLine SplitAt(float length)
1449
1423
if ( advance >= length )
1450
1424
{
1451
1425
int count = this . data . Count - i ;
1452
- TextLine result = new ( count ) ;
1426
+ result = new ( count ) ;
1453
1427
result . data . AddRange ( this . data . GetRange ( i , count ) ) ;
1454
1428
RecalculateLineMetrics ( result ) ;
1455
1429
1456
1430
this . data . RemoveRange ( i , count ) ;
1457
1431
RecalculateLineMetrics ( this ) ;
1458
- return result ;
1432
+ return true ;
1459
1433
}
1460
1434
}
1461
1435
1462
- return this ;
1436
+ result = null ;
1437
+ return false ;
1463
1438
}
1464
1439
1465
- public TextLine SplitAt ( LineBreak lineBreak , bool keepAll )
1440
+ public bool TrySplitAt ( LineBreak lineBreak , bool keepAll , [ NotNullWhen ( true ) ] out TextLine ? result )
1466
1441
{
1467
1442
int index = this . data . Count ;
1468
1443
GlyphLayoutData glyphWrap = default ;
@@ -1475,14 +1450,12 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll)
1475
1450
}
1476
1451
}
1477
1452
1478
- if ( index == 0 )
1479
- {
1480
- return this ;
1481
- }
1482
-
1483
1453
// Word breaks should not be used for Chinese/Japanese/Korean (CJK) text
1484
1454
// 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 ) )
1486
1459
{
1487
1460
// Loop through previous glyphs to see if there is
1488
1461
// a non CJK codepoint we can break at.
@@ -1495,23 +1468,25 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll)
1495
1468
break ;
1496
1469
}
1497
1470
}
1471
+ }
1498
1472
1499
- if ( index == 0 )
1500
- {
1501
- return this ;
1502
- }
1473
+ if ( index == 0 )
1474
+ {
1475
+ result = null ;
1476
+ return false ;
1503
1477
}
1504
1478
1505
1479
// Create a new line ensuring we capture the initial metrics.
1506
1480
int count = this . data . Count - index ;
1507
- TextLine result = new ( count ) ;
1481
+ result = new ( count ) ;
1508
1482
result . data . AddRange ( this . data . GetRange ( index , count ) ) ;
1509
1483
RecalculateLineMetrics ( result ) ;
1510
1484
1511
1485
// Remove those items from this line.
1512
1486
this . data . RemoveRange ( index , count ) ;
1513
1487
RecalculateLineMetrics ( this ) ;
1514
- return result ;
1488
+
1489
+ return true ;
1515
1490
}
1516
1491
1517
1492
private void TrimTrailingWhitespace ( )
0 commit comments