From 0632521e7c874ce1e84645c9953b151c251866af Mon Sep 17 00:00:00 2001 From: Jan Kubica Date: Fri, 24 Jul 2026 10:23:06 +0200 Subject: [PATCH 1/2] perf: reuse visible word measurements --- .../measure/measureParagraph.test.ts | 73 ++++++++++++++++++- .../layout-engine/measure/measureParagraph.ts | 36 ++++++++- 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/packages/core/src/layout-engine/measure/measureParagraph.test.ts b/packages/core/src/layout-engine/measure/measureParagraph.test.ts index 399479d0..de226255 100644 --- a/packages/core/src/layout-engine/measure/measureParagraph.test.ts +++ b/packages/core/src/layout-engine/measure/measureParagraph.test.ts @@ -8,9 +8,14 @@ import { } from "./__tests__/fakeTextMeasure"; import { hashParagraphBlock } from "./cache"; import { resetLineBreakProvider, setLineBreakProvider } from "./lineBreakProvider"; -import { buildFontString } from "./measureHelpers"; +import { buildFontString, buildRunFontStyle } from "./measureHelpers"; import { clampFloatingWrapMargins, getRunCharWidths, measureParagraph } from "./measureParagraph"; -import { getFontMetrics, measureTextWidth } from "./measureProvider"; +import { + getFontMetrics, + getMeasureProvider, + measureTextWidth, + setMeasureProvider, +} from "./measureProvider"; const PT_TO_PX = 96 / 72; @@ -550,6 +555,70 @@ describe("measureParagraph cross-run line breaking", () => { charIndex: number, ): boolean => lines.some((line) => line.fromRun === runIndex && line.fromChar === charIndex); + test("reuses visible-word measurements for ordinary trailing spaces", () => { + withFakeTextMeasure( + (getMeasureCount) => { + const text = "alpha beta gamma delta epsilon zeta eta theta"; + measureParagraph(paragraph([{ kind: "text", text }]), 1000); + + // Eight visible words, one shared space measurement, and one font-metrics probe. + expect(getMeasureCount()).toBe(10); + }, + { charWidth: fixedCharWidth(5) }, + ); + }); + + test("includes scaled letter spacing at the visible-word boundary", () => { + withFakeTextMeasure( + () => { + const run = { + kind: "text" as const, + text: "ab cd", + letterSpacing: 2, + horizontalScale: 150, + }; + const fontStyle = buildRunFontStyle(run, "Calibri", 11); + const expectedWidth = + measureTextWidth("ab ", fontStyle) + measureTextWidth("cd", fontStyle); + const measured = measureParagraph(paragraph([run]), 1000); + + expect(measured.lines[0]?.width).toBe(expectedWidth); + }, + { charWidth: fixedCharWidth(5) }, + ); + }); + + test("keeps exact full-string measurement for kerning and literal tabs", () => { + withFakeTextMeasure( + () => { + const baseProvider = getMeasureProvider(); + setMeasureProvider({ + ...baseProvider, + measureTextWidth: (text, measuredStyle) => { + const measuredWidth = baseProvider.measureTextWidth(text, measuredStyle); + if (measuredStyle.kerning && text === "AV ") { + return measuredWidth - 2; + } + if (text === "a\t") { + return 40; + } + return measuredWidth; + }, + }); + + const kerned = measureParagraph( + paragraph([{ kind: "text", text: "AV X", kerningMinPt: 1 }]), + 1000, + ); + const tabbed = measureParagraph(paragraph([{ kind: "text", text: "a\tb" }]), 1000); + + expect(kerned.lines[0]?.width).toBe(18); + expect(tabbed.lines[0]?.width).toBe(45); + }, + { charWidth: fixedCharWidth(5) }, + ); + }); + test("keeps an adjacent footnote marker glued to the preceding word", () => { withFakeTextMeasure(() => { const runs: Run[] = [ diff --git a/packages/core/src/layout-engine/measure/measureParagraph.ts b/packages/core/src/layout-engine/measure/measureParagraph.ts index 331c8552..564502f8 100644 --- a/packages/core/src/layout-engine/measure/measureParagraph.ts +++ b/packages/core/src/layout-engine/measure/measureParagraph.ts @@ -734,6 +734,35 @@ function trimTrailingSpacesAndTabs(text: string): string { return text.slice(0, end); } +function measureWordWithTrailingWhitespace( + word: string, + style: FontStyle, +): { measuredWord: string; wordWidth: number; fullWordWidth: number } { + const measuredWord = trimTrailingSpacesAndTabs(word); + const wordWidth = measureTextWidth(measuredWord, style); + const trailingWhitespace = word.slice(measuredWord.length); + if (trailingWhitespace.length === 0) { + return { measuredWord, wordWidth, fullWordWidth: wordWidth }; + } + + // Tabs have contextual advances, while kerning can change the advance at + // the split boundary. Keep the exact full-string measurement for both. + if (style.kerning || trailingWhitespace.includes("\t")) { + return { measuredWord, wordWidth, fullWordWidth: measureTextWidth(word, style) }; + } + + const trailingWhitespaceWidth = measureTextWidth(trailingWhitespace, style); + const boundarySpacing = + measuredWord.length > 0 + ? (style.letterSpacing ?? 0) * ((style.horizontalScale ?? 100) / 100) + : 0; + return { + measuredWord, + wordWidth, + fullWordWidth: wordWidth + trailingWhitespaceWidth + boundarySpacing, + }; +} + function trailingCodePoint(text: string): string | undefined { if (text.length === 0) { return undefined; @@ -1835,9 +1864,10 @@ export function measureParagraph( // Keep the full width while accumulating in case another word follows // on this line, then remove any whitespace still trailing at finalize. const word = text.slice(charIndex, nextBreak); - const measuredWord = trimTrailingSpacesAndTabs(word); - const wordWidth = measureTextWidth(measuredWord, style); - const fullWordWidth = measureTextWidth(word, style); + const { measuredWord, wordWidth, fullWordWidth } = measureWordWithTrailingWhitespace( + word, + style, + ); const hangingPunctuationWidth = trailingHangingPunctuationWidth( measuredWord, style, From 809890360ecab7a280e4569b5538bab4fd038bf7 Mon Sep 17 00:00:00 2001 From: Jan Kubica Date: Fri, 24 Jul 2026 10:25:51 +0200 Subject: [PATCH 2/2] chore: add word measurement changeset --- .changeset/quick-words-measure.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/quick-words-measure.md diff --git a/.changeset/quick-words-measure.md b/.changeset/quick-words-measure.md new file mode 100644 index 00000000..4a722395 --- /dev/null +++ b/.changeset/quick-words-measure.md @@ -0,0 +1,5 @@ +--- +"@stll/folio-core": patch +--- + +Reduce paragraph layout text measurements by reusing visible word widths for ordinary trailing whitespace.