Skip to content

Commit 70c0816

Browse files
committed
fix: render manual line breaks correctly
1 parent 4ba8992 commit 70c0816

File tree

4 files changed

+61
-4
lines changed

4 files changed

+61
-4
lines changed

packages/layout-engine/layout-bridge/src/remeasure.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,11 +1156,22 @@ export function remeasureParagraph(
11561156
let endChar = currentChar;
11571157
let tabStopCursor = 0;
11581158
let didBreakInThisLine = false;
1159+
let explicitLineBreakRun = -1;
11591160
let resumeRun = -1;
11601161
let resumeChar = 0;
11611162

11621163
for (let r = currentRun; r < runs.length; r += 1) {
11631164
const run = runs[r];
1165+
if (isLineBreakRun(run)) {
1166+
explicitLineBreakRun = r;
1167+
if (startRun === r && startChar === 0 && width === 0) {
1168+
// Preserve leading/manual explicit break as an empty line.
1169+
endRun = r;
1170+
endChar = 0;
1171+
}
1172+
didBreakInThisLine = true;
1173+
break;
1174+
}
11641175
if (run.kind === 'tab') {
11651176
const absCurrentX = width + effectiveIndent;
11661177
const { target, nextIndex, stop } = getNextTabStopPx(absCurrentX, tabStops, tabStopCursor);
@@ -1302,7 +1313,7 @@ export function remeasureParagraph(
13021313
}
13031314

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

13231334
// Advance to next line start
1324-
currentRun = endRun;
1325-
currentChar = endChar;
1335+
if (explicitLineBreakRun >= 0) {
1336+
// Explicit break consumed as a line boundary, continue from run after break.
1337+
currentRun = explicitLineBreakRun + 1;
1338+
currentChar = 0;
1339+
} else {
1340+
currentRun = endRun;
1341+
currentChar = endChar;
1342+
}
13261343
if (currentRun >= runs.length) {
13271344
break;
13281345
}

packages/layout-engine/layout-bridge/test/remeasure.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -872,7 +872,30 @@ describe('remeasureParagraph', () => {
872872
const block = createBlock([textRun('Hello'), { kind: 'lineBreak' } as Run, textRun('World')]);
873873
const measure = remeasureParagraph(block, 200);
874874

875-
expect(measure.lines.length).toBeGreaterThanOrEqual(1);
875+
expect(measure.lines).toHaveLength(2);
876+
expect(measure.lines[0].fromRun).toBe(0);
877+
expect(measure.lines[0].toRun).toBe(0);
878+
expect(measure.lines[1].fromRun).toBe(2);
879+
expect(measure.lines[1].toRun).toBe(2);
880+
});
881+
882+
it('preserves multiple explicit lineBreak boundaries', () => {
883+
const block = createBlock([
884+
textRun('One'),
885+
{ kind: 'lineBreak' } as Run,
886+
textRun('Two'),
887+
{ kind: 'lineBreak' } as Run,
888+
textRun('Three'),
889+
]);
890+
const measure = remeasureParagraph(block, 200);
891+
892+
expect(measure.lines).toHaveLength(3);
893+
expect(measure.lines[0].fromRun).toBe(0);
894+
expect(measure.lines[0].toRun).toBe(0);
895+
expect(measure.lines[1].fromRun).toBe(2);
896+
expect(measure.lines[1].toRun).toBe(2);
897+
expect(measure.lines[2].fromRun).toBe(4);
898+
expect(measure.lines[2].toRun).toBe(4);
876899
});
877900

878901
it('handles tabs followed immediately by line break', () => {

packages/layout-engine/measuring/dom/src/index.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,8 @@ describe('measureBlock', () => {
542542
expect(measure.lines).toHaveLength(2);
543543
expect(measure.lines[0].width).toBeGreaterThan(0);
544544
expect(measure.lines[1].width).toBeGreaterThan(0);
545+
expect(measure.lines[1].fromRun).toBe(2);
546+
expect(measure.lines[1].toRun).toBe(2);
545547
});
546548

547549
it('creates an empty line for leading lineBreak at start of paragraph', async () => {

packages/layout-engine/measuring/dom/src/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,6 +1397,21 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
13971397
continue;
13981398
}
13991399

1400+
// When a text/tab/atomic run follows an explicit lineBreak, currentLine is a
1401+
// placeholder line seeded with the break run index. Re-anchor it so line ranges
1402+
// start at the first visible run on the new line.
1403+
if (
1404+
currentLine &&
1405+
currentLine.width === 0 &&
1406+
currentLine.fromRun === currentLine.toRun &&
1407+
currentLine.fromChar === 0 &&
1408+
currentLine.toChar === 0 &&
1409+
isLineBreakRun(runsToProcess[currentLine.fromRun] as Run)
1410+
) {
1411+
currentLine.fromRun = runIndex;
1412+
currentLine.toRun = runIndex;
1413+
}
1414+
14001415
// Handle tab runs specially
14011416
if (isTabRun(run)) {
14021417
// Clear any previous tab group when we encounter a new tab

0 commit comments

Comments
 (0)