fix: correct tab leader from/to bounds when effectiveIndent is applied#2141
fix: correct tab leader from/to bounds when effectiveIndent is applied#2141roncallyt wants to merge 5 commits intosuperdoc-dev:mainfrom
Conversation
- Add effectiveIndent to leader `from` calculation so leaders start after indented text - Update leader `to` after look-ahead to account for effectiveIndent, ensuring leaders end where right-aligned content begins - Fix applies to `end`, `center`, and `decimal` tab stop alignments - Add unit tests for leader positioning with and without paragraph indent Closes superdoc-dev#2075
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
caio-pizzol
left a comment
There was a problem hiding this comment.
Hey @roncallyt, nice work!
The root cause is correct - leader bounds weren't accounting for effectiveIndent, so they overlapped with text and page numbers. The fix is clean and the before/after screenshots look great.
Couple of things to address before merging (details in inline comments)
Also, it would be great to add a visual regression test in tests/visual/ using one of the attached TOC documents to prevent future regressions on this.
Leader `from` and `to` were mixing relative and absolute coordinate spaces, causing start-aligned tabs with leaders to be off by `effectiveIndent` px. Both values now use absolute coordinates consistently. For end/center/decimal alignments, `to` is still overwritten by the look-ahead correction.
`leader.to` passed even without the absolute-coordinate fix. I measured the "42" text width independently so the check actually covers the `to` correction. Collapse the "without indent" and "with indent" variants into a single `it.each`.
|
@caio-pizzol I tried creating visual regression tests, but seems like I don't have permissions to do so. Perharps it's something to do with my Cloudflare account. Anyway, is there another way to create them? |
Right! Let us add that on your side for now |
caio-pizzol
left a comment
There was a problem hiding this comment.
all four items initial are addressed -- absolute coordinates, test precision, deduplication. looks good. left two non-blocking inline comments.
we'll add the visual regression test on our side.
| @@ -1383,6 +1382,13 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P | |||
| groupStartX = Math.max(0, relativeTarget - beforeDecimal); | |||
| } | |||
|
|
|||
| // Update leader "to" ensuring leaders end where right-aligned content begins | |||
| if (currentLine.leaders && currentLine.leaders.length > 0) { | |||
| const lastLeader = currentLine.leaders.at(-1); | |||
|
|
|||
| if (lastLeader) lastLeader.to = groupStartX + effectiveIndent; | |||
| } | |||
|
|
|||
| // Set up active tab group for subsequent run processing | |||
| activeTabGroup = { | |||
| measure: groupMeasure, | |||
| @@ -2307,9 +2313,8 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P | |||
| // Emit leader decoration if requested | |||
| if (stop && stop.leader && stop.leader !== 'none' && stop.leader !== 'middleDot') { | |||
| const leaderStyle: 'heavy' | 'dot' | 'hyphen' | 'underscore' = stop.leader; | |||
| const relativeTarget = clampedTarget - effectiveIndent; | |||
| const from = Math.min(originX, relativeTarget); | |||
| const to = Math.max(originX, relativeTarget); | |||
| const from = Math.min(originX + effectiveIndent, clampedTarget); | |||
| const to = Math.max(originX + effectiveIndent, clampedTarget); | |||
| if (!currentLine.leaders) currentLine.leaders = []; | |||
| currentLine.leaders.push({ from, to, style: leaderStyle }); | |||
There was a problem hiding this comment.
the inline-tab path here doesn't correct to after content is positioned (the explicit tab-run path does at L1389).
means inline \t + right-aligned tab + leader could still overlap.
not urgent since TOCs use explicit tab runs -- just something to know about.
|
|
||
| // Update leader "to" ensuring leaders end where right-aligned content begins | ||
| if (currentLine.leaders && currentLine.leaders.length > 0) { | ||
| const lastLeader = currentLine.leaders.at(-1); |
There was a problem hiding this comment.
.at(-1) grabs the last leader on the line, which might not be the one this tab just pushed. if a previous tab has a leader but this tab doesn't, this overwrites the wrong one.
easy fix: guard this block with stop.leader too, or save a reference when you push instead of fishing it back with .at(-1).
There was a problem hiding this comment.
Fixed, I preferred to save a reference when pushing to the leaders array, it worked properly.
|
|
||
| // Update leader "to" ensuring leaders end where right-aligned content begins | ||
| if (currentLine.leaders && currentLine.leaders.length > 0) { | ||
| const lastLeader = currentLine.leaders.at(-1); |
There was a problem hiding this comment.
nit: .length > 0 is redundant here -- .at(-1) returns undefined on empty arrays and the if (lastLeader) below handles it. can simplify to
| const lastLeader = currentLine.leaders.at(-1); | |
| const lastLeader = currentLine.leaders?.at(-1); |
There was a problem hiding this comment.
As I decided to save a reference to the current leader, this adjustment became unnecessary.
| const measure = expectParagraphMeasure(await measureBlock(block, 1000)); | ||
| expect(measure.lines).toHaveLength(1); | ||
|
|
||
| const leaders = measure.lines[0].leaders; |
There was a problem hiding this comment.
nit: expect(leaders).toBeDefined() + expect(leaders!.length).toBe(1) can be just expect(leaders).toHaveLength(1) -- covers both checks and avoids the ! assertion.
- Replace `toBeDefined()` and `toBe(1)` for `toHaveLength(1)`
Now saves a direct reference when pushing to the leaders array and uses it in the look-ahead correction.
There was a problem hiding this comment.
@roncallyt thanks again for the fixes
left an inline comment + one note below. nothing blocking.
packages/layout-engine/layout-bridge/src/remeasure.ts:804 (not in diff, noting here)
applyTabLayoutToLines still computes relativeTarget = clampedTarget - effectiveIndent for leader from/to, so remeasured lines get relative coordinates while initial measurement now produces absolute ones. the renderer uses ld.from directly as CSS left without paddingLeft, so remeasured leaders land effectiveIndent pixels too far left. this was pre-existing, but the PR widens the gap between the two paths — worth fixing together or tracking in a follow-up issue?
| const from = Math.min(originX + effectiveIndent, clampedTarget); | ||
| const to = Math.max(originX + effectiveIndent, clampedTarget); | ||
| if (!currentLine.leaders) currentLine.leaders = []; | ||
| currentLine.leaders.push({ from, to, style: leaderStyle }); |
There was a problem hiding this comment.
the inline tab path sets pendingTabAlignment but never updates the leader's to after aligning end/center/decimal stops — the same correction that TabRun applies at line ~1385. for a typical TOC this won't show up (they use TabRun objects), but if someone hits an end-aligned stop with a leader via an inline \t, the dots would stretch past where the text starts. worth a follow-up?
There was a problem hiding this comment.
Yeah, that makes sense, I'll look into it
|
Good catch! I'd prefer to fix this in the same PR to keep both paths consistent. I'll let you know as soon as I'm finished. |
Summary
Fixes tab leader rendering in Word-generated Tables of Contents. Leaders were overlapping with section titles on the left and page numbers on the right due to effectiveIndent not being considered when calculating leader boundaries.
Closes #2075
Changes
Showcase
ToC with lined tabs (3).docx
Before
After
ToC with dotted tabs (1).docx
Before
After
Observations