Skip to content

Commit 85802da

Browse files
authored
Merge pull request #3627 from superdoc-dev/caio-pizzol/sd3330-underlined-tabs-followup
fix(layout-engine): render underlined tabs as one continuous line
2 parents c18caea + 199be3d commit 85802da

8 files changed

Lines changed: 912 additions & 34 deletions

File tree

packages/layout-engine/layout-resolved/src/versionSignature.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,65 @@ describe('deriveBlockVersion - tab underline', () => {
9595
const b = deriveBlockVersion(makeTabParagraph({ style: 'single', color: '#000000' }));
9696
expect(a).toBe(b);
9797
});
98+
99+
// SD-3330: the painter's tab underline thickness comes from fontSize, and its offset/color
100+
// come from measured line metrics fed by fontFamily and the run color. Each must change the
101+
// block version, or a font-size/family/color edit leaves a stale tab underline cached.
102+
const makeStyledTabParagraph = (
103+
overrides: Partial<{ fontSize: number; fontFamily: string; color: string; bold: boolean; italic: boolean }>,
104+
): FlowBlock => ({
105+
kind: 'paragraph',
106+
id: 'p1',
107+
attrs: {},
108+
runs: [
109+
{
110+
kind: 'tab',
111+
text: '\t',
112+
pmStart: 1,
113+
pmEnd: 2,
114+
underline: { style: 'single', color: '#000000' },
115+
...overrides,
116+
} as TabRun,
117+
],
118+
});
119+
120+
it('produces a different version when the tab fontSize changes', () => {
121+
const small = deriveBlockVersion(makeStyledTabParagraph({ fontSize: 12 }));
122+
const large = deriveBlockVersion(makeStyledTabParagraph({ fontSize: 24 }));
123+
expect(large).not.toBe(small);
124+
});
125+
126+
it('produces a different version when the tab fontFamily changes', () => {
127+
const arial = deriveBlockVersion(makeStyledTabParagraph({ fontFamily: 'Arial' }));
128+
const times = deriveBlockVersion(makeStyledTabParagraph({ fontFamily: 'Times New Roman' }));
129+
expect(times).not.toBe(arial);
130+
});
131+
132+
it('produces a different version when the tab run color changes', () => {
133+
const black = deriveBlockVersion(makeStyledTabParagraph({ color: '#000000' }));
134+
const red = deriveBlockVersion(makeStyledTabParagraph({ color: '#FF0000' }));
135+
expect(red).not.toBe(black);
136+
});
137+
138+
// SD-3330 review: tab-only line metrics now come from the tab's font via getFontInfoFromRun, which
139+
// feeds bold/italic into the measured ascent/descent, so toggling them must change the version.
140+
it('produces a different version when the tab bold changes', () => {
141+
const plain = deriveBlockVersion(makeStyledTabParagraph({ bold: false }));
142+
const bold = deriveBlockVersion(makeStyledTabParagraph({ bold: true }));
143+
expect(bold).not.toBe(plain);
144+
});
145+
146+
it('produces a different version when the tab italic changes', () => {
147+
const plain = deriveBlockVersion(makeStyledTabParagraph({ italic: false }));
148+
const italic = deriveBlockVersion(makeStyledTabParagraph({ italic: true }));
149+
expect(italic).not.toBe(plain);
150+
});
151+
152+
it('is stable when tab fontSize, fontFamily and color are identical', () => {
153+
const a = deriveBlockVersion(makeStyledTabParagraph({ fontSize: 16, fontFamily: 'Arial', color: '#123456' }));
154+
const b = deriveBlockVersion(makeStyledTabParagraph({ fontSize: 16, fontFamily: 'Arial', color: '#123456' }));
155+
expect(a).toBe(b);
156+
});
98157
});
99158

100159
describe('deriveBlockVersion - table image content', () => {

packages/layout-engine/layout-resolved/src/versionSignature.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -307,11 +307,30 @@ export const deriveBlockVersion = (block: FlowBlock): string => {
307307
}
308308

309309
if (run.kind === 'tab') {
310-
// Include the underline (the only mark a tab paints, as a border) so toggling
311-
// underline on a tab changes the block version and the painter repaints it.
312-
// Without this, an underline applied to an already-rendered tab is not shown
313-
// until an unrelated edit forces a rebuild (SD-3330).
314-
return [run.text ?? '', 'tab', run.underline?.style ?? '', run.underline?.color ?? ''].join(',');
310+
// Include every input the painter's tab underline depends on so the paint cache is
311+
// not reused after a relevant change (SD-3330): underline style/color choose the
312+
// mark; fontSize sets its thickness; fontFamily/color feed measured line metrics and
313+
// the resolved underline color. The font epoch matters too: a tab's underline offset
314+
// is derived from measured line metrics, so when a font loads/changes (resolved family
315+
// unchanged, only availability) a tab-only underlined line must repaint - a mixed
316+
// text+tab line is already busted by its text run, but a tab-only line has none.
317+
// bold/italic matter for the same reason: a tab-only line's metrics now come from the
318+
// tab's font via getFontInfoFromRun, which feeds bold/italic into the measured ascent/
319+
// descent (buildFontString), so the underline offset and line height depend on them.
320+
// Without these a font/style/availability change can leave a stale tab underline until an
321+
// unrelated edit forces a rebuild.
322+
return [
323+
run.text ?? '',
324+
'tab',
325+
run.underline?.style ?? '',
326+
run.underline?.color ?? '',
327+
run.fontSize ?? '',
328+
run.fontFamily ?? '',
329+
(run as { bold?: boolean }).bold ? 1 : 0,
330+
(run as { italic?: boolean }).italic ? 1 : 0,
331+
getFontConfigVersion(),
332+
(run as { color?: string }).color ?? '',
333+
].join(',');
315334
}
316335

317336
if (run.kind === 'fieldAnnotation') {

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1614,7 +1614,12 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
16141614
toChar: 1,
16151615
width: 0,
16161616
maxFontSize: lastFontSize,
1617-
maxFontInfo: hasSeenTextRun ? undefined : fallbackFontInfo,
1617+
// A tab-only paragraph has no text run, so fallbackFontInfo is undefined and the line
1618+
// would fall back to synthetic 0.8/0.2 ascent/descent. Derive metrics from the tab's own
1619+
// font (it carries fontFamily/fontSize) so a tab-only underlined line gets the same
1620+
// measured ascent/descent - hence underline offset and line height - as the equivalent
1621+
// text line. getFontInfoFromRun reads only fontFamily/fontSize/bold/italic, all on a TabRun.
1622+
maxFontInfo: hasSeenTextRun ? undefined : (fallbackFontInfo ?? getFontInfoFromRun(run as unknown as TextRun)),
16181623
maxWidth: getEffectiveWidth(lines.length === 0 ? initialAvailableWidth : bodyContentWidth),
16191624
segments: [],
16201625
spaceCount: 0,

0 commit comments

Comments
 (0)