expo-pretext 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1113 @@
1
+ import type { SegmentBreakKind } from './analysis'
2
+ import { getEngineProfile } from './engine-profile'
3
+
4
+ export type LineBreakCursor = {
5
+ segmentIndex: number
6
+ graphemeIndex: number
7
+ }
8
+
9
+ export type PreparedLineBreakData = {
10
+ widths: number[]
11
+ lineEndFitAdvances: number[]
12
+ lineEndPaintAdvances: number[]
13
+ kinds: SegmentBreakKind[]
14
+ simpleLineWalkFastPath: boolean
15
+ breakableWidths: (number[] | null)[]
16
+ breakablePrefixWidths: (number[] | null)[]
17
+ discretionaryHyphenWidth: number
18
+ tabStopAdvance: number
19
+ chunks: {
20
+ startSegmentIndex: number
21
+ endSegmentIndex: number
22
+ consumedEndSegmentIndex: number
23
+ }[]
24
+ }
25
+
26
+ export type InternalLayoutLine = {
27
+ startSegmentIndex: number
28
+ startGraphemeIndex: number
29
+ endSegmentIndex: number
30
+ endGraphemeIndex: number
31
+ width: number
32
+ }
33
+
34
+ function normalizeSimpleLineStartSegmentIndex(
35
+ prepared: PreparedLineBreakData,
36
+ segmentIndex: number,
37
+ ): number {
38
+ while (segmentIndex < prepared.widths.length) {
39
+ const kind = prepared.kinds[segmentIndex]!
40
+ if (kind !== 'space' && kind !== 'zero-width-break' && kind !== 'soft-hyphen') break
41
+ segmentIndex++
42
+ }
43
+ return segmentIndex
44
+ }
45
+
46
+ function getTabAdvance(lineWidth: number, tabStopAdvance: number): number {
47
+ if (tabStopAdvance <= 0) return 0
48
+
49
+ const remainder = lineWidth % tabStopAdvance
50
+ if (Math.abs(remainder) <= 1e-6) return tabStopAdvance
51
+ return tabStopAdvance - remainder
52
+ }
53
+
54
+ function fitSoftHyphenBreak(
55
+ graphemeWidths: number[],
56
+ initialWidth: number,
57
+ maxWidth: number,
58
+ lineFitEpsilon: number,
59
+ discretionaryHyphenWidth: number,
60
+ cumulativeWidths: boolean,
61
+ ): { fitCount: number, fittedWidth: number } {
62
+ let fitCount = 0
63
+ let fittedWidth = initialWidth
64
+
65
+ while (fitCount < graphemeWidths.length) {
66
+ const nextWidth = cumulativeWidths
67
+ ? initialWidth + graphemeWidths[fitCount]!
68
+ : fittedWidth + graphemeWidths[fitCount]!
69
+ const nextLineWidth = fitCount + 1 < graphemeWidths.length
70
+ ? nextWidth + discretionaryHyphenWidth
71
+ : nextWidth
72
+ if (nextLineWidth > maxWidth + lineFitEpsilon) break
73
+ fittedWidth = nextWidth
74
+ fitCount++
75
+ }
76
+
77
+ return { fitCount, fittedWidth }
78
+ }
79
+
80
+ function findChunkIndexForStart(prepared: PreparedLineBreakData, segmentIndex: number): number {
81
+ let lo = 0
82
+ let hi = prepared.chunks.length
83
+
84
+ while (lo < hi) {
85
+ const mid = Math.floor((lo + hi) / 2)
86
+ if (segmentIndex < prepared.chunks[mid]!.consumedEndSegmentIndex) {
87
+ hi = mid
88
+ } else {
89
+ lo = mid + 1
90
+ }
91
+ }
92
+
93
+ return lo < prepared.chunks.length ? lo : -1
94
+ }
95
+
96
+ function normalizeLineStartInChunk(
97
+ prepared: PreparedLineBreakData,
98
+ chunkIndex: number,
99
+ cursor: LineBreakCursor,
100
+ ): number {
101
+ let segmentIndex = cursor.segmentIndex
102
+ if (cursor.graphemeIndex > 0) return chunkIndex
103
+
104
+ const chunk = prepared.chunks[chunkIndex]!
105
+ if (chunk.startSegmentIndex === chunk.endSegmentIndex && segmentIndex === chunk.startSegmentIndex) {
106
+ cursor.segmentIndex = segmentIndex
107
+ cursor.graphemeIndex = 0
108
+ return chunkIndex
109
+ }
110
+
111
+ if (segmentIndex < chunk.startSegmentIndex) segmentIndex = chunk.startSegmentIndex
112
+ while (segmentIndex < chunk.endSegmentIndex) {
113
+ const kind = prepared.kinds[segmentIndex]!
114
+ if (kind !== 'space' && kind !== 'zero-width-break' && kind !== 'soft-hyphen') {
115
+ cursor.segmentIndex = segmentIndex
116
+ cursor.graphemeIndex = 0
117
+ return chunkIndex
118
+ }
119
+ segmentIndex++
120
+ }
121
+
122
+ if (chunk.consumedEndSegmentIndex >= prepared.widths.length) return -1
123
+ cursor.segmentIndex = chunk.consumedEndSegmentIndex
124
+ cursor.graphemeIndex = 0
125
+ return chunkIndex + 1
126
+ }
127
+
128
+ function normalizeLineStartChunkIndex(
129
+ prepared: PreparedLineBreakData,
130
+ cursor: LineBreakCursor,
131
+ ): number {
132
+ if (cursor.segmentIndex >= prepared.widths.length) return -1
133
+
134
+ const chunkIndex = findChunkIndexForStart(prepared, cursor.segmentIndex)
135
+ if (chunkIndex < 0) return -1
136
+ return normalizeLineStartInChunk(prepared, chunkIndex, cursor)
137
+ }
138
+
139
+ function normalizeLineStartChunkIndexFromHint(
140
+ prepared: PreparedLineBreakData,
141
+ chunkIndex: number,
142
+ cursor: LineBreakCursor,
143
+ ): number {
144
+ if (cursor.segmentIndex >= prepared.widths.length) return -1
145
+
146
+ let nextChunkIndex = chunkIndex
147
+ while (
148
+ nextChunkIndex < prepared.chunks.length &&
149
+ cursor.segmentIndex >= prepared.chunks[nextChunkIndex]!.consumedEndSegmentIndex
150
+ ) {
151
+ nextChunkIndex++
152
+ }
153
+ if (nextChunkIndex >= prepared.chunks.length) return -1
154
+ return normalizeLineStartInChunk(prepared, nextChunkIndex, cursor)
155
+ }
156
+
157
+ export function normalizeLineStart(
158
+ prepared: PreparedLineBreakData,
159
+ start: LineBreakCursor,
160
+ ): LineBreakCursor | null {
161
+ const cursor = {
162
+ segmentIndex: start.segmentIndex,
163
+ graphemeIndex: start.graphemeIndex,
164
+ }
165
+ const chunkIndex = normalizeLineStartChunkIndex(prepared, cursor)
166
+ return chunkIndex < 0 ? null : cursor
167
+ }
168
+
169
+ export function countPreparedLines(prepared: PreparedLineBreakData, maxWidth: number): number {
170
+ if (prepared.simpleLineWalkFastPath) {
171
+ return countPreparedLinesSimple(prepared, maxWidth)
172
+ }
173
+ return walkPreparedLines(prepared, maxWidth)
174
+ }
175
+
176
+ function countPreparedLinesSimple(prepared: PreparedLineBreakData, maxWidth: number): number {
177
+ return walkPreparedLinesSimple(prepared, maxWidth)
178
+ }
179
+
180
+ function walkPreparedLinesSimple(
181
+ prepared: PreparedLineBreakData,
182
+ maxWidth: number,
183
+ onLine?: (line: InternalLayoutLine) => void,
184
+ ): number {
185
+ const { widths, kinds, breakableWidths, breakablePrefixWidths } = prepared
186
+ if (widths.length === 0) return 0
187
+
188
+ const engineProfile = getEngineProfile()
189
+ const lineFitEpsilon = engineProfile.lineFitEpsilon
190
+ const fitLimit = maxWidth + lineFitEpsilon
191
+
192
+ let lineCount = 0
193
+ let lineW = 0
194
+ let hasContent = false
195
+ let lineStartSegmentIndex = 0
196
+ let lineStartGraphemeIndex = 0
197
+ let lineEndSegmentIndex = 0
198
+ let lineEndGraphemeIndex = 0
199
+ let pendingBreakSegmentIndex = -1
200
+ let pendingBreakPaintWidth = 0
201
+
202
+ function clearPendingBreak(): void {
203
+ pendingBreakSegmentIndex = -1
204
+ pendingBreakPaintWidth = 0
205
+ }
206
+
207
+ function emitCurrentLine(
208
+ endSegmentIndex = lineEndSegmentIndex,
209
+ endGraphemeIndex = lineEndGraphemeIndex,
210
+ width = lineW,
211
+ ): void {
212
+ lineCount++
213
+ onLine?.({
214
+ startSegmentIndex: lineStartSegmentIndex,
215
+ startGraphemeIndex: lineStartGraphemeIndex,
216
+ endSegmentIndex,
217
+ endGraphemeIndex,
218
+ width,
219
+ })
220
+ lineW = 0
221
+ hasContent = false
222
+ clearPendingBreak()
223
+ }
224
+
225
+ function startLineAtSegment(segmentIndex: number, width: number): void {
226
+ hasContent = true
227
+ lineStartSegmentIndex = segmentIndex
228
+ lineStartGraphemeIndex = 0
229
+ lineEndSegmentIndex = segmentIndex + 1
230
+ lineEndGraphemeIndex = 0
231
+ lineW = width
232
+ }
233
+
234
+ function startLineAtGrapheme(segmentIndex: number, graphemeIndex: number, width: number): void {
235
+ hasContent = true
236
+ lineStartSegmentIndex = segmentIndex
237
+ lineStartGraphemeIndex = graphemeIndex
238
+ lineEndSegmentIndex = segmentIndex
239
+ lineEndGraphemeIndex = graphemeIndex + 1
240
+ lineW = width
241
+ }
242
+
243
+ function appendWholeSegment(segmentIndex: number, width: number): void {
244
+ if (!hasContent) {
245
+ startLineAtSegment(segmentIndex, width)
246
+ return
247
+ }
248
+ lineW += width
249
+ lineEndSegmentIndex = segmentIndex + 1
250
+ lineEndGraphemeIndex = 0
251
+ }
252
+
253
+ function appendBreakableSegment(segmentIndex: number): void {
254
+ appendBreakableSegmentFrom(segmentIndex, 0)
255
+ }
256
+
257
+ function appendBreakableSegmentFrom(segmentIndex: number, startGraphemeIndex: number): void {
258
+ const gWidths = breakableWidths[segmentIndex]!
259
+ const gPrefixWidths = breakablePrefixWidths[segmentIndex] ?? null
260
+ let previousPrefixWidth =
261
+ gPrefixWidths === null || startGraphemeIndex === 0 ? 0 : gPrefixWidths[startGraphemeIndex - 1]!
262
+
263
+ for (let g = startGraphemeIndex; g < gWidths.length; g++) {
264
+ const gw = gPrefixWidths === null ? gWidths[g]! : gPrefixWidths[g]! - previousPrefixWidth
265
+
266
+ if (!hasContent) {
267
+ startLineAtGrapheme(segmentIndex, g, gw)
268
+ } else if (lineW + gw > fitLimit) {
269
+ emitCurrentLine()
270
+ startLineAtGrapheme(segmentIndex, g, gw)
271
+ } else {
272
+ lineW += gw
273
+ lineEndSegmentIndex = segmentIndex
274
+ lineEndGraphemeIndex = g + 1
275
+ }
276
+
277
+ if (gPrefixWidths !== null) previousPrefixWidth = gPrefixWidths[g]!
278
+ }
279
+
280
+ if (hasContent && lineEndSegmentIndex === segmentIndex && lineEndGraphemeIndex === gWidths.length) {
281
+ lineEndSegmentIndex = segmentIndex + 1
282
+ lineEndGraphemeIndex = 0
283
+ }
284
+ }
285
+
286
+ let i = 0
287
+ while (i < widths.length) {
288
+ if (!hasContent) {
289
+ i = normalizeSimpleLineStartSegmentIndex(prepared, i)
290
+ if (i >= widths.length) break
291
+ }
292
+
293
+ const w = widths[i]!
294
+ const kind = kinds[i]!
295
+ const breakAfter = kind === 'space' || kind === 'preserved-space' || kind === 'tab' || kind === 'zero-width-break' || kind === 'soft-hyphen'
296
+
297
+ if (!hasContent) {
298
+ if (w > maxWidth && breakableWidths[i] !== null) {
299
+ appendBreakableSegment(i)
300
+ } else {
301
+ startLineAtSegment(i, w)
302
+ }
303
+ if (breakAfter) {
304
+ pendingBreakSegmentIndex = i + 1
305
+ pendingBreakPaintWidth = lineW - w
306
+ }
307
+ i++
308
+ continue
309
+ }
310
+
311
+ const newW = lineW + w
312
+ if (newW > fitLimit) {
313
+ if (breakAfter) {
314
+ appendWholeSegment(i, w)
315
+ emitCurrentLine(i + 1, 0, lineW - w)
316
+ i++
317
+ continue
318
+ }
319
+
320
+ if (pendingBreakSegmentIndex >= 0) {
321
+ if (
322
+ lineEndSegmentIndex > pendingBreakSegmentIndex ||
323
+ (lineEndSegmentIndex === pendingBreakSegmentIndex && lineEndGraphemeIndex > 0)
324
+ ) {
325
+ emitCurrentLine()
326
+ continue
327
+ }
328
+ emitCurrentLine(pendingBreakSegmentIndex, 0, pendingBreakPaintWidth)
329
+ continue
330
+ }
331
+
332
+ if (w > maxWidth && breakableWidths[i] !== null) {
333
+ emitCurrentLine()
334
+ appendBreakableSegment(i)
335
+ i++
336
+ continue
337
+ }
338
+
339
+ emitCurrentLine()
340
+ continue
341
+ }
342
+
343
+ appendWholeSegment(i, w)
344
+ if (breakAfter) {
345
+ pendingBreakSegmentIndex = i + 1
346
+ pendingBreakPaintWidth = lineW - w
347
+ }
348
+ i++
349
+ }
350
+
351
+ if (hasContent) emitCurrentLine()
352
+ return lineCount
353
+ }
354
+
355
+ export function walkPreparedLines(
356
+ prepared: PreparedLineBreakData,
357
+ maxWidth: number,
358
+ onLine?: (line: InternalLayoutLine) => void,
359
+ ): number {
360
+ if (prepared.simpleLineWalkFastPath) {
361
+ return walkPreparedLinesSimple(prepared, maxWidth, onLine)
362
+ }
363
+
364
+ const {
365
+ widths,
366
+ lineEndFitAdvances,
367
+ lineEndPaintAdvances,
368
+ kinds,
369
+ breakableWidths,
370
+ breakablePrefixWidths,
371
+ discretionaryHyphenWidth,
372
+ tabStopAdvance,
373
+ chunks,
374
+ } = prepared
375
+ if (widths.length === 0 || chunks.length === 0) return 0
376
+
377
+ const engineProfile = getEngineProfile()
378
+ const lineFitEpsilon = engineProfile.lineFitEpsilon
379
+ const fitLimit = maxWidth + lineFitEpsilon
380
+
381
+ let lineCount = 0
382
+ let lineW = 0
383
+ let hasContent = false
384
+ let lineStartSegmentIndex = 0
385
+ let lineStartGraphemeIndex = 0
386
+ let lineEndSegmentIndex = 0
387
+ let lineEndGraphemeIndex = 0
388
+ let pendingBreakSegmentIndex = -1
389
+ let pendingBreakFitWidth = 0
390
+ let pendingBreakPaintWidth = 0
391
+ let pendingBreakKind: SegmentBreakKind | null = null
392
+
393
+ function clearPendingBreak(): void {
394
+ pendingBreakSegmentIndex = -1
395
+ pendingBreakFitWidth = 0
396
+ pendingBreakPaintWidth = 0
397
+ pendingBreakKind = null
398
+ }
399
+
400
+ function emitCurrentLine(
401
+ endSegmentIndex = lineEndSegmentIndex,
402
+ endGraphemeIndex = lineEndGraphemeIndex,
403
+ width = lineW,
404
+ ): void {
405
+ lineCount++
406
+ onLine?.({
407
+ startSegmentIndex: lineStartSegmentIndex,
408
+ startGraphemeIndex: lineStartGraphemeIndex,
409
+ endSegmentIndex,
410
+ endGraphemeIndex,
411
+ width,
412
+ })
413
+ lineW = 0
414
+ hasContent = false
415
+ clearPendingBreak()
416
+ }
417
+
418
+ function startLineAtSegment(segmentIndex: number, width: number): void {
419
+ hasContent = true
420
+ lineStartSegmentIndex = segmentIndex
421
+ lineStartGraphemeIndex = 0
422
+ lineEndSegmentIndex = segmentIndex + 1
423
+ lineEndGraphemeIndex = 0
424
+ lineW = width
425
+ }
426
+
427
+ function startLineAtGrapheme(segmentIndex: number, graphemeIndex: number, width: number): void {
428
+ hasContent = true
429
+ lineStartSegmentIndex = segmentIndex
430
+ lineStartGraphemeIndex = graphemeIndex
431
+ lineEndSegmentIndex = segmentIndex
432
+ lineEndGraphemeIndex = graphemeIndex + 1
433
+ lineW = width
434
+ }
435
+
436
+ function appendWholeSegment(segmentIndex: number, width: number): void {
437
+ if (!hasContent) {
438
+ startLineAtSegment(segmentIndex, width)
439
+ return
440
+ }
441
+ lineW += width
442
+ lineEndSegmentIndex = segmentIndex + 1
443
+ lineEndGraphemeIndex = 0
444
+ }
445
+
446
+ function updatePendingBreakForWholeSegment(
447
+ kind: SegmentBreakKind,
448
+ breakAfter: boolean,
449
+ segmentIndex: number,
450
+ segmentWidth: number,
451
+ ): void {
452
+ if (!breakAfter) return
453
+ const fitAdvance = kind === 'tab' ? 0 : lineEndFitAdvances[segmentIndex]!
454
+ const paintAdvance = kind === 'tab' ? segmentWidth : lineEndPaintAdvances[segmentIndex]!
455
+ pendingBreakSegmentIndex = segmentIndex + 1
456
+ pendingBreakFitWidth = lineW - segmentWidth + fitAdvance
457
+ pendingBreakPaintWidth = lineW - segmentWidth + paintAdvance
458
+ pendingBreakKind = kind
459
+ }
460
+
461
+ function appendBreakableSegment(segmentIndex: number): void {
462
+ appendBreakableSegmentFrom(segmentIndex, 0)
463
+ }
464
+
465
+ function appendBreakableSegmentFrom(segmentIndex: number, startGraphemeIndex: number): void {
466
+ const gWidths = breakableWidths[segmentIndex]!
467
+ const gPrefixWidths = breakablePrefixWidths[segmentIndex] ?? null
468
+ let previousPrefixWidth =
469
+ gPrefixWidths === null || startGraphemeIndex === 0 ? 0 : gPrefixWidths[startGraphemeIndex - 1]!
470
+
471
+ for (let g = startGraphemeIndex; g < gWidths.length; g++) {
472
+ const gw = gPrefixWidths === null ? gWidths[g]! : gPrefixWidths[g]! - previousPrefixWidth
473
+
474
+ if (!hasContent) {
475
+ startLineAtGrapheme(segmentIndex, g, gw)
476
+ } else if (lineW + gw > fitLimit) {
477
+ emitCurrentLine()
478
+ startLineAtGrapheme(segmentIndex, g, gw)
479
+ } else {
480
+ lineW += gw
481
+ lineEndSegmentIndex = segmentIndex
482
+ lineEndGraphemeIndex = g + 1
483
+ }
484
+
485
+ if (gPrefixWidths !== null) previousPrefixWidth = gPrefixWidths[g]!
486
+ }
487
+
488
+ if (hasContent && lineEndSegmentIndex === segmentIndex && lineEndGraphemeIndex === gWidths.length) {
489
+ lineEndSegmentIndex = segmentIndex + 1
490
+ lineEndGraphemeIndex = 0
491
+ }
492
+ }
493
+
494
+ function continueSoftHyphenBreakableSegment(segmentIndex: number): boolean {
495
+ if (pendingBreakKind !== 'soft-hyphen') return false
496
+ const gWidths = breakableWidths[segmentIndex]!
497
+ if (gWidths === null) return false
498
+ const fitWidths = breakablePrefixWidths[segmentIndex] ?? gWidths
499
+ const usesPrefixWidths = fitWidths !== gWidths
500
+ const { fitCount, fittedWidth } = fitSoftHyphenBreak(
501
+ fitWidths,
502
+ lineW,
503
+ maxWidth,
504
+ lineFitEpsilon,
505
+ discretionaryHyphenWidth,
506
+ usesPrefixWidths,
507
+ )
508
+ if (fitCount === 0) return false
509
+
510
+ lineW = fittedWidth
511
+ lineEndSegmentIndex = segmentIndex
512
+ lineEndGraphemeIndex = fitCount
513
+ clearPendingBreak()
514
+
515
+ if (fitCount === gWidths.length) {
516
+ lineEndSegmentIndex = segmentIndex + 1
517
+ lineEndGraphemeIndex = 0
518
+ return true
519
+ }
520
+
521
+ emitCurrentLine(
522
+ segmentIndex,
523
+ fitCount,
524
+ fittedWidth + discretionaryHyphenWidth,
525
+ )
526
+ appendBreakableSegmentFrom(segmentIndex, fitCount)
527
+ return true
528
+ }
529
+
530
+ function emitEmptyChunk(chunk: { startSegmentIndex: number, consumedEndSegmentIndex: number }): void {
531
+ lineCount++
532
+ onLine?.({
533
+ startSegmentIndex: chunk.startSegmentIndex,
534
+ startGraphemeIndex: 0,
535
+ endSegmentIndex: chunk.consumedEndSegmentIndex,
536
+ endGraphemeIndex: 0,
537
+ width: 0,
538
+ })
539
+ clearPendingBreak()
540
+ }
541
+
542
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
543
+ const chunk = chunks[chunkIndex]!
544
+ if (chunk.startSegmentIndex === chunk.endSegmentIndex) {
545
+ emitEmptyChunk(chunk)
546
+ continue
547
+ }
548
+
549
+ hasContent = false
550
+ lineW = 0
551
+ lineStartSegmentIndex = chunk.startSegmentIndex
552
+ lineStartGraphemeIndex = 0
553
+ lineEndSegmentIndex = chunk.startSegmentIndex
554
+ lineEndGraphemeIndex = 0
555
+ clearPendingBreak()
556
+
557
+ let i = chunk.startSegmentIndex
558
+ while (i < chunk.endSegmentIndex) {
559
+ const kind = kinds[i]!
560
+ const breakAfter = kind === 'space' || kind === 'preserved-space' || kind === 'tab' || kind === 'zero-width-break' || kind === 'soft-hyphen'
561
+ const w = kind === 'tab' ? getTabAdvance(lineW, tabStopAdvance) : widths[i]!
562
+
563
+ if (kind === 'soft-hyphen') {
564
+ if (hasContent) {
565
+ lineEndSegmentIndex = i + 1
566
+ lineEndGraphemeIndex = 0
567
+ pendingBreakSegmentIndex = i + 1
568
+ pendingBreakFitWidth = lineW + discretionaryHyphenWidth
569
+ pendingBreakPaintWidth = lineW + discretionaryHyphenWidth
570
+ pendingBreakKind = kind
571
+ }
572
+ i++
573
+ continue
574
+ }
575
+
576
+ if (!hasContent) {
577
+ if (w > maxWidth && breakableWidths[i] !== null) {
578
+ appendBreakableSegment(i)
579
+ } else {
580
+ startLineAtSegment(i, w)
581
+ }
582
+ updatePendingBreakForWholeSegment(kind, breakAfter, i, w)
583
+ i++
584
+ continue
585
+ }
586
+
587
+ const newW = lineW + w
588
+ if (newW > fitLimit) {
589
+ const currentBreakFitWidth = lineW + (kind === 'tab' ? 0 : lineEndFitAdvances[i]!)
590
+ const currentBreakPaintWidth = lineW + (kind === 'tab' ? w : lineEndPaintAdvances[i]!)
591
+
592
+ if (
593
+ pendingBreakKind === 'soft-hyphen' &&
594
+ engineProfile.preferEarlySoftHyphenBreak &&
595
+ pendingBreakFitWidth <= fitLimit
596
+ ) {
597
+ emitCurrentLine(pendingBreakSegmentIndex, 0, pendingBreakPaintWidth)
598
+ continue
599
+ }
600
+
601
+ if (pendingBreakKind === 'soft-hyphen' && continueSoftHyphenBreakableSegment(i)) {
602
+ i++
603
+ continue
604
+ }
605
+
606
+ if (breakAfter && currentBreakFitWidth <= fitLimit) {
607
+ appendWholeSegment(i, w)
608
+ emitCurrentLine(i + 1, 0, currentBreakPaintWidth)
609
+ i++
610
+ continue
611
+ }
612
+
613
+ if (pendingBreakSegmentIndex >= 0 && pendingBreakFitWidth <= fitLimit) {
614
+ if (
615
+ lineEndSegmentIndex > pendingBreakSegmentIndex ||
616
+ (lineEndSegmentIndex === pendingBreakSegmentIndex && lineEndGraphemeIndex > 0)
617
+ ) {
618
+ emitCurrentLine()
619
+ continue
620
+ }
621
+ const nextSegmentIndex = pendingBreakSegmentIndex
622
+ emitCurrentLine(nextSegmentIndex, 0, pendingBreakPaintWidth)
623
+ i = nextSegmentIndex
624
+ continue
625
+ }
626
+
627
+ if (w > maxWidth && breakableWidths[i] !== null) {
628
+ emitCurrentLine()
629
+ appendBreakableSegment(i)
630
+ i++
631
+ continue
632
+ }
633
+
634
+ emitCurrentLine()
635
+ continue
636
+ }
637
+
638
+ appendWholeSegment(i, w)
639
+ updatePendingBreakForWholeSegment(kind, breakAfter, i, w)
640
+ i++
641
+ }
642
+
643
+ if (hasContent) {
644
+ const finalPaintWidth =
645
+ pendingBreakSegmentIndex === chunk.consumedEndSegmentIndex
646
+ ? pendingBreakPaintWidth
647
+ : lineW
648
+ emitCurrentLine(chunk.consumedEndSegmentIndex, 0, finalPaintWidth)
649
+ }
650
+ }
651
+
652
+ return lineCount
653
+ }
654
+
655
+ function stepPreparedChunkLineGeometry(
656
+ prepared: PreparedLineBreakData,
657
+ cursor: LineBreakCursor,
658
+ chunkIndex: number,
659
+ maxWidth: number,
660
+ ): number | null {
661
+ const chunk = prepared.chunks[chunkIndex]!
662
+ if (chunk.startSegmentIndex === chunk.endSegmentIndex) {
663
+ cursor.segmentIndex = chunk.consumedEndSegmentIndex
664
+ cursor.graphemeIndex = 0
665
+ return 0
666
+ }
667
+
668
+ const {
669
+ widths,
670
+ lineEndFitAdvances,
671
+ lineEndPaintAdvances,
672
+ kinds,
673
+ breakableWidths,
674
+ breakablePrefixWidths,
675
+ discretionaryHyphenWidth,
676
+ tabStopAdvance,
677
+ } = prepared
678
+ const engineProfile = getEngineProfile()
679
+ const lineFitEpsilon = engineProfile.lineFitEpsilon
680
+ const fitLimit = maxWidth + lineFitEpsilon
681
+
682
+ let lineW = 0
683
+ let hasContent = false
684
+ let lineEndSegmentIndex = cursor.segmentIndex
685
+ let lineEndGraphemeIndex = cursor.graphemeIndex
686
+ let pendingBreakSegmentIndex = -1
687
+ let pendingBreakFitWidth = 0
688
+ let pendingBreakPaintWidth = 0
689
+ let pendingBreakKind: SegmentBreakKind | null = null
690
+
691
+ function clearPendingBreak(): void {
692
+ pendingBreakSegmentIndex = -1
693
+ pendingBreakFitWidth = 0
694
+ pendingBreakPaintWidth = 0
695
+ pendingBreakKind = null
696
+ }
697
+
698
+ function finishLine(
699
+ endSegmentIndex = lineEndSegmentIndex,
700
+ endGraphemeIndex = lineEndGraphemeIndex,
701
+ width = lineW,
702
+ ): number | null {
703
+ if (!hasContent) return null
704
+ cursor.segmentIndex = endSegmentIndex
705
+ cursor.graphemeIndex = endGraphemeIndex
706
+ return width
707
+ }
708
+
709
+ function startLineAtSegment(segmentIndex: number, width: number): void {
710
+ hasContent = true
711
+ lineEndSegmentIndex = segmentIndex + 1
712
+ lineEndGraphemeIndex = 0
713
+ lineW = width
714
+ }
715
+
716
+ function startLineAtGrapheme(segmentIndex: number, graphemeIndex: number, width: number): void {
717
+ hasContent = true
718
+ lineEndSegmentIndex = segmentIndex
719
+ lineEndGraphemeIndex = graphemeIndex + 1
720
+ lineW = width
721
+ }
722
+
723
+ function appendWholeSegment(segmentIndex: number, width: number): void {
724
+ if (!hasContent) {
725
+ startLineAtSegment(segmentIndex, width)
726
+ return
727
+ }
728
+ lineW += width
729
+ lineEndSegmentIndex = segmentIndex + 1
730
+ lineEndGraphemeIndex = 0
731
+ }
732
+
733
+ function updatePendingBreakForWholeSegment(
734
+ kind: SegmentBreakKind,
735
+ breakAfter: boolean,
736
+ segmentIndex: number,
737
+ segmentWidth: number,
738
+ ): void {
739
+ if (!breakAfter) return
740
+ const fitAdvance = kind === 'tab' ? 0 : lineEndFitAdvances[segmentIndex]!
741
+ const paintAdvance = kind === 'tab' ? segmentWidth : lineEndPaintAdvances[segmentIndex]!
742
+ pendingBreakSegmentIndex = segmentIndex + 1
743
+ pendingBreakFitWidth = lineW - segmentWidth + fitAdvance
744
+ pendingBreakPaintWidth = lineW - segmentWidth + paintAdvance
745
+ pendingBreakKind = kind
746
+ }
747
+
748
+ function appendBreakableSegmentFrom(segmentIndex: number, startGraphemeIndex: number): number | null {
749
+ const gWidths = breakableWidths[segmentIndex]!
750
+ const gPrefixWidths = breakablePrefixWidths[segmentIndex] ?? null
751
+ let previousPrefixWidth =
752
+ gPrefixWidths === null || startGraphemeIndex === 0 ? 0 : gPrefixWidths[startGraphemeIndex - 1]!
753
+
754
+ for (let g = startGraphemeIndex; g < gWidths.length; g++) {
755
+ const gw = gPrefixWidths === null ? gWidths[g]! : gPrefixWidths[g]! - previousPrefixWidth
756
+
757
+ if (!hasContent) {
758
+ startLineAtGrapheme(segmentIndex, g, gw)
759
+ } else {
760
+ if (lineW + gw > fitLimit) {
761
+ return finishLine()
762
+ }
763
+
764
+ lineW += gw
765
+ lineEndSegmentIndex = segmentIndex
766
+ lineEndGraphemeIndex = g + 1
767
+ }
768
+
769
+ if (gPrefixWidths !== null) previousPrefixWidth = gPrefixWidths[g]!
770
+ }
771
+
772
+ if (hasContent && lineEndSegmentIndex === segmentIndex && lineEndGraphemeIndex === gWidths.length) {
773
+ lineEndSegmentIndex = segmentIndex + 1
774
+ lineEndGraphemeIndex = 0
775
+ }
776
+ return null
777
+ }
778
+
779
+ function maybeFinishAtSoftHyphen(segmentIndex: number): number | null {
780
+ if (pendingBreakKind !== 'soft-hyphen' || pendingBreakSegmentIndex < 0) return null
781
+
782
+ const gWidths = breakableWidths[segmentIndex] ?? null
783
+ if (gWidths !== null) {
784
+ const fitWidths = breakablePrefixWidths[segmentIndex] ?? gWidths
785
+ const usesPrefixWidths = fitWidths !== gWidths
786
+ const { fitCount, fittedWidth } = fitSoftHyphenBreak(
787
+ fitWidths,
788
+ lineW,
789
+ maxWidth,
790
+ lineFitEpsilon,
791
+ discretionaryHyphenWidth,
792
+ usesPrefixWidths,
793
+ )
794
+
795
+ if (fitCount === gWidths.length) {
796
+ lineW = fittedWidth
797
+ lineEndSegmentIndex = segmentIndex + 1
798
+ lineEndGraphemeIndex = 0
799
+ clearPendingBreak()
800
+ return null
801
+ }
802
+
803
+ if (fitCount > 0) {
804
+ return finishLine(
805
+ segmentIndex,
806
+ fitCount,
807
+ fittedWidth + discretionaryHyphenWidth,
808
+ )
809
+ }
810
+ }
811
+
812
+ if (pendingBreakFitWidth <= fitLimit) {
813
+ return finishLine(pendingBreakSegmentIndex, 0, pendingBreakPaintWidth)
814
+ }
815
+
816
+ return null
817
+ }
818
+
819
+ for (let i = cursor.segmentIndex; i < chunk.endSegmentIndex; i++) {
820
+ const kind = kinds[i]!
821
+ const breakAfter = kind === 'space' || kind === 'preserved-space' || kind === 'tab' || kind === 'zero-width-break' || kind === 'soft-hyphen'
822
+ const startGraphemeIndex = i === cursor.segmentIndex ? cursor.graphemeIndex : 0
823
+ const w = kind === 'tab' ? getTabAdvance(lineW, tabStopAdvance) : widths[i]!
824
+
825
+ if (kind === 'soft-hyphen' && startGraphemeIndex === 0) {
826
+ if (hasContent) {
827
+ lineEndSegmentIndex = i + 1
828
+ lineEndGraphemeIndex = 0
829
+ pendingBreakSegmentIndex = i + 1
830
+ pendingBreakFitWidth = lineW + discretionaryHyphenWidth
831
+ pendingBreakPaintWidth = lineW + discretionaryHyphenWidth
832
+ pendingBreakKind = kind
833
+ }
834
+ continue
835
+ }
836
+
837
+ if (!hasContent) {
838
+ if (startGraphemeIndex > 0) {
839
+ const line = appendBreakableSegmentFrom(i, startGraphemeIndex)
840
+ if (line !== null) return line
841
+ } else if (w > maxWidth && breakableWidths[i] !== null) {
842
+ const line = appendBreakableSegmentFrom(i, 0)
843
+ if (line !== null) return line
844
+ } else {
845
+ startLineAtSegment(i, w)
846
+ }
847
+ updatePendingBreakForWholeSegment(kind, breakAfter, i, w)
848
+ continue
849
+ }
850
+
851
+ const newW = lineW + w
852
+ if (newW > fitLimit) {
853
+ const currentBreakFitWidth = lineW + (kind === 'tab' ? 0 : lineEndFitAdvances[i]!)
854
+ const currentBreakPaintWidth = lineW + (kind === 'tab' ? w : lineEndPaintAdvances[i]!)
855
+
856
+ if (
857
+ pendingBreakKind === 'soft-hyphen' &&
858
+ engineProfile.preferEarlySoftHyphenBreak &&
859
+ pendingBreakFitWidth <= fitLimit
860
+ ) {
861
+ return finishLine(pendingBreakSegmentIndex, 0, pendingBreakPaintWidth)
862
+ }
863
+
864
+ const softBreakLine = maybeFinishAtSoftHyphen(i)
865
+ if (softBreakLine !== null) return softBreakLine
866
+
867
+ if (breakAfter && currentBreakFitWidth <= fitLimit) {
868
+ appendWholeSegment(i, w)
869
+ return finishLine(i + 1, 0, currentBreakPaintWidth)
870
+ }
871
+
872
+ if (pendingBreakSegmentIndex >= 0 && pendingBreakFitWidth <= fitLimit) {
873
+ if (
874
+ lineEndSegmentIndex > pendingBreakSegmentIndex ||
875
+ (lineEndSegmentIndex === pendingBreakSegmentIndex && lineEndGraphemeIndex > 0)
876
+ ) {
877
+ return finishLine()
878
+ }
879
+ return finishLine(pendingBreakSegmentIndex, 0, pendingBreakPaintWidth)
880
+ }
881
+
882
+ if (w > maxWidth && breakableWidths[i] !== null) {
883
+ const currentLine = finishLine()
884
+ if (currentLine !== null) return currentLine
885
+ const line = appendBreakableSegmentFrom(i, 0)
886
+ if (line !== null) return line
887
+ }
888
+
889
+ return finishLine()
890
+ }
891
+
892
+ appendWholeSegment(i, w)
893
+ updatePendingBreakForWholeSegment(kind, breakAfter, i, w)
894
+ }
895
+
896
+ if (pendingBreakSegmentIndex === chunk.consumedEndSegmentIndex && lineEndGraphemeIndex === 0) {
897
+ return finishLine(chunk.consumedEndSegmentIndex, 0, pendingBreakPaintWidth)
898
+ }
899
+
900
+ return finishLine(chunk.consumedEndSegmentIndex, 0, lineW)
901
+ }
902
+
903
+ function stepPreparedSimpleLineGeometry(
904
+ prepared: PreparedLineBreakData,
905
+ cursor: LineBreakCursor,
906
+ maxWidth: number,
907
+ ): number | null {
908
+ const { widths, kinds, breakableWidths, breakablePrefixWidths } = prepared
909
+ const engineProfile = getEngineProfile()
910
+ const lineFitEpsilon = engineProfile.lineFitEpsilon
911
+ const fitLimit = maxWidth + lineFitEpsilon
912
+
913
+ let lineW = 0
914
+ let hasContent = false
915
+ let lineEndSegmentIndex = cursor.segmentIndex
916
+ let lineEndGraphemeIndex = cursor.graphemeIndex
917
+ let pendingBreakSegmentIndex = -1
918
+ let pendingBreakPaintWidth = 0
919
+
920
+ for (let i = cursor.segmentIndex; i < widths.length; i++) {
921
+ const w = widths[i]!
922
+ const kind = kinds[i]!
923
+ const breakAfter = kind === 'space' || kind === 'preserved-space' || kind === 'tab' || kind === 'zero-width-break' || kind === 'soft-hyphen'
924
+ const startGraphemeIndex = i === cursor.segmentIndex ? cursor.graphemeIndex : 0
925
+ const breakableWidth = breakableWidths[i]
926
+
927
+ if (!hasContent) {
928
+ if (startGraphemeIndex > 0 || (w > maxWidth && breakableWidth !== null)) {
929
+ const gWidths = breakableWidth!
930
+ const gPrefixWidths = breakablePrefixWidths[i] ?? null
931
+ let previousPrefixWidth =
932
+ gPrefixWidths === null || startGraphemeIndex === 0
933
+ ? 0
934
+ : gPrefixWidths[startGraphemeIndex - 1]!
935
+ const firstGraphemeWidth =
936
+ gPrefixWidths === null
937
+ ? gWidths[startGraphemeIndex]!
938
+ : gPrefixWidths[startGraphemeIndex]! - previousPrefixWidth
939
+
940
+ hasContent = true
941
+ lineW = firstGraphemeWidth
942
+ lineEndSegmentIndex = i
943
+ lineEndGraphemeIndex = startGraphemeIndex + 1
944
+
945
+ if (gPrefixWidths !== null) previousPrefixWidth = gPrefixWidths[startGraphemeIndex]!
946
+
947
+ for (let g = startGraphemeIndex + 1; g < gWidths.length; g++) {
948
+ const gw = gPrefixWidths === null ? gWidths[g]! : gPrefixWidths[g]! - previousPrefixWidth
949
+ if (lineW + gw > fitLimit) {
950
+ cursor.segmentIndex = lineEndSegmentIndex
951
+ cursor.graphemeIndex = lineEndGraphemeIndex
952
+ return lineW
953
+ }
954
+ lineW += gw
955
+ lineEndSegmentIndex = i
956
+ lineEndGraphemeIndex = g + 1
957
+ if (gPrefixWidths !== null) previousPrefixWidth = gPrefixWidths[g]!
958
+ }
959
+
960
+ if (lineEndSegmentIndex === i && lineEndGraphemeIndex === gWidths.length) {
961
+ lineEndSegmentIndex = i + 1
962
+ lineEndGraphemeIndex = 0
963
+ }
964
+ } else {
965
+ hasContent = true
966
+ lineW = w
967
+ lineEndSegmentIndex = i + 1
968
+ lineEndGraphemeIndex = 0
969
+ }
970
+ if (breakAfter) {
971
+ pendingBreakSegmentIndex = i + 1
972
+ pendingBreakPaintWidth = lineW - w
973
+ }
974
+ continue
975
+ }
976
+
977
+ if (lineW + w > fitLimit) {
978
+ if (breakAfter) {
979
+ cursor.segmentIndex = i + 1
980
+ cursor.graphemeIndex = 0
981
+ return lineW
982
+ }
983
+
984
+ if (pendingBreakSegmentIndex >= 0) {
985
+ if (
986
+ lineEndSegmentIndex > pendingBreakSegmentIndex ||
987
+ (lineEndSegmentIndex === pendingBreakSegmentIndex && lineEndGraphemeIndex > 0)
988
+ ) {
989
+ cursor.segmentIndex = lineEndSegmentIndex
990
+ cursor.graphemeIndex = lineEndGraphemeIndex
991
+ return lineW
992
+ }
993
+ cursor.segmentIndex = pendingBreakSegmentIndex
994
+ cursor.graphemeIndex = 0
995
+ return pendingBreakPaintWidth
996
+ }
997
+
998
+ cursor.segmentIndex = lineEndSegmentIndex
999
+ cursor.graphemeIndex = lineEndGraphemeIndex
1000
+ return lineW
1001
+ }
1002
+
1003
+ lineW += w
1004
+ lineEndSegmentIndex = i + 1
1005
+ lineEndGraphemeIndex = 0
1006
+ if (breakAfter) {
1007
+ pendingBreakSegmentIndex = i + 1
1008
+ pendingBreakPaintWidth = lineW - w
1009
+ }
1010
+ }
1011
+
1012
+ if (!hasContent) return null
1013
+ cursor.segmentIndex = lineEndSegmentIndex
1014
+ cursor.graphemeIndex = lineEndGraphemeIndex
1015
+ return lineW
1016
+ }
1017
+
1018
+ export function layoutNextLineRange(
1019
+ prepared: PreparedLineBreakData,
1020
+ start: LineBreakCursor,
1021
+ maxWidth: number,
1022
+ ): InternalLayoutLine | null {
1023
+ const end: LineBreakCursor = {
1024
+ segmentIndex: start.segmentIndex,
1025
+ graphemeIndex: start.graphemeIndex,
1026
+ }
1027
+ const chunkIndex = normalizeLineStartChunkIndex(prepared, end)
1028
+ if (chunkIndex < 0) return null
1029
+
1030
+ const lineStartSegmentIndex = end.segmentIndex
1031
+ const lineStartGraphemeIndex = end.graphemeIndex
1032
+ const width = prepared.simpleLineWalkFastPath
1033
+ ? stepPreparedSimpleLineGeometry(prepared, end, maxWidth)
1034
+ : stepPreparedChunkLineGeometry(prepared, end, chunkIndex, maxWidth)
1035
+ if (width === null) return null
1036
+
1037
+ return {
1038
+ startSegmentIndex: lineStartSegmentIndex,
1039
+ startGraphemeIndex: lineStartGraphemeIndex,
1040
+ endSegmentIndex: end.segmentIndex,
1041
+ endGraphemeIndex: end.graphemeIndex,
1042
+ width,
1043
+ }
1044
+ }
1045
+
1046
+ export function stepPreparedLineGeometry(
1047
+ prepared: PreparedLineBreakData,
1048
+ cursor: LineBreakCursor,
1049
+ maxWidth: number,
1050
+ ): number | null {
1051
+ const chunkIndex = normalizeLineStartChunkIndex(prepared, cursor)
1052
+ if (chunkIndex < 0) return null
1053
+
1054
+ if (prepared.simpleLineWalkFastPath) {
1055
+ return stepPreparedSimpleLineGeometry(prepared, cursor, maxWidth)
1056
+ }
1057
+
1058
+ return stepPreparedChunkLineGeometry(prepared, cursor, chunkIndex, maxWidth)
1059
+ }
1060
+
1061
+ export function measurePreparedLineGeometry(
1062
+ prepared: PreparedLineBreakData,
1063
+ maxWidth: number,
1064
+ ): {
1065
+ lineCount: number
1066
+ maxLineWidth: number
1067
+ } {
1068
+ if (prepared.widths.length === 0) {
1069
+ return {
1070
+ lineCount: 0,
1071
+ maxLineWidth: 0,
1072
+ }
1073
+ }
1074
+
1075
+ const cursor: LineBreakCursor = {
1076
+ segmentIndex: 0,
1077
+ graphemeIndex: 0,
1078
+ }
1079
+ let lineCount = 0
1080
+ let maxLineWidth = 0
1081
+
1082
+ if (!prepared.simpleLineWalkFastPath) {
1083
+ let chunkIndex = normalizeLineStartChunkIndex(prepared, cursor)
1084
+ while (chunkIndex >= 0) {
1085
+ const lineWidth = stepPreparedChunkLineGeometry(prepared, cursor, chunkIndex, maxWidth)
1086
+ if (lineWidth === null) {
1087
+ return {
1088
+ lineCount,
1089
+ maxLineWidth,
1090
+ }
1091
+ }
1092
+ lineCount++
1093
+ if (lineWidth > maxLineWidth) maxLineWidth = lineWidth
1094
+ chunkIndex = normalizeLineStartChunkIndexFromHint(prepared, chunkIndex, cursor)
1095
+ }
1096
+ return {
1097
+ lineCount,
1098
+ maxLineWidth,
1099
+ }
1100
+ }
1101
+
1102
+ while (true) {
1103
+ const lineWidth = stepPreparedLineGeometry(prepared, cursor, maxWidth)
1104
+ if (lineWidth === null) {
1105
+ return {
1106
+ lineCount,
1107
+ maxLineWidth,
1108
+ }
1109
+ }
1110
+ lineCount++
1111
+ if (lineWidth > maxLineWidth) maxLineWidth = lineWidth
1112
+ }
1113
+ }