refactor(Font): use pretext layout() directly in row size resolver
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.
This commit is contained in:
@@ -1,7 +1,19 @@
|
|||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
import { TextLayoutEngine } from '$shared/lib';
|
|
||||||
import { installCanvasMock } from '$shared/lib/helpers/__mocks__/canvas';
|
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<typeof import('@chenglou/pretext')>('@chenglou/pretext');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
layout: vi.fn(actual.layout),
|
||||||
|
};
|
||||||
|
});
|
||||||
import {
|
import {
|
||||||
beforeEach,
|
beforeEach,
|
||||||
describe,
|
describe,
|
||||||
@@ -112,13 +124,13 @@ describe('createFontRowSizeResolver', () => {
|
|||||||
const { resolver } = makeResolver();
|
const { resolver } = makeResolver();
|
||||||
statusMap.set('inter@400', 'loaded');
|
statusMap.set('inter@400', 'loaded');
|
||||||
|
|
||||||
const layoutSpy = vi.spyOn(TextLayoutEngine.prototype, 'layout');
|
const layoutSpy = vi.mocked(layout);
|
||||||
|
layoutSpy.mockClear();
|
||||||
|
|
||||||
resolver(0);
|
resolver(0);
|
||||||
resolver(0);
|
resolver(0);
|
||||||
|
|
||||||
expect(layoutSpy).toHaveBeenCalledTimes(1);
|
expect(layoutSpy).toHaveBeenCalledTimes(1);
|
||||||
layoutSpy.mockRestore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls layout() again when containerWidth changes (cache miss)', () => {
|
it('calls layout() again when containerWidth changes (cache miss)', () => {
|
||||||
@@ -126,14 +138,14 @@ describe('createFontRowSizeResolver', () => {
|
|||||||
const { resolver } = makeResolver({ getContainerWidth: () => width });
|
const { resolver } = makeResolver({ getContainerWidth: () => width });
|
||||||
statusMap.set('inter@400', 'loaded');
|
statusMap.set('inter@400', 'loaded');
|
||||||
|
|
||||||
const layoutSpy = vi.spyOn(TextLayoutEngine.prototype, 'layout');
|
const layoutSpy = vi.mocked(layout);
|
||||||
|
layoutSpy.mockClear();
|
||||||
|
|
||||||
resolver(0);
|
resolver(0);
|
||||||
width = 100;
|
width = 100;
|
||||||
resolver(0);
|
resolver(0);
|
||||||
|
|
||||||
expect(layoutSpy).toHaveBeenCalledTimes(2);
|
expect(layoutSpy).toHaveBeenCalledTimes(2);
|
||||||
layoutSpy.mockRestore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns greater height when container narrows (more wrapping)', () => {
|
it('returns greater height when container narrows (more wrapping)', () => {
|
||||||
|
|||||||
@@ -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 { generateFontKey } from '../../model/store/fontLifecycleManager/utils/generateFontKey/generateFontKey';
|
||||||
import type {
|
import type {
|
||||||
FontLoadStatus,
|
FontLoadStatus,
|
||||||
@@ -79,14 +82,13 @@ export interface FontRowSizeResolverOptions {
|
|||||||
* no DOM snap occurs.
|
* no DOM snap occurs.
|
||||||
*
|
*
|
||||||
* **Caching:** A `Map` keyed by `fontCssString|text|contentWidth|lineHeightPx`
|
* **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.
|
* naturally because a change in any input produces a different cache key.
|
||||||
*
|
*
|
||||||
* @param options - Configuration and getter functions (all injected for testability).
|
* @param options - Configuration and getter functions (all injected for testability).
|
||||||
* @returns A function `(rowIndex: number) => number` for use as `VirtualList.itemHeight`.
|
* @returns A function `(rowIndex: number) => number` for use as `VirtualList.itemHeight`.
|
||||||
*/
|
*/
|
||||||
export function createFontRowSizeResolver(options: FontRowSizeResolverOptions): (rowIndex: number) => number {
|
export function createFontRowSizeResolver(options: FontRowSizeResolverOptions): (rowIndex: number) => number {
|
||||||
const engine = new TextLayoutEngine();
|
|
||||||
// Key: `${fontCssString}|${text}|${contentWidth}|${lineHeightPx}`
|
// Key: `${fontCssString}|${text}|${contentWidth}|${lineHeightPx}`
|
||||||
const cache = new Map<string, number>();
|
const cache = new Map<string, number>();
|
||||||
|
|
||||||
@@ -126,7 +128,11 @@ export function createFontRowSizeResolver(options: FontRowSizeResolverOptions):
|
|||||||
return cached;
|
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;
|
const result = totalHeight + options.chromeHeight;
|
||||||
cache.set(cacheKey, result);
|
cache.set(cacheKey, result);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -70,6 +70,14 @@ export interface LayoutResult {
|
|||||||
* **Canvas requirement:** pretext calls `document.createElement('canvas').getContext('2d')` on
|
* **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
|
* 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.
|
* (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 {
|
export class TextLayoutEngine {
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user