From f92577608a472479f7319e151b36ce8b363092a1 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 24 May 2026 20:12:48 +0300 Subject: [PATCH] refactor(Font): use pretext layout() directly in row size resolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit createFontRowSizeResolver was reaching into TextLayoutEngine, which internally called pretext's heavy layoutWithLines and then walked per-grapheme cursors to build chars arrays. The resolver discarded all that work and used only totalHeight. Replace with direct prepare + layout from @chenglou/pretext — pretext docs explicitly recommend layout() (not layoutWithLines) for the resize hot path: pure arithmetic on cached segment widths, no canvas calls, no string allocations. Test spies on TextLayoutEngine.prototype.layout migrated to vi.mock-ed pretext layout (pretext's ESM exports are frozen — vi.spyOn fails with "Cannot redefine property"). TextLayoutEngine marked @deprecated since it has no remaining consumers; slated for removal after a release cycle. --- .../createFontRowSizeResolver.test.ts | 24 ++++++++++++++----- .../sizeResolver/createFontRowSizeResolver.ts | 14 +++++++---- .../TextLayoutEngine.svelte.ts | 8 +++++++ 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/entities/Font/lib/sizeResolver/createFontRowSizeResolver.test.ts b/src/entities/Font/lib/sizeResolver/createFontRowSizeResolver.test.ts index 33525d6..a274b40 100644 --- a/src/entities/Font/lib/sizeResolver/createFontRowSizeResolver.test.ts +++ b/src/entities/Font/lib/sizeResolver/createFontRowSizeResolver.test.ts @@ -1,7 +1,19 @@ // @vitest-environment jsdom -import { TextLayoutEngine } from '$shared/lib'; import { installCanvasMock } from '$shared/lib/helpers/__mocks__/canvas'; -import { clearCache } from '@chenglou/pretext'; +import { + clearCache, + layout, +} from '@chenglou/pretext'; + +// Wrap pretext's `layout` in a spy-able mock so tests can assert call counts. +// `vi.mock` is hoisted, so the import above receives the mocked module. +vi.mock('@chenglou/pretext', async () => { + const actual = await vi.importActual('@chenglou/pretext'); + return { + ...actual, + layout: vi.fn(actual.layout), + }; +}); import { beforeEach, describe, @@ -112,13 +124,13 @@ describe('createFontRowSizeResolver', () => { const { resolver } = makeResolver(); statusMap.set('inter@400', 'loaded'); - const layoutSpy = vi.spyOn(TextLayoutEngine.prototype, 'layout'); + const layoutSpy = vi.mocked(layout); + layoutSpy.mockClear(); resolver(0); resolver(0); expect(layoutSpy).toHaveBeenCalledTimes(1); - layoutSpy.mockRestore(); }); it('calls layout() again when containerWidth changes (cache miss)', () => { @@ -126,14 +138,14 @@ describe('createFontRowSizeResolver', () => { const { resolver } = makeResolver({ getContainerWidth: () => width }); statusMap.set('inter@400', 'loaded'); - const layoutSpy = vi.spyOn(TextLayoutEngine.prototype, 'layout'); + const layoutSpy = vi.mocked(layout); + layoutSpy.mockClear(); resolver(0); width = 100; resolver(0); expect(layoutSpy).toHaveBeenCalledTimes(2); - layoutSpy.mockRestore(); }); it('returns greater height when container narrows (more wrapping)', () => { diff --git a/src/entities/Font/lib/sizeResolver/createFontRowSizeResolver.ts b/src/entities/Font/lib/sizeResolver/createFontRowSizeResolver.ts index 93e76fa..97e089c 100644 --- a/src/entities/Font/lib/sizeResolver/createFontRowSizeResolver.ts +++ b/src/entities/Font/lib/sizeResolver/createFontRowSizeResolver.ts @@ -1,4 +1,7 @@ -import { TextLayoutEngine } from '$shared/lib'; +import { + layout, + prepare, +} from '@chenglou/pretext'; import { generateFontKey } from '../../model/store/fontLifecycleManager/utils/generateFontKey/generateFontKey'; import type { FontLoadStatus, @@ -79,14 +82,13 @@ export interface FontRowSizeResolverOptions { * no DOM snap occurs. * * **Caching:** A `Map` keyed by `fontCssString|text|contentWidth|lineHeightPx` - * prevents redundant `TextLayoutEngine.layout()` calls. The cache is invalidated + * prevents redundant `pretext.layout()` calls. The cache is invalidated * naturally because a change in any input produces a different cache key. * * @param options - Configuration and getter functions (all injected for testability). * @returns A function `(rowIndex: number) => number` for use as `VirtualList.itemHeight`. */ export function createFontRowSizeResolver(options: FontRowSizeResolverOptions): (rowIndex: number) => number { - const engine = new TextLayoutEngine(); // Key: `${fontCssString}|${text}|${contentWidth}|${lineHeightPx}` const cache = new Map(); @@ -126,7 +128,11 @@ export function createFontRowSizeResolver(options: FontRowSizeResolverOptions): return cached; } - const { totalHeight } = engine.layout(previewText, fontCssString, contentWidth, lineHeightPx); + // Pretext docs recommend `layout()` (not `layoutWithLines`) for the + // resize hot path — pure arithmetic on cached segment widths, no canvas + // calls, no string allocations. + const prepared = prepare(previewText, fontCssString); + const { height: totalHeight } = layout(prepared, contentWidth, lineHeightPx); const result = totalHeight + options.chromeHeight; cache.set(cacheKey, result); return result; diff --git a/src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.svelte.ts b/src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.svelte.ts index b581905..3b7b1f9 100644 --- a/src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.svelte.ts +++ b/src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.svelte.ts @@ -70,6 +70,14 @@ export interface LayoutResult { * **Canvas requirement:** pretext calls `document.createElement('canvas').getContext('2d')` on * first use and caches the context for the process lifetime. Tests must install a canvas mock * (see `__mocks__/canvas.ts`) before the first `layout()` call. + * + * @deprecated No live consumers remain — the only previous caller + * (`createFontRowSizeResolver`) now invokes pretext's `prepare` + `layout` + * directly (per pretext's "hot-path resize function" guidance). If you need + * single-font height-only measurement, use `prepare` + `layout` from + * `@chenglou/pretext` directly. If you need per-grapheme x/width data, see + * `CharacterComparisonEngine` (dual-font) or revive a slimmer wrapper. + * Slated for removal once it has been absent from `main` for a release cycle. */ export class TextLayoutEngine { /**