From 9caa89e6f8c4e57aec9729f8651ec5fe50c01fe5 Mon Sep 17 00:00:00 2001 From: Anton A S Date: Tue, 10 Mar 2026 22:42:52 +0300 Subject: [PATCH] =?UTF-8?q?Fix=20text=20layout=20without=20CanvasKit:=20es?= =?UTF-8?q?timate=20size=20instead=20of=20100=C3=97100=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/core/src/layout.ts | 23 +++++++++++++++++++++++ packages/core/src/render/renderer.ts | 1 + tests/engine/layout.test.ts | 8 ++++++-- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/core/src/layout.ts b/packages/core/src/layout.ts index 9b1a35d01..6c85cdbfa 100644 --- a/packages/core/src/layout.ts +++ b/packages/core/src/layout.ts @@ -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, diff --git a/packages/core/src/render/renderer.ts b/packages/core/src/render/renderer.ts index 9701ddd1e..f038e7fa1 100644 --- a/packages/core/src/render/renderer.ts +++ b/packages/core/src/render/renderer.ts @@ -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)) { diff --git a/tests/engine/layout.test.ts b/tests/engine/layout.test.ts index 7c681044a..3bb323123 100644 --- a/tests/engine/layout.test.ts +++ b/tests/engine/layout.test.ts @@ -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', () => {