Fix text layout without CanvasKit: estimate size instead of 100×100 default

Text nodes with textAutoResize=WIDTH_AND_HEIGHT kept their 100×100
default SceneNode size when MeasureFunc was unavailable (no CanvasKit),
blowing up every HUG container.

Add fallback estimator (~0.6 × fontSize per char) in layout.ts so
headless layout produces sane sizes.

DO NOT change textAutoResize defaults in renderer.ts without testing
headless layout — see comments there and in layout.ts.
This commit is contained in:
Anton A S 2026-03-10 22:42:52 +03:00 committed by Danila Poyarkov
parent 57eaf870af
commit 9caa89e6f8
3 changed files with 30 additions and 2 deletions

View file

@ -23,6 +23,8 @@ export type TextMeasurer = (
let globalTextMeasurer: TextMeasurer | null = null
const GLYPH_WIDTH_FACTOR = 0.6
export function setTextMeasurer(measurer: TextMeasurer | null): void {
globalTextMeasurer = measurer
}
@ -343,6 +345,15 @@ function configureChildAsLeaf(yogaChild: YogaNode, child: SceneNode, parent: Sce
if (needsMeasureFunc) {
configureTextLeaf(yogaChild, child, parent)
} else if (isText && !globalTextMeasurer && child.textAutoResize !== 'NONE') {
const est = estimateTextSize(child)
if (child.textAutoResize === 'WIDTH_AND_HEIGHT') {
yogaChild.setWidth(est.width)
yogaChild.setHeight(est.height)
} else if (child.textAutoResize === 'HEIGHT') {
yogaChild.setWidth(child.width)
yogaChild.setHeight(est.height)
}
} else {
configureNonTextLeaf(yogaChild, child, isRow, stretchCross)
}
@ -353,6 +364,18 @@ function configureChildAsLeaf(yogaChild: YogaNode, child: SceneNode, parent: Sce
applyMinMaxConstraints(yogaChild, child)
}
// Fallback text size estimate when CanvasKit is unavailable (headless/tests).
// Without this, text nodes keep their 100×100 default and blow up HUG containers.
function estimateTextSize(node: SceneNode): { width: number; height: number } {
const fontSize = node.fontSize || 14
const lineHeight = fontSize * 1.2
const charWidth = fontSize * GLYPH_WIDTH_FACTOR
return {
width: Math.ceil(node.text.length * charWidth),
height: Math.ceil(lineHeight)
}
}
function configureTextLeaf(
yogaChild: YogaNode,
child: SceneNode,

View file

@ -401,6 +401,7 @@ function applyTextOverrides(
const fillsParent = w === 'fill' || (props.grow as number) > 0
const isInsideAutoLayout = parentLayout !== 'NONE'
// Coupled with estimateTextSize() in layout.ts — test headless layout after changes.
if (props.textAutoResize) {
o.textAutoResize = TEXT_AUTO_RESIZE_MAP[props.textAutoResize as string] ?? 'NONE'
} else if (hasExplicitWidth || (isInsideAutoLayout && fillsParent)) {

View file

@ -1020,7 +1020,7 @@ describe('Auto Layout', () => {
expect(updatedArrow2.x).toBe(190)
})
test('without measurer, text keeps its existing width', () => {
test('without measurer, text uses estimated size instead of 100x100 default', () => {
const graph = new SceneGraph()
const pid = pageId(graph)
@ -1045,7 +1045,11 @@ describe('Auto Layout', () => {
computeAllLayouts(graph)
const updatedText = graph.getNode(text.id)!
expect(updatedText.width).toBe(200)
// Rough estimate: ~0.6 × fontSize × charCount, not the 100×100 default
expect(updatedText.width).toBeLessThan(100)
expect(updatedText.width).toBeGreaterThan(0)
expect(updatedText.height).toBeLessThan(100)
expect(updatedText.height).toBeGreaterThan(0)
})
test('HEIGHT auto-resize text wraps via MeasureFunc when filling parent', () => {