Fix font loading not registering in core cache, add layout text measurement tests

Tauri's loadFont registered fonts with the CanvasKit provider but
not in core's loadedFamilies cache, so isFontLoaded returned false
and measureTextNode returned null during layout recomputation.

Add markFontLoaded() to core and call it from Tauri font loader.
Add tests verifying text measurement integrates with auto-layout
centering.
This commit is contained in:
Danila Poyarkov 2026-03-04 00:16:15 +03:00
parent bf83118f96
commit 9c82cba672
4 changed files with 90 additions and 4 deletions

View file

@ -133,6 +133,12 @@ export async function ensureNodeFont(family: string, weight: number): Promise<vo
await loadFont(family, style)
}
export function markFontLoaded(family: string, style: string, data: ArrayBuffer): void {
const cacheKey = `${family}|${style}`
loadedFamilies.set(cacheKey, data)
registerFontInCanvasKit(family, data)
}
export function isFontLoaded(family: string): boolean {
return [...loadedFamilies.keys()].some((k) => k.startsWith(`${family}|`))
}

View file

@ -57,6 +57,7 @@ export {
initFontService,
getFontProvider,
isFontLoaded,
markFontLoaded,
ensureNodeFont,
styleToWeight,
weightToStyle

View file

@ -1,6 +1,6 @@
export { initFontService, getFontProvider, ensureNodeFont } from '@open-pencil/core'
import { loadFont as loadFontCore, getFontProvider, styleToWeight } from '@open-pencil/core'
import { loadFont as loadFontCore, markFontLoaded, styleToWeight } from '@open-pencil/core'
interface TauriFontFamily {
family: string
@ -60,8 +60,7 @@ export async function loadFont(family: string, style = 'Regular'): Promise<Array
const data = await invoke<number[]>('load_system_font', { family, style })
const buffer = new Uint8Array(data).buffer
const provider = getFontProvider()
if (provider) provider.registerFont(buffer, family)
markFontLoaded(family, style, buffer)
const weight = styleToWeight(style)
const italic = style.toLowerCase().includes('italic') ? 'italic' : 'normal'

View file

@ -1,7 +1,7 @@
import { describe, test, expect } from 'bun:test'
import { SceneGraph, type SceneNode } from '../../src/engine/scene-graph'
import { computeLayout, computeAllLayouts } from '../../src/engine/layout'
import { computeLayout, computeAllLayouts, setTextMeasurer } from '../../src/engine/layout'
function pageId(graph: SceneGraph) {
return graph.getPages()[0].id
@ -885,4 +885,84 @@ describe('Auto Layout', () => {
expect(children[3].y).toBe(90)
})
})
describe('text measurement', () => {
test('WIDTH_AND_HEIGHT text uses measured width in centered layout', () => {
const graph = new SceneGraph()
const pid = pageId(graph)
const frame = autoFrame(graph, pid, {
width: 300,
height: 40,
layoutMode: 'HORIZONTAL',
primaryAxisSizing: 'FIXED',
counterAxisSizing: 'FIXED',
primaryAxisAlign: 'CENTER',
paddingLeft: 10,
paddingRight: 10,
itemSpacing: 10,
})
const arrow1 = graph.createNode('FRAME', frame.id, { width: 20, height: 20 })
const text = graph.createNode('TEXT', frame.id, {
width: 200,
height: 20,
text: 'Test',
fontSize: 14,
textAutoResize: 'WIDTH_AND_HEIGHT' as const,
})
const arrow2 = graph.createNode('FRAME', frame.id, { width: 20, height: 20 })
setTextMeasurer((node) => {
if (node.type === 'TEXT' && node.textAutoResize === 'WIDTH_AND_HEIGHT') {
return { width: 60, height: 20 }
}
return null
})
computeAllLayouts(graph)
setTextMeasurer(null)
const updatedText = graph.getNode(text.id)!
const updatedArrow1 = graph.getNode(arrow1.id)!
const updatedArrow2 = graph.getNode(arrow2.id)!
expect(updatedText.width).toBe(60)
// Total content: 10 + 20 + 10 + 60 + 10 + 20 + 10 = 140
// Free space: 300 - 140 = 160, centered offset = 80
expect(updatedArrow1.x).toBe(90)
expect(updatedText.x).toBe(120)
expect(updatedArrow2.x).toBe(190)
})
test('without measurer, text keeps its existing width', () => {
const graph = new SceneGraph()
const pid = pageId(graph)
const frame = autoFrame(graph, pid, {
width: 300,
height: 40,
layoutMode: 'HORIZONTAL',
primaryAxisSizing: 'FIXED',
counterAxisSizing: 'FIXED',
primaryAxisAlign: 'CENTER',
})
const text = graph.createNode('TEXT', frame.id, {
width: 200,
height: 20,
text: 'Test',
fontSize: 14,
textAutoResize: 'WIDTH_AND_HEIGHT' as const,
})
setTextMeasurer(null)
computeAllLayouts(graph)
const updatedText = graph.getNode(text.id)!
expect(updatedText.width).toBe(200)
})
})
})