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.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/android/src/main/java/expo/modules/pretext/ExpoPretextModule.kt +354 -0
- package/expo-module.config.json +9 -0
- package/ios/ExpoPretext.podspec +20 -0
- package/ios/ExpoPretext.swift +444 -0
- package/package.json +59 -0
- package/src/ExpoPretext.ts +70 -0
- package/src/__tests__/cache.test.ts +71 -0
- package/src/__tests__/font-utils.test.ts +69 -0
- package/src/__tests__/layout.test.ts +300 -0
- package/src/__tests__/obstacle-layout.test.ts +127 -0
- package/src/__tests__/setup-mocks.ts +14 -0
- package/src/analysis.ts +1208 -0
- package/src/bidi.ts +175 -0
- package/src/build.ts +503 -0
- package/src/cache.ts +59 -0
- package/src/engine-profile.ts +38 -0
- package/src/font-utils.ts +50 -0
- package/src/generated/bidi-data.ts +998 -0
- package/src/hooks/useFlashListHeights.ts +88 -0
- package/src/hooks/usePreparedText.ts +16 -0
- package/src/hooks/useTextHeight.ts +45 -0
- package/src/index.ts +56 -0
- package/src/layout.ts +353 -0
- package/src/line-break.ts +1113 -0
- package/src/obstacle-layout.ts +193 -0
- package/src/prepare.ts +246 -0
- package/src/rich-inline.ts +647 -0
- package/src/streaming.ts +61 -0
- package/src/types.ts +104 -0
|
@@ -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
|
+
}
|