Chore/architecture refactoring #42

Merged
ilia merged 29 commits from chore/architecture-refactoring into main 2026-05-25 08:43:07 +00:00
3 changed files with 36 additions and 10 deletions
Showing only changes of commit f92577608a - Show all commits
@@ -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 {
/** /**