layout.ts: When text fills width (stretch in col parent), MeasureFunc now uses Yoga's constraint width directly instead of clamping to the 100px default fixedWidth. prompt: Emphasize that ALL intermediate flex containers need w="fill", not just the first level. grow inside HUG = zero width.
629 lines
20 KiB
TypeScript
629 lines
20 KiB
TypeScript
/* eslint-disable max-lines -- layout tree building is a single recursive pass */
|
||
import Yoga, {
|
||
Align,
|
||
Direction,
|
||
Display,
|
||
FlexDirection,
|
||
GridTrackType,
|
||
Gutter,
|
||
Justify,
|
||
Edge,
|
||
MeasureMode,
|
||
Overflow,
|
||
PositionType,
|
||
Wrap,
|
||
type Node as YogaNode
|
||
} from 'yoga-layout'
|
||
|
||
import type { GridTrack, SceneGraph, SceneNode } from './scene-graph'
|
||
|
||
export type TextMeasurer = (
|
||
node: SceneNode,
|
||
maxWidth?: number
|
||
) => { width: number; height: number } | null
|
||
|
||
let globalTextMeasurer: TextMeasurer | null = null
|
||
|
||
const GLYPH_WIDTH_FACTOR = 0.6
|
||
|
||
export function setTextMeasurer(measurer: TextMeasurer | null): void {
|
||
globalTextMeasurer = measurer
|
||
}
|
||
|
||
export function computeLayout(graph: SceneGraph, frameId: string): void {
|
||
const frame = graph.getNode(frameId)
|
||
if (!frame || frame.layoutMode === 'NONE') return
|
||
|
||
const yogaRoot =
|
||
frame.layoutMode === 'GRID' ? buildGridTree(graph, frame) : buildYogaTree(graph, frame)
|
||
yogaRoot.calculateLayout(undefined, undefined, Direction.LTR)
|
||
applyYogaLayout(graph, frame, yogaRoot)
|
||
freeYogaTree(yogaRoot)
|
||
}
|
||
|
||
export function computeAllLayouts(graph: SceneGraph, scopeId?: string): void {
|
||
const visited = new Set<string>()
|
||
computeLayoutsBottomUp(graph, scopeId ?? graph.rootId, visited)
|
||
}
|
||
|
||
function computeLayoutsBottomUp(graph: SceneGraph, nodeId: string, visited: Set<string>): void {
|
||
const node = graph.getNode(nodeId)
|
||
if (!node || visited.has(nodeId)) return
|
||
visited.add(nodeId)
|
||
|
||
for (const childId of node.childIds) {
|
||
computeLayoutsBottomUp(graph, childId, visited)
|
||
}
|
||
|
||
if (node.layoutMode !== 'NONE') {
|
||
computeLayout(graph, nodeId)
|
||
}
|
||
}
|
||
|
||
// --- Grid layout ---
|
||
|
||
function mapGridTrack(track: GridTrack): { type: GridTrackType; value: number } {
|
||
switch (track.sizing) {
|
||
case 'FR':
|
||
return { type: GridTrackType.Fr, value: track.value }
|
||
case 'FIXED':
|
||
return { type: GridTrackType.Points, value: track.value }
|
||
default:
|
||
return { type: GridTrackType.Auto, value: 0 }
|
||
}
|
||
}
|
||
|
||
function configureAsGrid(yogaNode: YogaNode, node: SceneNode): void {
|
||
yogaNode.setDisplay(Display.Grid)
|
||
yogaNode.setWidth(node.width)
|
||
if (node.gridTemplateRows.length > 0 || node.height > 0) {
|
||
yogaNode.setHeight(node.height)
|
||
}
|
||
|
||
if (node.gridTemplateColumns.length > 0) {
|
||
yogaNode.setGridTemplateColumns(node.gridTemplateColumns.map(mapGridTrack))
|
||
}
|
||
if (node.gridTemplateRows.length > 0) {
|
||
yogaNode.setGridTemplateRows(node.gridTemplateRows.map(mapGridTrack))
|
||
}
|
||
|
||
yogaNode.setGap(Gutter.Column, node.gridColumnGap)
|
||
yogaNode.setGap(Gutter.Row, node.gridRowGap)
|
||
|
||
yogaNode.setPadding(Edge.Top, node.paddingTop)
|
||
yogaNode.setPadding(Edge.Right, node.paddingRight)
|
||
yogaNode.setPadding(Edge.Bottom, node.paddingBottom)
|
||
yogaNode.setPadding(Edge.Left, node.paddingLeft)
|
||
}
|
||
|
||
function createGridChildNode(child: SceneNode): YogaNode {
|
||
const yogaChild = Yoga.Node.create()
|
||
if (!child.visible) {
|
||
yogaChild.setDisplay(Display.None)
|
||
} else {
|
||
const pos = child.gridPosition
|
||
if (pos) {
|
||
yogaChild.setGridColumnStart(pos.column)
|
||
yogaChild.setGridColumnEndSpan(pos.columnSpan)
|
||
yogaChild.setGridRowStart(pos.row)
|
||
yogaChild.setGridRowEndSpan(pos.rowSpan)
|
||
}
|
||
const hasLayout = child.layoutMode !== 'NONE'
|
||
const explicitStretch = child.layoutGrow > 0 || child.layoutAlignSelf === 'STRETCH'
|
||
|
||
if (explicitStretch || hasLayout) {
|
||
yogaChild.setWidthStretch()
|
||
} else {
|
||
yogaChild.setWidth(child.width)
|
||
}
|
||
if (explicitStretch) {
|
||
yogaChild.setHeightStretch()
|
||
} else {
|
||
yogaChild.setHeight(child.height)
|
||
}
|
||
}
|
||
return yogaChild
|
||
}
|
||
|
||
function buildGridTree(graph: SceneGraph, frame: SceneNode): YogaNode {
|
||
const root = Yoga.Node.create()
|
||
configureAsGrid(root, frame)
|
||
|
||
const children = graph.getChildren(frame.id)
|
||
for (const child of children) {
|
||
if (child.layoutPositioning === 'ABSOLUTE') {
|
||
const yogaChild = Yoga.Node.create()
|
||
configureAbsoluteChild(yogaChild, child)
|
||
root.insertChild(yogaChild, root.getChildCount())
|
||
} else {
|
||
root.insertChild(createGridChildNode(child), root.getChildCount())
|
||
}
|
||
}
|
||
|
||
return root
|
||
}
|
||
|
||
// --- Flex layout ---
|
||
|
||
function buildYogaTree(graph: SceneGraph, frame: SceneNode): YogaNode {
|
||
const root = Yoga.Node.create()
|
||
|
||
if (frame.primaryAxisSizing === 'FIXED') {
|
||
if (frame.layoutMode === 'HORIZONTAL') root.setWidth(frame.width)
|
||
else root.setHeight(frame.height)
|
||
}
|
||
if (frame.counterAxisSizing === 'FIXED') {
|
||
if (frame.layoutMode === 'HORIZONTAL') root.setHeight(frame.height)
|
||
else root.setWidth(frame.width)
|
||
}
|
||
|
||
configureFlexContainer(root, frame)
|
||
|
||
const children = graph.getChildren(frame.id)
|
||
for (const child of children) {
|
||
const yogaChild = Yoga.Node.create()
|
||
|
||
if (child.layoutPositioning === 'ABSOLUTE') {
|
||
configureAbsoluteChild(yogaChild, child)
|
||
} else if (!child.visible) {
|
||
yogaChild.setDisplay(Display.None)
|
||
} else if (child.layoutMode === 'GRID') {
|
||
configureChildAsGrid(yogaChild, child, frame, graph)
|
||
} else if (child.layoutMode !== 'NONE') {
|
||
configureChildAsAutoLayout(yogaChild, child, frame, graph)
|
||
} else {
|
||
configureChildAsLeaf(yogaChild, child, frame)
|
||
}
|
||
|
||
root.insertChild(yogaChild, root.getChildCount())
|
||
}
|
||
|
||
return root
|
||
}
|
||
|
||
function configureAbsoluteChild(yogaChild: YogaNode, child: SceneNode): void {
|
||
yogaChild.setPositionType(PositionType.Absolute)
|
||
yogaChild.setPosition(Edge.Left, child.x)
|
||
yogaChild.setPosition(Edge.Top, child.y)
|
||
yogaChild.setWidth(child.width)
|
||
yogaChild.setHeight(child.height)
|
||
}
|
||
|
||
function configureFlexContainer(yogaNode: YogaNode, node: SceneNode): void {
|
||
yogaNode.setFlexDirection(
|
||
node.layoutMode === 'HORIZONTAL' ? FlexDirection.Row : FlexDirection.Column
|
||
)
|
||
yogaNode.setFlexWrap(node.layoutWrap === 'WRAP' ? Wrap.Wrap : Wrap.NoWrap)
|
||
yogaNode.setJustifyContent(mapJustify(node.primaryAxisAlign))
|
||
yogaNode.setAlignItems(mapAlign(node.counterAxisAlign))
|
||
if (node.clipsContent) yogaNode.setOverflow(Overflow.Hidden)
|
||
|
||
if (node.layoutWrap === 'WRAP' && node.counterAxisAlignContent === 'SPACE_BETWEEN') {
|
||
yogaNode.setAlignContent(Align.SpaceBetween)
|
||
}
|
||
|
||
yogaNode.setPadding(Edge.Top, node.paddingTop)
|
||
yogaNode.setPadding(Edge.Right, node.paddingRight)
|
||
yogaNode.setPadding(Edge.Bottom, node.paddingBottom)
|
||
yogaNode.setPadding(Edge.Left, node.paddingLeft)
|
||
|
||
yogaNode.setGap(
|
||
Gutter.Column,
|
||
node.layoutMode === 'HORIZONTAL' ? node.itemSpacing : node.counterAxisSpacing
|
||
)
|
||
yogaNode.setGap(
|
||
Gutter.Row,
|
||
node.layoutMode === 'HORIZONTAL' ? node.counterAxisSpacing : node.itemSpacing
|
||
)
|
||
|
||
applyMinMaxConstraints(yogaNode, node)
|
||
}
|
||
|
||
function configureChildAsGrid(
|
||
yogaChild: YogaNode,
|
||
child: SceneNode,
|
||
parent: SceneNode,
|
||
graph: SceneGraph
|
||
): void {
|
||
yogaChild.setDisplay(Display.Grid)
|
||
|
||
if (child.gridTemplateColumns.length > 0) {
|
||
yogaChild.setGridTemplateColumns(child.gridTemplateColumns.map(mapGridTrack))
|
||
}
|
||
if (child.gridTemplateRows.length > 0) {
|
||
yogaChild.setGridTemplateRows(child.gridTemplateRows.map(mapGridTrack))
|
||
}
|
||
|
||
yogaChild.setGap(Gutter.Column, child.gridColumnGap)
|
||
yogaChild.setGap(Gutter.Row, child.gridRowGap)
|
||
|
||
yogaChild.setPadding(Edge.Top, child.paddingTop)
|
||
yogaChild.setPadding(Edge.Right, child.paddingRight)
|
||
yogaChild.setPadding(Edge.Bottom, child.paddingBottom)
|
||
yogaChild.setPadding(Edge.Left, child.paddingLeft)
|
||
|
||
const isParentRow = parent.layoutMode === 'HORIZONTAL'
|
||
const selfOverride = child.layoutAlignSelf !== 'AUTO'
|
||
const stretchCross = selfOverride
|
||
? child.layoutAlignSelf === 'STRETCH'
|
||
: parent.counterAxisAlign === 'STRETCH'
|
||
|
||
if (child.layoutGrow > 0) {
|
||
yogaChild.setFlexGrow(child.layoutGrow)
|
||
yogaChild.setFlexShrink(1)
|
||
yogaChild.setFlexBasis(0)
|
||
if (!stretchCross) {
|
||
if (isParentRow) yogaChild.setHeight(child.height)
|
||
else yogaChild.setWidth(child.width)
|
||
}
|
||
} else {
|
||
if (isParentRow) {
|
||
yogaChild.setWidth(child.width)
|
||
if (!stretchCross) yogaChild.setHeight(child.height)
|
||
} else {
|
||
if (child.gridTemplateRows.length > 0) yogaChild.setHeight(child.height)
|
||
if (!stretchCross) yogaChild.setWidth(child.width)
|
||
}
|
||
}
|
||
|
||
const selfAlign = mapAlignSelf(child.layoutAlignSelf)
|
||
if (selfAlign != null) yogaChild.setAlignSelf(selfAlign)
|
||
|
||
applyMinMaxConstraints(yogaChild, child)
|
||
|
||
const grandchildren = graph.getChildren(child.id)
|
||
for (const gc of grandchildren) {
|
||
if (gc.layoutPositioning === 'ABSOLUTE') {
|
||
const yogaGC = Yoga.Node.create()
|
||
configureAbsoluteChild(yogaGC, gc)
|
||
yogaChild.insertChild(yogaGC, yogaChild.getChildCount())
|
||
} else {
|
||
yogaChild.insertChild(createGridChildNode(gc), yogaChild.getChildCount())
|
||
}
|
||
}
|
||
}
|
||
|
||
function applyMinMaxConstraints(yogaNode: YogaNode, node: SceneNode): void {
|
||
if (node.minWidth != null) yogaNode.setMinWidth(node.minWidth)
|
||
if (node.maxWidth != null) yogaNode.setMaxWidth(node.maxWidth)
|
||
if (node.minHeight != null) yogaNode.setMinHeight(node.minHeight)
|
||
if (node.maxHeight != null) yogaNode.setMaxHeight(node.maxHeight)
|
||
}
|
||
|
||
function configureChildAsAutoLayout(
|
||
yogaChild: YogaNode,
|
||
child: SceneNode,
|
||
parent: SceneNode,
|
||
graph: SceneGraph
|
||
): void {
|
||
const isParentRow = parent.layoutMode === 'HORIZONTAL'
|
||
const isChildRow = child.layoutMode === 'HORIZONTAL'
|
||
|
||
const widthSizing = isChildRow ? child.primaryAxisSizing : child.counterAxisSizing
|
||
const heightSizing = isChildRow ? child.counterAxisSizing : child.primaryAxisSizing
|
||
|
||
if (isParentRow) {
|
||
setSizing(yogaChild, 'width', widthSizing, child.width, child.layoutGrow)
|
||
setSizing(yogaChild, 'height', heightSizing, child.height, 0)
|
||
} else {
|
||
setSizing(yogaChild, 'width', widthSizing, child.width, 0)
|
||
setSizing(yogaChild, 'height', heightSizing, child.height, child.layoutGrow)
|
||
}
|
||
|
||
const selfAlign = mapAlignSelf(child.layoutAlignSelf)
|
||
if (selfAlign != null) yogaChild.setAlignSelf(selfAlign)
|
||
|
||
configureFlexContainer(yogaChild, child)
|
||
|
||
const grandchildren = graph.getChildren(child.id)
|
||
for (const gc of grandchildren) {
|
||
const yogaGC = Yoga.Node.create()
|
||
if (gc.layoutPositioning === 'ABSOLUTE') {
|
||
configureAbsoluteChild(yogaGC, gc)
|
||
} else if (!gc.visible) {
|
||
yogaGC.setDisplay(Display.None)
|
||
} else if (gc.layoutMode === 'GRID') {
|
||
configureChildAsGrid(yogaGC, gc, child, graph)
|
||
} else if (gc.layoutMode !== 'NONE') {
|
||
configureChildAsAutoLayout(yogaGC, gc, child, graph)
|
||
} else {
|
||
configureChildAsLeaf(yogaGC, gc, child)
|
||
}
|
||
yogaChild.insertChild(yogaGC, yogaChild.getChildCount())
|
||
}
|
||
}
|
||
|
||
// Rough estimate for text size when CanvasKit/font is not available.
|
||
// Uses ~0.6 × fontSize per character as average glyph width.
|
||
// DO NOT REMOVE: without this, text nodes keep their 100×100 default size
|
||
// and blow up every HUG container. The real MeasureFunc (CanvasKit) overrides
|
||
// this when available — this is only the fallback.
|
||
const GLYPH_WIDTH_FACTOR = 0.6
|
||
function estimateTextSize(node: SceneNode): { width: number; height: number } {
|
||
const fontSize = node.fontSize || 14
|
||
const text = node.characters || node.text || ''
|
||
const lineHeight = fontSize * 1.2
|
||
const charWidth = fontSize * GLYPH_WIDTH_FACTOR
|
||
const width = Math.ceil(text.length * charWidth)
|
||
const height = Math.ceil(lineHeight)
|
||
return { width, height }
|
||
}
|
||
|
||
function configureChildAsLeaf(yogaChild: YogaNode, child: SceneNode, parent: SceneNode): void {
|
||
const isRow = parent.layoutMode === 'HORIZONTAL'
|
||
const selfOverride = child.layoutAlignSelf !== 'AUTO'
|
||
const stretchCross = selfOverride
|
||
? child.layoutAlignSelf === 'STRETCH'
|
||
: parent.counterAxisAlign === 'STRETCH'
|
||
|
||
const isText = child.type === 'TEXT'
|
||
const needsMeasureFunc =
|
||
isText && globalTextMeasurer && child.textAutoResize !== 'NONE'
|
||
|
||
if (needsMeasureFunc) {
|
||
configureTextLeaf(yogaChild, child, parent)
|
||
} else if (isText && !globalTextMeasurer && child.textAutoResize !== 'NONE') {
|
||
// No CanvasKit — use rough estimate so HUG containers don't inherit
|
||
// the 100×100 default SceneNode size. See estimateTextSize above.
|
||
const est = estimateTextSize(child)
|
||
if (child.textAutoResize === 'WIDTH_AND_HEIGHT') {
|
||
yogaChild.setWidth(est.width)
|
||
yogaChild.setHeight(est.height)
|
||
} else if (child.textAutoResize === 'HEIGHT') {
|
||
const stretches = child.layoutAlignSelf === 'STRETCH' ||
|
||
(child.layoutAlignSelf === 'AUTO' && parent.counterAxisAlign === 'STRETCH')
|
||
if (!(!isRow && stretches)) {
|
||
yogaChild.setWidth(child.width)
|
||
}
|
||
yogaChild.setHeight(est.height)
|
||
}
|
||
} else {
|
||
configureNonTextLeaf(yogaChild, child, isRow, stretchCross)
|
||
}
|
||
|
||
const selfAlign = mapAlignSelf(child.layoutAlignSelf)
|
||
if (selfAlign != null) yogaChild.setAlignSelf(selfAlign)
|
||
|
||
applyMinMaxConstraints(yogaChild, child)
|
||
}
|
||
|
||
|
||
|
||
function configureTextLeaf(
|
||
yogaChild: YogaNode,
|
||
child: SceneNode,
|
||
_parent: SceneNode
|
||
): void {
|
||
const autoResize = child.textAutoResize
|
||
|
||
if (child.layoutGrow > 0) {
|
||
yogaChild.setFlexGrow(child.layoutGrow)
|
||
}
|
||
|
||
const cache = new Map<number, { width: number; height: number }>()
|
||
const UNCONSTRAINED_KEY = -1
|
||
|
||
if (autoResize === 'WIDTH_AND_HEIGHT') {
|
||
yogaChild.setMeasureFunc((width, widthMode, _height, _heightMode) => {
|
||
const maxW = widthMode === MeasureMode.Undefined ? undefined : width
|
||
const cacheKey = maxW === undefined ? UNCONSTRAINED_KEY : Math.round(maxW)
|
||
const cached = cache.get(cacheKey)
|
||
if (cached) return cached
|
||
|
||
const measured = globalTextMeasurer?.(child, maxW)
|
||
const result = measured ?? estimateTextSize(child)
|
||
cache.set(cacheKey, result)
|
||
return result
|
||
})
|
||
} else if (autoResize === 'HEIGHT') {
|
||
const stretchesCross = child.layoutAlignSelf === 'STRETCH' ||
|
||
(child.layoutAlignSelf === 'AUTO' && parent.counterAxisAlign === 'STRETCH')
|
||
// Don't set fixed width when text stretches on cross axis (w="fill" in
|
||
// flex="col" parent) — setWidth blocks Yoga's alignSelf:stretch, leaving
|
||
// text at 100px default instead of filling the parent.
|
||
const fillsWidth = !isRow && stretchesCross
|
||
const fixedWidth = child.width
|
||
if (child.layoutGrow <= 0 && !fillsWidth) {
|
||
yogaChild.setWidth(fixedWidth)
|
||
}
|
||
yogaChild.setMeasureFunc((width, widthMode, _height, _heightMode) => {
|
||
const constraintW = fillsWidth
|
||
? (widthMode === MeasureMode.Undefined ? fixedWidth : width)
|
||
: (widthMode === MeasureMode.Undefined ? fixedWidth : Math.min(width, fixedWidth || width))
|
||
const cacheKey = Math.round(constraintW)
|
||
const cached = cache.get(cacheKey)
|
||
if (cached) return cached
|
||
|
||
const measured = globalTextMeasurer?.(child, constraintW)
|
||
const result = { width: constraintW, height: measured?.height ?? estimateTextSize(child).height }
|
||
cache.set(cacheKey, result)
|
||
return result
|
||
})
|
||
}
|
||
}
|
||
|
||
function configureNonTextLeaf(
|
||
yogaChild: YogaNode,
|
||
child: SceneNode,
|
||
isRow: boolean,
|
||
stretchCross: boolean
|
||
): void {
|
||
const w = child.width
|
||
const h = child.height
|
||
|
||
if (child.layoutGrow > 0) {
|
||
yogaChild.setFlexGrow(child.layoutGrow)
|
||
if (!stretchCross) {
|
||
if (isRow) yogaChild.setHeight(h)
|
||
else yogaChild.setWidth(w)
|
||
}
|
||
} else {
|
||
if (isRow) {
|
||
yogaChild.setWidth(w)
|
||
if (!stretchCross) yogaChild.setHeight(h)
|
||
} else {
|
||
yogaChild.setHeight(h)
|
||
if (!stretchCross) yogaChild.setWidth(w)
|
||
}
|
||
}
|
||
}
|
||
|
||
function setSizing(
|
||
yogaNode: YogaNode,
|
||
axis: 'width' | 'height',
|
||
sizing: string,
|
||
fixedValue: number,
|
||
grow: number
|
||
): void {
|
||
if (grow > 0) {
|
||
yogaNode.setFlexGrow(grow)
|
||
yogaNode.setFlexShrink(1)
|
||
yogaNode.setFlexBasis(0)
|
||
return
|
||
}
|
||
|
||
switch (sizing) {
|
||
case 'FIXED':
|
||
if (axis === 'width') yogaNode.setWidth(fixedValue)
|
||
else yogaNode.setHeight(fixedValue)
|
||
break
|
||
case 'HUG':
|
||
break
|
||
case 'FILL':
|
||
yogaNode.setFlexGrow(1)
|
||
yogaNode.setFlexShrink(1)
|
||
yogaNode.setFlexBasis(0)
|
||
break
|
||
}
|
||
}
|
||
|
||
function applyFrameSize(graph: SceneGraph, frame: SceneNode, yogaNode: YogaNode): void {
|
||
if (frame.layoutMode === 'GRID') {
|
||
if (frame.gridTemplateRows.length === 0) {
|
||
graph.updateNode(frame.id, { height: yogaNode.getComputedHeight() })
|
||
}
|
||
return
|
||
}
|
||
|
||
if (frame.primaryAxisSizing !== 'HUG' && frame.counterAxisSizing !== 'HUG') return
|
||
|
||
const computedW = yogaNode.getComputedWidth()
|
||
const computedH = yogaNode.getComputedHeight()
|
||
const updates: Partial<SceneNode> = {}
|
||
|
||
if (frame.primaryAxisSizing === 'HUG') {
|
||
if (frame.layoutMode === 'HORIZONTAL') updates.width = computedW
|
||
else updates.height = computedH
|
||
}
|
||
if (frame.counterAxisSizing === 'HUG') {
|
||
if (frame.layoutMode === 'HORIZONTAL') updates.height = computedH
|
||
else updates.width = computedW
|
||
}
|
||
|
||
graph.updateNode(frame.id, updates)
|
||
}
|
||
|
||
function applyYogaLayout(graph: SceneGraph, frame: SceneNode, yogaNode: YogaNode): void {
|
||
applyFrameSize(graph, frame, yogaNode)
|
||
|
||
const children = graph.getChildren(frame.id)
|
||
let yogaIndex = 0
|
||
for (const child of children) {
|
||
const yogaChild = yogaNode.getChild(yogaIndex)
|
||
yogaIndex++
|
||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||
if (!yogaChild) continue
|
||
|
||
if (child.visible && child.layoutPositioning !== 'ABSOLUTE') {
|
||
graph.updateNode(child.id, {
|
||
x: yogaChild.getComputedLeft(),
|
||
y: yogaChild.getComputedTop(),
|
||
width: yogaChild.getComputedWidth(),
|
||
height: yogaChild.getComputedHeight()
|
||
})
|
||
}
|
||
|
||
if (child.layoutMode !== 'NONE') {
|
||
if (child.layoutMode === 'GRID' && child.visible && child.layoutPositioning !== 'ABSOLUTE') {
|
||
computeLayout(graph, child.id)
|
||
} else if (frame.layoutMode === 'GRID' && child.visible && child.layoutPositioning !== 'ABSOLUTE') {
|
||
recomputeGridChild(graph, child)
|
||
} else {
|
||
applyYogaLayout(graph, child, yogaChild)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function recomputeGridChild(graph: SceneGraph, child: SceneNode): void {
|
||
const updated = graph.getNode(child.id)
|
||
if (!updated || updated.layoutMode === 'NONE') return
|
||
|
||
const savedPrimary = updated.primaryAxisSizing
|
||
const savedCounter = updated.counterAxisSizing
|
||
const updates: Partial<SceneNode> = {}
|
||
|
||
if (savedPrimary === 'HUG') updates.primaryAxisSizing = 'FIXED'
|
||
if (savedCounter === 'HUG') updates.counterAxisSizing = 'FIXED'
|
||
if (Object.keys(updates).length > 0) graph.updateNode(child.id, updates)
|
||
|
||
computeLayout(graph, child.id)
|
||
|
||
const restore: Partial<SceneNode> = {}
|
||
if (updates.primaryAxisSizing) restore.primaryAxisSizing = savedPrimary
|
||
if (updates.counterAxisSizing) restore.counterAxisSizing = savedCounter
|
||
if (Object.keys(restore).length > 0) graph.updateNode(child.id, restore)
|
||
}
|
||
|
||
function freeYogaTree(node: YogaNode): void {
|
||
for (let i = node.getChildCount() - 1; i >= 0; i--) {
|
||
freeYogaTree(node.getChild(i))
|
||
}
|
||
if ('free' in node) (node as { free(): void }).free()
|
||
}
|
||
|
||
function mapJustify(align: string): Justify {
|
||
switch (align) {
|
||
case 'CENTER':
|
||
return Justify.Center
|
||
case 'MAX':
|
||
return Justify.FlexEnd
|
||
case 'SPACE_BETWEEN':
|
||
return Justify.SpaceBetween
|
||
default:
|
||
return Justify.FlexStart
|
||
}
|
||
}
|
||
|
||
function mapAlign(align: string): Align {
|
||
switch (align) {
|
||
case 'CENTER':
|
||
return Align.Center
|
||
case 'MAX':
|
||
return Align.FlexEnd
|
||
case 'STRETCH':
|
||
return Align.Stretch
|
||
case 'BASELINE':
|
||
return Align.Baseline
|
||
default:
|
||
return Align.FlexStart
|
||
}
|
||
}
|
||
|
||
function mapAlignSelf(alignSelf: string): Align | null {
|
||
switch (alignSelf) {
|
||
case 'MIN':
|
||
return Align.FlexStart
|
||
case 'CENTER':
|
||
return Align.Center
|
||
case 'MAX':
|
||
return Align.FlexEnd
|
||
case 'STRETCH':
|
||
return Align.Stretch
|
||
case 'BASELINE':
|
||
return Align.Baseline
|
||
default:
|
||
return null
|
||
}
|
||
}
|