1import { getEngineProfile } from './measurement.js';2// End cursors consume source. A terminal SHY is not a selected wrap, even3// though it is the final consumed segment. Rendering derives that distinction4// from the endpoint instead of treating every consumed SHY as visible.5export function isDiscretionaryLineEnd(kinds, endSegmentIndex, endGraphemeIndex) {6 return endGraphemeIndex === 0 && endSegmentIndex > 0 && endSegmentIndex < kinds.length && kinds[endSegmentIndex - 1] === 'soft-hyphen';7}8function consumesAtLineStart(kind) {9 return kind === 'space' || kind === 'zero-width-break' || kind === 'soft-hyphen';10}11function breaksAfter(kind) {12 return (kind === 'space' ||13 kind === 'preserved-space' ||14 kind === 'tab' ||15 kind === 'zero-width-break' ||16 kind === 'soft-hyphen');17}18function normalizeLineStartSegmentIndex(prepared, segmentIndex, endSegmentIndex = prepared.widths.length) {19 while (segmentIndex < endSegmentIndex) {20 const kind = prepared.kinds[segmentIndex];21 if (!consumesAtLineStart(kind))22 break;23 segmentIndex++;24 }25 return segmentIndex;26}27function getTabAdvance(lineWidth, tabStopAdvance) {28 if (tabStopAdvance <= 0)29 return 0;30 const remainder = lineWidth % tabStopAdvance;31 if (Math.abs(remainder) <= 1e-6)32 return tabStopAdvance;33 return tabStopAdvance - remainder;34}35function getLeadingLetterSpacing(prepared, hasContent, segmentIndex) {36 return (prepared.letterSpacing !== 0 &&37 hasContent &&38 prepared.spacingGraphemeCounts[segmentIndex] > 0)39 ? prepared.letterSpacing40 : 0;41}42function getLineEndContribution(leadingSpacing, segmentContribution) {43 return segmentContribution === 0 ? 0 : leadingSpacing + segmentContribution;44}45function getTabTrailingLetterSpacing(prepared, segmentIndex) {46 return (prepared.letterSpacing !== 0 &&47 prepared.spacingGraphemeCounts[segmentIndex] > 0)48 ? prepared.letterSpacing49 : 0;50}51function getWholeSegmentFitContribution(prepared, kind, segmentIndex, leadingSpacing, segmentWidth) {52 const segmentContribution = kind === 'tab'53 ? segmentWidth + getTabTrailingLetterSpacing(prepared, segmentIndex)54 : prepared.lineEndFitAdvances[segmentIndex];55 return getLineEndContribution(leadingSpacing, segmentContribution);56}57function getBreakOpportunityFitContribution(prepared, kind, segmentIndex, leadingSpacing) {58 const segmentContribution = kind === 'tab' ? 0 : prepared.lineEndFitAdvances[segmentIndex];59 return getLineEndContribution(leadingSpacing, segmentContribution);60}61function getLineEndPaintContribution(prepared, kind, segmentIndex, leadingSpacing, segmentWidth) {62 const segmentContribution = kind === 'tab' ? segmentWidth : prepared.lineEndPaintAdvances[segmentIndex];63 return getLineEndContribution(leadingSpacing, segmentContribution);64}65function getBreakableGraphemeAdvance(prepared, hasContent, baseAdvance) {66 return prepared.letterSpacing !== 0 && hasContent67 ? baseAdvance + prepared.letterSpacing68 : baseAdvance;69}70function getBreakableCandidateFitWidth(prepared, candidatePaintWidth) {71 return prepared.letterSpacing === 072 ? candidatePaintWidth73 : candidatePaintWidth + prepared.letterSpacing;74}75function getNextPreferredBreakIndex(preferredBreaks, preferredBreakIndex, graphemeEnd) {76 let lo = preferredBreakIndex;77 if (lo >= preferredBreaks.length || preferredBreaks[lo] >= graphemeEnd)78 return lo;79 // Simple batch walking carries the next boundary. The shared complex loop80 // and public continuations seek instead of rescanning every prior cut.81 let hi = preferredBreaks.length;82 lo++;83 while (lo < hi) {84 const mid = Math.floor((lo + hi) / 2);85 if (preferredBreaks[mid] < graphemeEnd)86 lo = mid + 1;87 else88 hi = mid;89 }90 return lo;91}92function getTerminalLetterSpacing(prepared, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex) {93 if (prepared.letterSpacing === 0)94 return 0;95 if (endGraphemeIndex > 0) {96 return prepared.spacingGraphemeCounts[endSegmentIndex] > 097 ? prepared.letterSpacing98 : 0;99 }100 if (isDiscretionaryLineEnd(prepared.kinds, endSegmentIndex, endGraphemeIndex))101 return 0;102 for (let i = endSegmentIndex - 1; i >= startSegmentIndex; i--) {103 const kind = prepared.kinds[i];104 if (kind === 'space' || kind === 'zero-width-break' || kind === 'hard-break' || kind === 'soft-hyphen')105 continue;106 if (i === startSegmentIndex && startGraphemeIndex > 0) {107 return prepared.letterSpacing;108 }109 return prepared.spacingGraphemeCounts[i] > 0110 ? prepared.letterSpacing111 : 0;112 }113 return 0;114}115function finalizeLinePaintWidth(prepared, width, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex) {116 return width + getTerminalLetterSpacing(prepared, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex);117}118function findChunkIndexForStart(prepared, segmentIndex) {119 let lo = 0;120 let hi = prepared.chunks.length;121 while (lo < hi) {122 const mid = Math.floor((lo + hi) / 2);123 if (segmentIndex < prepared.chunks[mid].consumedEndSegmentIndex) {124 hi = mid;125 }126 else {127 lo = mid + 1;128 }129 }130 return lo < prepared.chunks.length ? lo : -1;131}132function normalizeLineStartInChunk(prepared, chunkIndex, cursor) {133 let segmentIndex = cursor.segmentIndex;134 if (cursor.graphemeIndex > 0)135 return chunkIndex;136 // Consumed-only chunks can occur consecutively. Normalize through each of137 // them before entering the walker, while keeping actual empty hard-break138 // chunks observable as empty lines.139 for (let currentChunkIndex = chunkIndex; currentChunkIndex < prepared.chunks.length; currentChunkIndex++) {140 const chunk = prepared.chunks[currentChunkIndex];141 if (chunk.startSegmentIndex === chunk.endSegmentIndex && segmentIndex === chunk.startSegmentIndex) {142 cursor.segmentIndex = segmentIndex;143 cursor.graphemeIndex = 0;144 return currentChunkIndex;145 }146 if (segmentIndex < chunk.startSegmentIndex)147 segmentIndex = chunk.startSegmentIndex;148 segmentIndex = normalizeLineStartSegmentIndex(prepared, segmentIndex, chunk.endSegmentIndex);149 if (segmentIndex < chunk.endSegmentIndex) {150 cursor.segmentIndex = segmentIndex;151 cursor.graphemeIndex = 0;152 return currentChunkIndex;153 }154 if (chunk.consumedEndSegmentIndex >= prepared.widths.length)155 return -1;156 segmentIndex = chunk.consumedEndSegmentIndex;157 cursor.segmentIndex = segmentIndex;158 cursor.graphemeIndex = 0;159 }160 return -1;161}162// Mutates `cursor` to the next renderable line start and returns its chunk index.163export function normalizePreparedLineStart(prepared, cursor) {164 if (cursor.segmentIndex >= prepared.widths.length)165 return -1;166 const chunkIndex = findChunkIndexForStart(prepared, cursor.segmentIndex);167 if (chunkIndex < 0)168 return -1;169 return normalizeLineStartInChunk(prepared, chunkIndex, cursor);170}171function normalizeLineStartChunkIndexFromHint(prepared, chunkIndex, cursor) {172 if (cursor.segmentIndex >= prepared.widths.length)173 return -1;174 let nextChunkIndex = chunkIndex;175 while (nextChunkIndex < prepared.chunks.length &&176 cursor.segmentIndex >= prepared.chunks[nextChunkIndex].consumedEndSegmentIndex) {177 nextChunkIndex++;178 }179 if (nextChunkIndex >= prepared.chunks.length)180 return -1;181 return normalizeLineStartInChunk(prepared, nextChunkIndex, cursor);182}183export function countPreparedLines(prepared, maxWidth) {184 return walkPreparedLinesRaw(prepared, maxWidth);185}186function walkPreparedLinesSimple(prepared, maxWidth, onLine) {187 const { widths, kinds, breakableFitAdvances, breakablePreferredBreaks } = prepared;188 if (widths.length === 0)189 return 0;190 const engineProfile = getEngineProfile();191 const lineFitEpsilon = engineProfile.lineFitEpsilon;192 const fitLimit = maxWidth + lineFitEpsilon;193 let lineCount = 0;194 let lineW = 0;195 let hasContent = false;196 let lineStartSegmentIndex = 0;197 let lineStartGraphemeIndex = 0;198 let lineEndSegmentIndex = 0;199 let lineEndGraphemeIndex = 0;200 let pendingBreakSegmentIndex = -1;201 let pendingBreakPaintWidth = 0;202 function clearPendingBreak() {203 pendingBreakSegmentIndex = -1;204 pendingBreakPaintWidth = 0;205 }206 function emitCurrentLine(endSegmentIndex = lineEndSegmentIndex, endGraphemeIndex = lineEndGraphemeIndex, width = lineW) {207 lineCount++;208 onLine?.(width, lineStartSegmentIndex, lineStartGraphemeIndex, endSegmentIndex, endGraphemeIndex);209 lineW = 0;210 hasContent = false;211 clearPendingBreak();212 }213 function startLineAtSegment(segmentIndex, width) {214 hasContent = true;215 lineStartSegmentIndex = segmentIndex;216 lineStartGraphemeIndex = 0;217 lineEndSegmentIndex = segmentIndex + 1;218 lineEndGraphemeIndex = 0;219 lineW = width;220 }221 function startLineAtGrapheme(segmentIndex, graphemeIndex, width) {222 hasContent = true;223 lineStartSegmentIndex = segmentIndex;224 lineStartGraphemeIndex = graphemeIndex;225 lineEndSegmentIndex = segmentIndex;226 lineEndGraphemeIndex = graphemeIndex + 1;227 lineW = width;228 }229 function appendWholeSegment(segmentIndex, width) {230 if (!hasContent) {231 startLineAtSegment(segmentIndex, width);232 return;233 }234 lineW += width;235 lineEndSegmentIndex = segmentIndex + 1;236 lineEndGraphemeIndex = 0;237 }238 function appendBreakableSegmentFrom(segmentIndex, startGraphemeIndex) {239 const fitAdvances = breakableFitAdvances[segmentIndex];240 const preferredBreaks = breakablePreferredBreaks[segmentIndex] ?? null;241 let preferredBreakIndex = preferredBreaks === null242 ? -1243 : getNextPreferredBreakIndex(preferredBreaks, 0, startGraphemeIndex + 1);244 let lastPreferredBreakEnd = -1;245 let lastPreferredBreakWidth = 0;246 let g = startGraphemeIndex;247 while (g < fitAdvances.length) {248 const gw = fitAdvances[g];249 if (!hasContent) {250 startLineAtGrapheme(segmentIndex, g, gw);251 }252 else if (lineW + gw > fitLimit) {253 if (preferredBreaks !== null && lastPreferredBreakEnd > startGraphemeIndex) {254 emitCurrentLine(segmentIndex, lastPreferredBreakEnd, lastPreferredBreakWidth);255 g = lastPreferredBreakEnd;256 preferredBreakIndex = getNextPreferredBreakIndex(preferredBreaks, preferredBreakIndex, g + 1);257 lastPreferredBreakEnd = -1;258 lastPreferredBreakWidth = 0;259 continue;260 }261 emitCurrentLine();262 startLineAtGrapheme(segmentIndex, g, gw);263 }264 else {265 lineW += gw;266 lineEndSegmentIndex = segmentIndex;267 lineEndGraphemeIndex = g + 1;268 }269 const graphemeEnd = g + 1;270 if (preferredBreaks !== null && preferredBreaks[preferredBreakIndex] === graphemeEnd) {271 lastPreferredBreakEnd = graphemeEnd;272 lastPreferredBreakWidth = lineW;273 preferredBreakIndex++;274 }275 g++;276 }277 if (hasContent && lineEndSegmentIndex === segmentIndex && lineEndGraphemeIndex === fitAdvances.length) {278 lineEndSegmentIndex = segmentIndex + 1;279 lineEndGraphemeIndex = 0;280 }281 }282 let i = 0;283 while (i < widths.length) {284 if (!hasContent) {285 i = normalizeLineStartSegmentIndex(prepared, i);286 if (i >= widths.length)287 break;288 }289 const w = widths[i];290 const kind = kinds[i];291 const breakAfter = breaksAfter(kind);292 if (!hasContent) {293 if (w > fitLimit && breakableFitAdvances[i] !== null) {294 appendBreakableSegmentFrom(i, 0);295 }296 else {297 startLineAtSegment(i, w);298 }299 if (breakAfter) {300 pendingBreakSegmentIndex = i + 1;301 pendingBreakPaintWidth = lineW - w;302 }303 i++;304 continue;305 }306 const newW = lineW + w;307 if (newW > fitLimit) {308 if (breakAfter) {309 appendWholeSegment(i, w);310 emitCurrentLine(i + 1, 0, lineW - w);311 i++;312 continue;313 }314 if (pendingBreakSegmentIndex >= 0) {315 if (lineEndSegmentIndex > pendingBreakSegmentIndex ||316 (lineEndSegmentIndex === pendingBreakSegmentIndex && lineEndGraphemeIndex > 0)) {317 emitCurrentLine();318 continue;319 }320 emitCurrentLine(pendingBreakSegmentIndex, 0, pendingBreakPaintWidth);321 continue;322 }323 if (w > fitLimit && breakableFitAdvances[i] !== null) {324 emitCurrentLine();325 appendBreakableSegmentFrom(i, 0);326 i++;327 continue;328 }329 emitCurrentLine();330 continue;331 }332 appendWholeSegment(i, w);333 if (breakAfter) {334 pendingBreakSegmentIndex = i + 1;335 pendingBreakPaintWidth = lineW - w;336 }337 i++;338 }339 if (hasContent)340 emitCurrentLine();341 return lineCount;342}343export function walkPreparedLinesRaw(prepared, maxWidth, onLine) {344 if (prepared.simpleLineWalkFastPath)345 return walkPreparedLinesSimple(prepared, maxWidth, onLine);346 const cursor = { segmentIndex: 0, graphemeIndex: 0 };347 const chunkIndex = normalizePreparedLineStart(prepared, cursor);348 return walkPreparedComplexLines(prepared, cursor, chunkIndex, maxWidth, onLine).lineCount;349}350function stepPreparedChunkLineGeometry(prepared, cursor, chunkIndex, maxWidth) {351 return walkPreparedComplexLines(prepared, cursor, chunkIndex, maxWidth, undefined, 1).lastLineWidth;352}353function walkPreparedComplexLines(prepared, cursor, chunkIndex, maxWidth, onLine, lineLimit = Number.POSITIVE_INFINITY) {354 const { widths, kinds, breakableFitAdvances, breakablePreferredBreaks, discretionaryHyphenWidth, } = prepared;355 const engineProfile = getEngineProfile();356 const lineFitEpsilon = engineProfile.lineFitEpsilon;357 const fitLimit = maxWidth + lineFitEpsilon;358 let lineStartSegmentIndex;359 let lineStartGraphemeIndex;360 let lineW;361 let hasContent;362 let lineEndSegmentIndex;363 let lineEndGraphemeIndex;364 let pendingBreakSegmentIndex;365 let pendingBreakFitWidth;366 let pendingBreakPaintWidth;367 let pendingBreakKind;368 function getCurrentLinePaintWidth() {369 return (pendingBreakKind === 'soft-hyphen' &&370 pendingBreakSegmentIndex === lineEndSegmentIndex &&371 lineEndGraphemeIndex === 0)372 ? pendingBreakPaintWidth373 : lineW;374 }375 function finishLine(endSegmentIndex = lineEndSegmentIndex, endGraphemeIndex = lineEndGraphemeIndex, width = getCurrentLinePaintWidth()) {376 if (!hasContent)377 return null;378 cursor.segmentIndex = endSegmentIndex;379 cursor.graphemeIndex = endGraphemeIndex;380 return finalizeLinePaintWidth(prepared, width, lineStartSegmentIndex, lineStartGraphemeIndex, endSegmentIndex, endGraphemeIndex);381 }382 function startLineAtSegment(segmentIndex, width) {383 hasContent = true;384 lineEndSegmentIndex = segmentIndex + 1;385 lineEndGraphemeIndex = 0;386 lineW = width;387 }388 function startLineAtGrapheme(segmentIndex, graphemeIndex, width) {389 hasContent = true;390 lineEndSegmentIndex = segmentIndex;391 lineEndGraphemeIndex = graphemeIndex + 1;392 lineW = width;393 }394 function appendWholeSegment(segmentIndex, advance) {395 if (!hasContent) {396 startLineAtSegment(segmentIndex, advance);397 return;398 }399 lineW += advance;400 lineEndSegmentIndex = segmentIndex + 1;401 lineEndGraphemeIndex = 0;402 }403 function updatePendingBreakForWholeSegment(kind, breakAfter, segmentIndex, segmentWidth, leadingSpacing, advance) {404 if (!breakAfter)405 return;406 const fitAdvance = getBreakOpportunityFitContribution(prepared, kind, segmentIndex, leadingSpacing);407 const paintAdvance = getLineEndPaintContribution(prepared, kind, segmentIndex, leadingSpacing, segmentWidth);408 pendingBreakSegmentIndex = segmentIndex + 1;409 pendingBreakFitWidth = lineW - advance + fitAdvance;410 pendingBreakPaintWidth = lineW - advance + paintAdvance;411 pendingBreakKind = kind;412 }413 function appendBreakableSegmentFrom(segmentIndex, startGraphemeIndex) {414 const fitAdvances = breakableFitAdvances[segmentIndex];415 const preferredBreaks = breakablePreferredBreaks[segmentIndex] ?? null;416 let preferredBreakIndex = preferredBreaks === null417 ? -1418 : getNextPreferredBreakIndex(preferredBreaks, 0, startGraphemeIndex + 1);419 let lastPreferredBreakEnd = -1;420 let lastPreferredBreakWidth = 0;421 for (let g = startGraphemeIndex; g < fitAdvances.length; g++) {422 const baseGw = fitAdvances[g];423 if (!hasContent) {424 startLineAtGrapheme(segmentIndex, g, baseGw);425 }426 else {427 const gw = getBreakableGraphemeAdvance(prepared, true, baseGw);428 const candidatePaintWidth = lineW + gw;429 if (getBreakableCandidateFitWidth(prepared, candidatePaintWidth) > fitLimit) {430 if (preferredBreaks !== null && lastPreferredBreakEnd > startGraphemeIndex) {431 return finishLine(segmentIndex, lastPreferredBreakEnd, lastPreferredBreakWidth);432 }433 return finishLine();434 }435 lineW = candidatePaintWidth;436 lineEndSegmentIndex = segmentIndex;437 lineEndGraphemeIndex = g + 1;438 }439 const graphemeEnd = g + 1;440 if (preferredBreaks !== null && preferredBreaks[preferredBreakIndex] === graphemeEnd) {441 lastPreferredBreakEnd = graphemeEnd;442 lastPreferredBreakWidth = lineW;443 preferredBreakIndex++;444 }445 }446 if (hasContent && lineEndSegmentIndex === segmentIndex && lineEndGraphemeIndex === fitAdvances.length) {447 lineEndSegmentIndex = segmentIndex + 1;448 lineEndGraphemeIndex = 0;449 }450 return null;451 }452 let lineCount = 0;453 let lastLineWidth = null;454 while (chunkIndex >= 0 && lineCount < lineLimit) {455 lineStartSegmentIndex = cursor.segmentIndex;456 lineStartGraphemeIndex = cursor.graphemeIndex;457 lineW = 0;458 hasContent = false;459 lineEndSegmentIndex = cursor.segmentIndex;460 lineEndGraphemeIndex = cursor.graphemeIndex;461 pendingBreakSegmentIndex = -1;462 pendingBreakFitWidth = 0;463 pendingBreakPaintWidth = 0;464 pendingBreakKind = null;465 const chunk = prepared.chunks[chunkIndex];466 let lineWidth = null;467 if (chunk.startSegmentIndex === chunk.endSegmentIndex) {468 cursor.segmentIndex = chunk.consumedEndSegmentIndex;469 cursor.graphemeIndex = 0;470 lineWidth = 0;471 }472 else {473 lineLoop: for (let i = cursor.segmentIndex; i < chunk.endSegmentIndex; i++) {474 const kind = kinds[i];475 const breakAfter = breaksAfter(kind);476 const startGraphemeIndex = i === cursor.segmentIndex ? cursor.graphemeIndex : 0;477 const leadingSpacing = getLeadingLetterSpacing(prepared, hasContent, i);478 const w = kind === 'tab'479 ? getTabAdvance(lineW + leadingSpacing, prepared.tabStopAdvance)480 : widths[i];481 const advance = leadingSpacing + w;482 const fitAdvance = getWholeSegmentFitContribution(prepared, kind, i, leadingSpacing, w);483 if (kind === 'soft-hyphen' && startGraphemeIndex === 0) {484 if (hasContent) {485 lineEndSegmentIndex = i + 1;486 lineEndGraphemeIndex = 0;487 if (i + 1 < chunk.endSegmentIndex) {488 pendingBreakSegmentIndex = i + 1;489 pendingBreakFitWidth = lineW + discretionaryHyphenWidth;490 pendingBreakPaintWidth = lineW + discretionaryHyphenWidth;491 pendingBreakKind = kind;492 }493 }494 continue;495 }496 if (!hasContent) {497 if (startGraphemeIndex > 0) {498 const line = appendBreakableSegmentFrom(i, startGraphemeIndex);499 if (line !== null) {500 lineWidth = line;501 break lineLoop;502 }503 }504 else if (fitAdvance > fitLimit && breakableFitAdvances[i] !== null) {505 const line = appendBreakableSegmentFrom(i, 0);506 if (line !== null) {507 lineWidth = line;508 break lineLoop;509 }510 }511 else {512 startLineAtSegment(i, w);513 }514 updatePendingBreakForWholeSegment(kind, breakAfter, i, w, leadingSpacing, advance);515 continue;516 }517 const newFitW = lineW + fitAdvance;518 if (newFitW > fitLimit) {519 const currentBreakFitWidth = lineW + getBreakOpportunityFitContribution(prepared, kind, i, leadingSpacing);520 const currentBreakPaintWidth = lineW + getLineEndPaintContribution(prepared, kind, i, leadingSpacing, w);521 if (breakAfter && currentBreakFitWidth <= fitLimit) {522 appendWholeSegment(i, advance);523 lineWidth = finishLine(i + 1, 0, currentBreakPaintWidth);524 break lineLoop;525 }526 if (pendingBreakSegmentIndex >= 0 && pendingBreakFitWidth <= fitLimit) {527 if (lineEndSegmentIndex > pendingBreakSegmentIndex ||528 (lineEndSegmentIndex === pendingBreakSegmentIndex && lineEndGraphemeIndex > 0)) {529 lineWidth = finishLine();530 break lineLoop;531 }532 lineWidth = finishLine(pendingBreakSegmentIndex, 0, pendingBreakPaintWidth);533 break lineLoop;534 }535 lineWidth = finishLine();536 break lineLoop;537 }538 appendWholeSegment(i, advance);539 updatePendingBreakForWholeSegment(kind, breakAfter, i, w, leadingSpacing, advance);540 }541 if (lineWidth === null) {542 lineWidth = pendingBreakSegmentIndex === chunk.consumedEndSegmentIndex && lineEndGraphemeIndex === 0543 ? finishLine(chunk.consumedEndSegmentIndex, 0, pendingBreakPaintWidth)544 : finishLine(chunk.consumedEndSegmentIndex, 0, lineW);545 }546 }547 if (lineWidth === null)548 break;549 lastLineWidth = lineWidth;550 lineCount++;551 onLine?.(lineWidth, lineStartSegmentIndex, lineStartGraphemeIndex, cursor.segmentIndex, cursor.graphemeIndex);552 // A single-line caller owns normalization of the following line.553 if (lineCount < lineLimit)554 chunkIndex = normalizeLineStartChunkIndexFromHint(prepared, chunkIndex, cursor);555 }556 return { lineCount, lastLineWidth };557}558function stepPreparedSimpleLineGeometry(prepared, cursor, maxWidth) {559 const { widths, kinds, breakableFitAdvances, breakablePreferredBreaks } = prepared;560 const engineProfile = getEngineProfile();561 const lineFitEpsilon = engineProfile.lineFitEpsilon;562 const fitLimit = maxWidth + lineFitEpsilon;563 let lineW = 0;564 let hasContent = false;565 let lineEndSegmentIndex = cursor.segmentIndex;566 let lineEndGraphemeIndex = cursor.graphemeIndex;567 let pendingBreakSegmentIndex = -1;568 let pendingBreakPaintWidth = 0;569 for (let i = cursor.segmentIndex; i < widths.length; i++) {570 const kind = kinds[i];571 const breakAfter = breaksAfter(kind);572 const startGraphemeIndex = i === cursor.segmentIndex ? cursor.graphemeIndex : 0;573 const breakableFitAdvance = breakableFitAdvances[i];574 const w = widths[i];575 if (!hasContent) {576 if (startGraphemeIndex > 0 || (w > fitLimit && breakableFitAdvance !== null)) {577 const fitAdvances = breakableFitAdvance;578 const preferredBreaks = breakablePreferredBreaks[i] ?? null;579 let preferredBreakIndex = preferredBreaks === null580 ? -1581 : getNextPreferredBreakIndex(preferredBreaks, 0, startGraphemeIndex + 1);582 let lastPreferredBreakEnd = -1;583 let lastPreferredBreakWidth = 0;584 const firstGraphemeWidth = fitAdvances[startGraphemeIndex];585 hasContent = true;586 lineW = firstGraphemeWidth;587 lineEndSegmentIndex = i;588 lineEndGraphemeIndex = startGraphemeIndex + 1;589 if (preferredBreaks !== null && preferredBreaks[preferredBreakIndex] === lineEndGraphemeIndex) {590 lastPreferredBreakEnd = lineEndGraphemeIndex;591 lastPreferredBreakWidth = lineW;592 preferredBreakIndex++;593 }594 for (let g = startGraphemeIndex + 1; g < fitAdvances.length; g++) {595 const gw = fitAdvances[g];596 if (lineW + gw > fitLimit) {597 if (preferredBreaks !== null && lastPreferredBreakEnd > startGraphemeIndex) {598 cursor.segmentIndex = i;599 cursor.graphemeIndex = lastPreferredBreakEnd;600 return lastPreferredBreakWidth;601 }602 cursor.segmentIndex = lineEndSegmentIndex;603 cursor.graphemeIndex = lineEndGraphemeIndex;604 return lineW;605 }606 lineW += gw;607 lineEndSegmentIndex = i;608 lineEndGraphemeIndex = g + 1;609 if (preferredBreaks !== null && preferredBreaks[preferredBreakIndex] === lineEndGraphemeIndex) {610 lastPreferredBreakEnd = lineEndGraphemeIndex;611 lastPreferredBreakWidth = lineW;612 preferredBreakIndex++;613 }614 }615 if (lineEndSegmentIndex === i && lineEndGraphemeIndex === fitAdvances.length) {616 lineEndSegmentIndex = i + 1;617 lineEndGraphemeIndex = 0;618 }619 }620 else {621 hasContent = true;622 lineW = w;623 lineEndSegmentIndex = i + 1;624 lineEndGraphemeIndex = 0;625 }626 if (breakAfter) {627 pendingBreakSegmentIndex = i + 1;628 pendingBreakPaintWidth = lineW - w;629 }630 continue;631 }632 if (lineW + w > fitLimit) {633 if (breakAfter) {634 cursor.segmentIndex = i + 1;635 cursor.graphemeIndex = 0;636 return lineW;637 }638 if (pendingBreakSegmentIndex >= 0) {639 if (lineEndSegmentIndex > pendingBreakSegmentIndex ||640 (lineEndSegmentIndex === pendingBreakSegmentIndex && lineEndGraphemeIndex > 0)) {641 cursor.segmentIndex = lineEndSegmentIndex;642 cursor.graphemeIndex = lineEndGraphemeIndex;643 return lineW;644 }645 cursor.segmentIndex = pendingBreakSegmentIndex;646 cursor.graphemeIndex = 0;647 return pendingBreakPaintWidth;648 }649 cursor.segmentIndex = lineEndSegmentIndex;650 cursor.graphemeIndex = lineEndGraphemeIndex;651 return lineW;652 }653 lineW += w;654 lineEndSegmentIndex = i + 1;655 lineEndGraphemeIndex = 0;656 if (breakAfter) {657 pendingBreakSegmentIndex = i + 1;658 pendingBreakPaintWidth = lineW - w;659 }660 }661 if (!hasContent)662 return null;663 cursor.segmentIndex = lineEndSegmentIndex;664 cursor.graphemeIndex = lineEndGraphemeIndex;665 return lineW;666}667export function stepPreparedLineGeometryFromChunk(prepared, cursor, chunkIndex, maxWidth) {668 if (prepared.simpleLineWalkFastPath) {669 return stepPreparedSimpleLineGeometry(prepared, cursor, maxWidth);670 }671 return stepPreparedChunkLineGeometry(prepared, cursor, chunkIndex, maxWidth);672}673export function stepPreparedLineGeometry(prepared, cursor, maxWidth) {674 const chunkIndex = normalizePreparedLineStart(prepared, cursor);675 if (chunkIndex < 0)676 return null;677 return stepPreparedLineGeometryFromChunk(prepared, cursor, chunkIndex, maxWidth);678}679export function measurePreparedLineGeometry(prepared, maxWidth) {680 if (prepared.widths.length === 0) {681 return {682 lineCount: 0,683 maxLineWidth: 0,684 };685 }686 const cursor = {687 segmentIndex: 0,688 graphemeIndex: 0,689 };690 let lineCount = 0;691 let maxLineWidth = 0;692 if (!prepared.simpleLineWalkFastPath) {693 const chunkIndex = normalizePreparedLineStart(prepared, cursor);694 lineCount = walkPreparedComplexLines(prepared, cursor, chunkIndex, maxWidth, width => {695 if (width > maxLineWidth)696 maxLineWidth = width;697 }).lineCount;698 return { lineCount, maxLineWidth };699 }700 while (true) {701 const lineWidth = stepPreparedLineGeometry(prepared, cursor, maxWidth);702 if (lineWidth === null) {703 return {704 lineCount,705 maxLineWidth,706 };707 }708 lineCount++;709 if (lineWidth > maxLineWidth)710 maxLineWidth = lineWidth;711 }712}