Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions packages/layout-engine/layout-bridge/src/remeasure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1156,11 +1156,22 @@ export function remeasureParagraph(
let endChar = currentChar;
let tabStopCursor = 0;
let didBreakInThisLine = false;
let explicitLineBreakRun = -1;
let resumeRun = -1;
let resumeChar = 0;

for (let r = currentRun; r < runs.length; r += 1) {
const run = runs[r];
if (isLineBreakRun(run)) {
explicitLineBreakRun = r;
if (startRun === r && startChar === 0 && width === 0) {
// Preserve leading/manual explicit break as an empty line.
endRun = r;
endChar = 0;
}
didBreakInThisLine = true;
break;
}
if (run.kind === 'tab') {
const absCurrentX = width + effectiveIndent;
const { target, nextIndex, stop } = getNextTabStopPx(absCurrentX, tabStops, tabStopCursor);
Expand Down Expand Up @@ -1302,7 +1313,7 @@ export function remeasureParagraph(
}

// If we didn't consume any chars (e.g., very long single char), force one char
if (startRun === endRun && startChar === endChar) {
if (explicitLineBreakRun < 0 && startRun === endRun && startChar === endChar) {
endRun = startRun;
endChar = startChar + 1;
}
Expand All @@ -1321,8 +1332,14 @@ export function remeasureParagraph(
lines.push(line);

// Advance to next line start
currentRun = endRun;
currentChar = endChar;
if (explicitLineBreakRun >= 0) {
// Explicit break consumed as a line boundary, continue from run after break.
currentRun = explicitLineBreakRun + 1;
currentChar = 0;
Comment on lines +1335 to +1338
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve trailing explicit lineBreak as final blank line

Advancing currentRun to explicitLineBreakRun + 1 immediately consumes the break run, so when a paragraph ends with lineBreak (or with multiple trailing lineBreak runs), the loop exits without emitting the final empty line(s). This regresses trailing manual-break rendering in remeasure paths (e.g., signature blocks or intentional blank lines at paragraph end) because each trailing break should contribute a visible blank line boundary.

Useful? React with 👍 / 👎.

} else {
currentRun = endRun;
currentChar = endChar;
}
if (currentRun >= runs.length) {
break;
}
Expand Down
25 changes: 24 additions & 1 deletion packages/layout-engine/layout-bridge/test/remeasure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,30 @@ describe('remeasureParagraph', () => {
const block = createBlock([textRun('Hello'), { kind: 'lineBreak' } as Run, textRun('World')]);
const measure = remeasureParagraph(block, 200);

expect(measure.lines.length).toBeGreaterThanOrEqual(1);
expect(measure.lines).toHaveLength(2);
expect(measure.lines[0].fromRun).toBe(0);
expect(measure.lines[0].toRun).toBe(0);
expect(measure.lines[1].fromRun).toBe(2);
expect(measure.lines[1].toRun).toBe(2);
});

it('preserves multiple explicit lineBreak boundaries', () => {
const block = createBlock([
textRun('One'),
{ kind: 'lineBreak' } as Run,
textRun('Two'),
{ kind: 'lineBreak' } as Run,
textRun('Three'),
]);
const measure = remeasureParagraph(block, 200);

expect(measure.lines).toHaveLength(3);
expect(measure.lines[0].fromRun).toBe(0);
expect(measure.lines[0].toRun).toBe(0);
expect(measure.lines[1].fromRun).toBe(2);
expect(measure.lines[1].toRun).toBe(2);
expect(measure.lines[2].fromRun).toBe(4);
expect(measure.lines[2].toRun).toBe(4);
});

it('handles tabs followed immediately by line break', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/layout-engine/measuring/dom/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,8 @@ describe('measureBlock', () => {
expect(measure.lines).toHaveLength(2);
expect(measure.lines[0].width).toBeGreaterThan(0);
expect(measure.lines[1].width).toBeGreaterThan(0);
expect(measure.lines[1].fromRun).toBe(2);
expect(measure.lines[1].toRun).toBe(2);
});

it('creates an empty line for leading lineBreak at start of paragraph', async () => {
Expand Down
15 changes: 15 additions & 0 deletions packages/layout-engine/measuring/dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1397,6 +1397,21 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
continue;
}

// When a text/tab/atomic run follows an explicit lineBreak, currentLine is a
// placeholder line seeded with the break run index. Re-anchor it so line ranges
// start at the first visible run on the new line.
if (
currentLine &&
currentLine.width === 0 &&
currentLine.fromRun === currentLine.toRun &&
currentLine.fromChar === 0 &&
currentLine.toChar === 0 &&
isLineBreakRun(runsToProcess[currentLine.fromRun] as Run)
) {
currentLine.fromRun = runIndex;
currentLine.toRun = runIndex;
}

// Handle tab runs specially
if (isTabRun(run)) {
// Clear any previous tab group when we encounter a new tab
Expand Down
Loading