diff --git a/packages/core/src/layout/engine/layoutEngine.ts b/packages/core/src/layout/engine/layoutEngine.ts index a6cfbe6d..219ca20f 100644 --- a/packages/core/src/layout/engine/layoutEngine.ts +++ b/packages/core/src/layout/engine/layoutEngine.ts @@ -43,9 +43,15 @@ type MeasureCacheEntry = Readonly<{ }>; type MeasureCache = WeakMap; +type LayoutCacheLeaf = Map>; +type LayoutCacheByX = Map; +type LayoutCacheByForcedH = Map; +type LayoutCacheByForcedW = Map; +type LayoutCacheByMaxH = Map; +type LayoutAxisCache = Map; type LayoutCacheEntry = Readonly<{ - row: Map>; - column: Map>; + row: LayoutAxisCache; + column: LayoutAxisCache; }>; type LayoutCache = WeakMap; @@ -80,6 +86,7 @@ const measureCacheStack: MeasureCache[] = []; let activeLayoutCache: LayoutCache | null = null; const layoutCacheStack: LayoutCache[] = []; const syntheticThemedColumnCache = new WeakMap(); +const NULL_FORCED_DIMENSION = -1; function pushMeasureCache(cache: MeasureCache): void { measureCacheStack.push(cache); @@ -103,17 +110,74 @@ function popLayoutCache(): void { layoutCacheStack.length > 0 ? (layoutCacheStack[layoutCacheStack.length - 1] ?? null) : null; } -function layoutCacheKey( +function forcedDimensionKey(value: number | null): number { + if (value === null) return NULL_FORCED_DIMENSION; + if (value < 0) { + throw new RangeError("layout: forced dimensions must be >= 0"); + } + return value; +} + +function getLayoutCacheHit( + axisMap: LayoutAxisCache, + maxW: number, + maxH: number, + forcedW: number | null, + forcedH: number | null, + x: number, + y: number, +): LayoutResult | null { + const byMaxH = axisMap.get(maxW); + if (!byMaxH) return null; + const byForcedW = byMaxH.get(maxH); + if (!byForcedW) return null; + const byForcedH = byForcedW.get(forcedDimensionKey(forcedW)); + if (!byForcedH) return null; + const byX = byForcedH.get(forcedDimensionKey(forcedH)); + if (!byX) return null; + const byY = byX.get(x); + if (!byY) return null; + return byY.get(y) ?? null; +} + +function setLayoutCacheValue( + axisMap: LayoutAxisCache, maxW: number, maxH: number, forcedW: number | null, forcedH: number | null, x: number, y: number, -): string { - return `${String(maxW)}:${String(maxH)}:${forcedW === null ? "n" : String(forcedW)}:${ - forcedH === null ? "n" : String(forcedH) - }:${String(x)}:${String(y)}`; + value: LayoutResult, +): void { + let byMaxH = axisMap.get(maxW); + if (!byMaxH) { + byMaxH = new Map(); + axisMap.set(maxW, byMaxH); + } + let byForcedW = byMaxH.get(maxH); + if (!byForcedW) { + byForcedW = new Map(); + byMaxH.set(maxH, byForcedW); + } + const forcedWKey = forcedDimensionKey(forcedW); + let byForcedH = byForcedW.get(forcedWKey); + if (!byForcedH) { + byForcedH = new Map(); + byForcedW.set(forcedWKey, byForcedH); + } + const forcedHKey = forcedDimensionKey(forcedH); + let byX = byForcedH.get(forcedHKey); + if (!byX) { + byX = new Map(); + byForcedH.set(forcedHKey, byX); + } + let byY = byX.get(x); + if (!byY) { + byY = new Map(); + byX.set(x, byY); + } + byY.set(y, value); } function getSyntheticThemedColumn(vnode: ThemedVNode): VNode { @@ -378,14 +442,13 @@ function layoutNode( } const cache = activeLayoutCache; - const cacheKey = layoutCacheKey(maxW, maxH, forcedW, forcedH, x, y); const dirtySet = getActiveDirtySet(); let cacheHit: LayoutResult | null = null; if (cache) { const entry = cache.get(vnode); if (entry) { const axisMap = axis === "row" ? entry.row : entry.column; - cacheHit = axisMap.get(cacheKey) ?? null; + cacheHit = getLayoutCacheHit(axisMap, maxW, maxH, forcedW, forcedH, x, y); if (cacheHit && (dirtySet === null || !dirtySet.has(vnode))) { if (__layoutProfile.enabled) __layoutProfile.layoutCacheHits++; return cacheHit; @@ -697,7 +760,7 @@ function layoutNode( cache.set(vnode, entry); } const axisMap = axis === "row" ? entry.row : entry.column; - axisMap.set(cacheKey, computed); + setLayoutCacheValue(axisMap, maxW, maxH, forcedW, forcedH, x, y, computed); } return computed; diff --git a/packages/core/src/layout/engine/pool.ts b/packages/core/src/layout/engine/pool.ts index 280830f5..ecf5b237 100644 --- a/packages/core/src/layout/engine/pool.ts +++ b/packages/core/src/layout/engine/pool.ts @@ -4,7 +4,7 @@ /** Pool of reusable number arrays for layout computation. */ const arrayPool: number[][] = []; -const MAX_POOL_SIZE = 8; +const MAX_POOL_SIZE = 32; /** * Get or create a number array of the specified length, zeroed. @@ -15,7 +15,12 @@ export function acquireArray(length: number): number[] { for (let i = 0; i < arrayPool.length; i++) { const arr = arrayPool[i]; if (arr !== undefined && arr.length >= length) { - arrayPool.splice(i, 1); + const lastIndex = arrayPool.length - 1; + if (i !== lastIndex) { + const last = arrayPool[lastIndex]; + if (last !== undefined) arrayPool[i] = last; + } + arrayPool.length = lastIndex; // Zero the portion we'll use arr.fill(0, 0, length); return arr; diff --git a/packages/core/src/layout/kinds/stack.ts b/packages/core/src/layout/kinds/stack.ts index 08711801..16abad6a 100644 --- a/packages/core/src/layout/kinds/stack.ts +++ b/packages/core/src/layout/kinds/stack.ts @@ -24,7 +24,7 @@ import { getConstraintProps, } from "../engine/guards.js"; import { measureMaxContent, measureMinContent } from "../engine/intrinsic.js"; -import { releaseArray } from "../engine/pool.js"; +import { acquireArray, releaseArray } from "../engine/pool.js"; import { ok } from "../engine/result.js"; import type { LayoutTree } from "../engine/types.js"; import { resolveResponsiveValue } from "../responsive.js"; @@ -470,208 +470,218 @@ function computeWrapConstraintLine( const gapTotal = lineChildCount <= 1 ? 0 : gap * (lineChildCount - 1); const availableForChildren = clampNonNegative(mainLimit - gapTotal); - const mainSizes = new Array(lineChildCount).fill(0); - const measureMaxMain = new Array(lineChildCount).fill(0); - const crossSizes = includeChildren ? new Array(lineChildCount).fill(0) : null; + const mainSizes = acquireArray(lineChildCount); + const measureMaxMain = acquireArray(lineChildCount); + const crossSizes = includeChildren ? acquireArray(lineChildCount) : null; + const crossPass1 = acquireArray(lineChildCount); - const flexItems: FlexItem[] = []; - let remaining = availableForChildren; + try { + const flexItems: FlexItem[] = []; + let remaining = availableForChildren; - for (let i = 0; i < lineChildCount; i++) { - const child = lineChildren[i]; - if (!child || childHasAbsolutePosition(child)) continue; + for (let i = 0; i < lineChildCount; i++) { + const child = lineChildren[i]; + if (!child || childHasAbsolutePosition(child)) continue; + + if (child.kind === "spacer") { + const sp = validateSpacerProps(child.props); + if (!sp.ok) return sp; + + const maxMain = availableForChildren; + if (remaining === 0) { + mainSizes[i] = 0; + measureMaxMain[i] = 0; + continue; + } + + if (sp.value.flex > 0) { + flexItems.push({ + index: i, + flex: sp.value.flex, + shrink: 0, + basis: 0, + min: sp.value.size, + max: maxMain, + }); + continue; + } - if (child.kind === "spacer") { - const sp = validateSpacerProps(child.props); - if (!sp.ok) return sp; + const size = Math.min(sp.value.size, remaining); + mainSizes[i] = size; + measureMaxMain[i] = size; + remaining = clampNonNegative(remaining - size); + continue; + } + + const childProps = getConstraintProps(child) ?? {}; + const resolved = resolveLayoutConstraints(childProps as never, parentRect, axis.axis); + + const fixedMain = resolved[axis.mainProp]; + const minMain = resolved[axis.minMainProp]; + const maxMain = Math.min( + toFiniteMax(resolved[axis.maxMainProp], availableForChildren), + availableForChildren, + ); + const flex = resolved.flex; + + const rawMain = (childProps as ConstraintPropBag)[axis.mainProp]; + const mainIsPercent = isPercentString(rawMain); - const maxMain = availableForChildren; if (remaining === 0) { mainSizes[i] = 0; measureMaxMain[i] = 0; continue; } - if (sp.value.flex > 0) { + if (fixedMain !== null) { + const desired = clampWithin(fixedMain, minMain, maxMain); + const size = Math.min(desired, remaining); + mainSizes[i] = size; + measureMaxMain[i] = mainIsPercent ? mainLimit : size; + remaining = clampNonNegative(remaining - size); + continue; + } + + if (flex > 0) { flexItems.push({ index: i, - flex: sp.value.flex, + flex, shrink: 0, basis: 0, - min: sp.value.size, + min: minMain, max: maxMain, }); continue; } - const size = Math.min(sp.value.size, remaining); - mainSizes[i] = size; - measureMaxMain[i] = size; - remaining = clampNonNegative(remaining - size); - continue; + const childRes = measureNodeOnAxis(axis, child, remaining, crossLimit, measureNode); + if (!childRes.ok) return childRes; + const childMain = mainFromSize(axis, childRes.value); + mainSizes[i] = childMain; + measureMaxMain[i] = childMain; + remaining = clampNonNegative(remaining - childMain); } - const childProps = getConstraintProps(child) ?? {}; - const resolved = resolveLayoutConstraints(childProps as never, parentRect, axis.axis); + if (flexItems.length > 0 && remaining > 0) { + const alloc = distributeFlex(remaining, flexItems); + for (let j = 0; j < flexItems.length; j++) { + const it = flexItems[j]; + if (!it) continue; + const size = alloc[j] ?? 0; + mainSizes[it.index] = size; + const child = lineChildren[it.index]; + if (child?.kind === "spacer") { + measureMaxMain[it.index] = size; + continue; + } + const childProps = getConstraintProps(child as VNode) ?? {}; + const rawMain = (childProps as ConstraintPropBag)[axis.mainProp]; + measureMaxMain[it.index] = isPercentString(rawMain) ? mainLimit : size; + } + releaseArray(alloc); + } - const fixedMain = resolved[axis.mainProp]; - const minMain = resolved[axis.minMainProp]; - const maxMain = Math.min( - toFiniteMax(resolved[axis.maxMainProp], availableForChildren), + maybeRebalanceNearFullPercentChildren( + axis, + lineChildren, + mainSizes, + measureMaxMain, availableForChildren, + parentRect, ); - const flex = resolved.flex; - - const rawMain = (childProps as ConstraintPropBag)[axis.mainProp]; - const mainIsPercent = isPercentString(rawMain); - - if (remaining === 0) { - mainSizes[i] = 0; - measureMaxMain[i] = 0; - continue; - } - - if (fixedMain !== null) { - const desired = clampWithin(fixedMain, minMain, maxMain); - const size = Math.min(desired, remaining); - mainSizes[i] = size; - measureMaxMain[i] = mainIsPercent ? mainLimit : size; - remaining = clampNonNegative(remaining - size); - continue; - } - if (flex > 0) { - flexItems.push({ - index: i, - flex, - shrink: 0, - basis: 0, - min: minMain, - max: maxMain, - }); - continue; + let lineMain = 0; + for (let i = 0; i < lineChildCount; i++) { + lineMain += mainSizes[i] ?? 0; } + lineMain += lineChildCount <= 1 ? 0 : gap * (lineChildCount - 1); - const childRes = measureNodeOnAxis(axis, child, remaining, crossLimit, measureNode); - if (!childRes.ok) return childRes; - const childMain = mainFromSize(axis, childRes.value); - mainSizes[i] = childMain; - measureMaxMain[i] = childMain; - remaining = clampNonNegative(remaining - childMain); - } + let lineCross = 0; + const sizeCache = new Array(lineChildCount).fill(null); + const mayFeedback = new Array(lineChildCount).fill(false); + let feedbackCandidate = false; - if (flexItems.length > 0 && remaining > 0) { - const alloc = distributeFlex(remaining, flexItems); - for (let j = 0; j < flexItems.length; j++) { - const it = flexItems[j]; - if (!it) continue; - const size = alloc[j] ?? 0; - mainSizes[it.index] = size; - const child = lineChildren[it.index]; - if (child?.kind === "spacer") { - measureMaxMain[it.index] = size; - continue; - } - const childProps = getConstraintProps(child as VNode) ?? {}; + for (let i = 0; i < lineChildCount; i++) { + const child = lineChildren[i]; + if (!child || childHasAbsolutePosition(child)) continue; + const main = mainSizes[i] ?? 0; + const mm = measureMaxMain[i] ?? 0; + const childSizeRes = + main === 0 + ? measureNodeOnAxis(axis, child, 0, 0, measureNode) + : measureNodeOnAxis(axis, child, mm, crossLimit, measureNode); + if (!childSizeRes.ok) return childSizeRes; + const childCross = crossFromSize(axis, childSizeRes.value); + if (crossSizes) crossSizes[i] = childCross; + sizeCache[i] = childSizeRes.value; + const childProps = getConstraintProps(child) ?? {}; const rawMain = (childProps as ConstraintPropBag)[axis.mainProp]; - measureMaxMain[it.index] = isPercentString(rawMain) ? mainLimit : size; + const needsFeedback = + main > 0 && + mm !== main && + !isPercentString(rawMain) && + childMayNeedCrossAxisFeedback(child); + mayFeedback[i] = needsFeedback; + crossPass1[i] = childCross; + if (needsFeedback) feedbackCandidate = true; + if (childCross > lineCross) lineCross = childCross; } - releaseArray(alloc); - } - maybeRebalanceNearFullPercentChildren( - axis, - lineChildren, - mainSizes, - measureMaxMain, - availableForChildren, - parentRect, - ); + if (feedbackCandidate) { + lineCross = 0; + for (let i = 0; i < lineChildCount; i++) { + const child = lineChildren[i]; + if (!child || childHasAbsolutePosition(child)) continue; - let lineMain = 0; - for (let i = 0; i < mainSizes.length; i++) { - lineMain += mainSizes[i] ?? 0; - } - lineMain += lineChildCount <= 1 ? 0 : gap * (lineChildCount - 1); + const needsFeedback = mayFeedback[i] === true; + let size = sizeCache[i] ?? null; + if (needsFeedback) { + const main = mainSizes[i] ?? 0; + const nextSizeRes = + main === 0 + ? measureNodeOnAxis(axis, child, 0, 0, measureNode) + : measureNodeOnAxis(axis, child, main, crossLimit, measureNode); + if (!nextSizeRes.ok) return nextSizeRes; + const nextCross = crossFromSize(axis, nextSizeRes.value); + if (nextCross !== (crossPass1[i] ?? 0)) { + size = nextSizeRes.value; + sizeCache[i] = size; + if (crossSizes) crossSizes[i] = nextCross; + crossPass1[i] = nextCross; + } + } - let lineCross = 0; - const sizeCache = new Array(lineChildCount).fill(null); - const mayFeedback = new Array(lineChildCount).fill(false); - const crossPass1 = new Array(lineChildCount).fill(0); - let feedbackCandidate = false; + const cross = + crossSizes?.[i] ?? crossPass1[i] ?? (size === null ? 0 : crossFromSize(axis, size)); + if (cross > lineCross) lineCross = cross; + } + } - for (let i = 0; i < lineChildCount; i++) { - const child = lineChildren[i]; - if (!child || childHasAbsolutePosition(child)) continue; - const main = mainSizes[i] ?? 0; - const mm = measureMaxMain[i] ?? 0; - const childSizeRes = - main === 0 - ? measureNodeOnAxis(axis, child, 0, 0, measureNode) - : measureNodeOnAxis(axis, child, mm, crossLimit, measureNode); - if (!childSizeRes.ok) return childSizeRes; - const childCross = crossFromSize(axis, childSizeRes.value); - if (crossSizes) crossSizes[i] = childCross; - sizeCache[i] = childSizeRes.value; - const childProps = getConstraintProps(child) ?? {}; - const rawMain = (childProps as ConstraintPropBag)[axis.mainProp]; - const needsFeedback = - main > 0 && mm !== main && !isPercentString(rawMain) && childMayNeedCrossAxisFeedback(child); - mayFeedback[i] = needsFeedback; - crossPass1[i] = childCross; - if (needsFeedback) feedbackCandidate = true; - if (childCross > lineCross) lineCross = childCross; - } + if (!includeChildren) return ok({ main: lineMain, cross: lineCross }); - if (feedbackCandidate) { - lineCross = 0; + const plannedChildren: WrapLineChildLayout[] = []; for (let i = 0; i < lineChildCount; i++) { const child = lineChildren[i]; if (!child || childHasAbsolutePosition(child)) continue; - - const needsFeedback = mayFeedback[i] === true; - let size = sizeCache[i] ?? null; - if (needsFeedback) { - const main = mainSizes[i] ?? 0; - const nextSizeRes = - main === 0 - ? measureNodeOnAxis(axis, child, 0, 0, measureNode) - : measureNodeOnAxis(axis, child, main, crossLimit, measureNode); - if (!nextSizeRes.ok) return nextSizeRes; - const nextCross = crossFromSize(axis, nextSizeRes.value); - if (nextCross !== (crossPass1[i] ?? 0)) { - size = nextSizeRes.value; - sizeCache[i] = size; - if (crossSizes) crossSizes[i] = nextCross; - crossPass1[i] = nextCross; - } - } - - const cross = - crossSizes?.[i] ?? crossPass1[i] ?? (size === null ? 0 : crossFromSize(axis, size)); - if (cross > lineCross) lineCross = cross; + plannedChildren.push({ + child, + main: mainSizes[i] ?? 0, + measureMaxMain: measureMaxMain[i] ?? 0, + cross: crossSizes?.[i] ?? 0, + }); } - } - if (!includeChildren) return ok({ main: lineMain, cross: lineCross }); - - const plannedChildren: WrapLineChildLayout[] = []; - for (let i = 0; i < lineChildCount; i++) { - const child = lineChildren[i]; - if (!child || childHasAbsolutePosition(child)) continue; - plannedChildren.push({ - child, - main: mainSizes[i] ?? 0, - measureMaxMain: measureMaxMain[i] ?? 0, - cross: crossSizes?.[i] ?? 0, + return ok({ + children: Object.freeze(plannedChildren), + main: lineMain, + cross: lineCross, }); + } finally { + releaseArray(mainSizes); + releaseArray(measureMaxMain); + if (crossSizes) releaseArray(crossSizes); + releaseArray(crossPass1); } - - return ok({ - children: Object.freeze(plannedChildren), - main: lineMain, - cross: lineCross, - }); } function measureWrapConstraintLine( @@ -889,183 +899,191 @@ function planConstraintMainSizes( } // Advanced path: supports flexShrink/flexBasis while keeping legacy defaults. - const minMains = new Array(children.length).fill(0); - const maxMains = new Array(children.length).fill(availableForChildren); - const shrinkFactors = new Array(children.length).fill(0); + const minMains = acquireArray(children.length); + const maxMains = acquireArray(children.length); + maxMains.fill(availableForChildren, 0, children.length); + const shrinkFactors = acquireArray(children.length); - const growItems: FlexItem[] = []; - let totalMain = 0; + try { + const growItems: FlexItem[] = []; + let totalMain = 0; - for (let i = 0; i < children.length; i++) { - const child = children[i]; - if (!child || childHasAbsolutePosition(child)) continue; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (!child || childHasAbsolutePosition(child)) continue; + + if (child.kind === "spacer") { + const sp = validateSpacerProps(child.props); + if (!sp.ok) return sp; + + const basis = sp.value.flex > 0 ? 0 : sp.value.size; + mainSizes[i] = basis; + measureMaxMain[i] = basis; + minMains[i] = 0; + maxMains[i] = availableForChildren; + shrinkFactors[i] = 0; + totalMain += basis; + + if (sp.value.flex > 0) { + growItems.push({ + index: i, + flex: sp.value.flex, + shrink: 0, + basis: 0, + min: sp.value.size, + max: availableForChildren, + }); + } + continue; + } - if (child.kind === "spacer") { - const sp = validateSpacerProps(child.props); - if (!sp.ok) return sp; + const childProps = (getConstraintProps(child) ?? {}) as Record & FlexPropBag; + const resolved = resolveLayoutConstraints(childProps as never, parentRect, axis.axis); + + const fixedMain = resolved[axis.mainProp]; + const maxMain = Math.min( + toFiniteMax(resolved[axis.maxMainProp], availableForChildren), + availableForChildren, + ); + let minMain = Math.min(resolved[axis.minMainProp], availableForChildren); + + const rawMain = childProps[axis.mainProp]; + const rawMinMain = childProps[axis.minMainProp]; + const rawFlexBasis = childProps.flexBasis; + const mainPercent = isPercentString(rawMain); + const flexBasisIsAuto = resolveResponsiveValue(rawFlexBasis) === "auto"; + + if (rawMinMain === undefined && resolved.flexShrink > 0) { + const intrinsicMinRes = measureMinContent(child, axis.axis, measureNode); + if (!intrinsicMinRes.ok) return intrinsicMinRes; + const intrinsicMain = mainFromSize(axis, intrinsicMinRes.value); + minMain = Math.max(minMain, Math.min(intrinsicMain, availableForChildren)); + } + + const normalizedMinMain = Math.min(minMain, maxMain); + minMains[i] = normalizedMinMain; + maxMains[i] = maxMain; + shrinkFactors[i] = resolved.flexShrink; + + let measuredSize: Size | null = null; + let basis: number; + if (fixedMain !== null) { + basis = clampWithin(fixedMain, normalizedMinMain, maxMain); + } else if (resolved.flexBasis !== null) { + basis = clampWithin(resolved.flexBasis, normalizedMinMain, maxMain); + } else if (flexBasisIsAuto) { + const intrinsicMaxRes = measureMaxContent(child, axis.axis, measureNode); + if (!intrinsicMaxRes.ok) return intrinsicMaxRes; + const intrinsicMain = mainFromSize(axis, intrinsicMaxRes.value); + basis = clampWithin(intrinsicMain, normalizedMinMain, maxMain); + } else if (resolved.flex > 0) { + basis = 0; + } else { + const childRes = measureNodeOnAxis( + axis, + child, + availableForChildren, + crossLimit, + measureNode, + ); + if (!childRes.ok) return childRes; + measuredSize = childRes.value; + basis = clampWithin(mainFromSize(axis, childRes.value), normalizedMinMain, maxMain); + } - const basis = sp.value.flex > 0 ? 0 : sp.value.size; mainSizes[i] = basis; - measureMaxMain[i] = basis; - minMains[i] = 0; - maxMains[i] = availableForChildren; - shrinkFactors[i] = 0; + measureMaxMain[i] = mainPercent ? mainLimit : basis; + if (collectPrecomputed && measuredSize !== null) precomputedSizes[i] = measuredSize; totalMain += basis; - if (sp.value.flex > 0) { + if (fixedMain === null && resolved.flex > 0) { + const growMin = Math.max(0, normalizedMinMain - basis); + const growCap = Math.max(0, maxMain - basis); growItems.push({ index: i, - flex: sp.value.flex, + flex: resolved.flex, shrink: 0, basis: 0, - min: sp.value.size, - max: availableForChildren, + min: growMin, + max: growCap, }); } - continue; } - const childProps = (getConstraintProps(child) ?? {}) as Record & FlexPropBag; - const resolved = resolveLayoutConstraints(childProps as never, parentRect, axis.axis); - - const fixedMain = resolved[axis.mainProp]; - const maxMain = Math.min( - toFiniteMax(resolved[axis.maxMainProp], availableForChildren), - availableForChildren, - ); - let minMain = Math.min(resolved[axis.minMainProp], availableForChildren); - - const rawMain = childProps[axis.mainProp]; - const rawMinMain = childProps[axis.minMainProp]; - const rawFlexBasis = childProps.flexBasis; - const mainPercent = isPercentString(rawMain); - const flexBasisIsAuto = resolveResponsiveValue(rawFlexBasis) === "auto"; - - if (rawMinMain === undefined && resolved.flexShrink > 0) { - const intrinsicMinRes = measureMinContent(child, axis.axis, measureNode); - if (!intrinsicMinRes.ok) return intrinsicMinRes; - const intrinsicMain = mainFromSize(axis, intrinsicMinRes.value); - minMain = Math.max(minMain, Math.min(intrinsicMain, availableForChildren)); - } - - const normalizedMinMain = Math.min(minMain, maxMain); - minMains[i] = normalizedMinMain; - maxMains[i] = maxMain; - shrinkFactors[i] = resolved.flexShrink; - - let measuredSize: Size | null = null; - let basis: number; - if (fixedMain !== null) { - basis = clampWithin(fixedMain, normalizedMinMain, maxMain); - } else if (resolved.flexBasis !== null) { - basis = clampWithin(resolved.flexBasis, normalizedMinMain, maxMain); - } else if (flexBasisIsAuto) { - const intrinsicMaxRes = measureMaxContent(child, axis.axis, measureNode); - if (!intrinsicMaxRes.ok) return intrinsicMaxRes; - const intrinsicMain = mainFromSize(axis, intrinsicMaxRes.value); - basis = clampWithin(intrinsicMain, normalizedMinMain, maxMain); - } else if (resolved.flex > 0) { - basis = 0; - } else { - const childRes = measureNodeOnAxis( - axis, - child, - availableForChildren, - crossLimit, - measureNode, - ); - if (!childRes.ok) return childRes; - measuredSize = childRes.value; - basis = clampWithin(mainFromSize(axis, childRes.value), normalizedMinMain, maxMain); + let didResize = false; + const growRemaining = availableForChildren - totalMain; + if (growItems.length > 0 && growRemaining > 0) { + const alloc = distributeFlex(growRemaining, growItems); + for (let i = 0; i < growItems.length; i++) { + const item = growItems[i]; + if (!item) continue; + const add = alloc[i] ?? 0; + if (add <= 0) continue; + const current = mainSizes[item.index] ?? 0; + const next = Math.min(maxMains[item.index] ?? availableForChildren, current + add); + if (next !== current) didResize = true; + mainSizes[item.index] = next; + } + releaseArray(alloc); } - mainSizes[i] = basis; - measureMaxMain[i] = mainPercent ? mainLimit : basis; - if (collectPrecomputed && measuredSize !== null) precomputedSizes[i] = measuredSize; - totalMain += basis; - - if (fixedMain === null && resolved.flex > 0) { - const growMin = Math.max(0, normalizedMinMain - basis); - const growCap = Math.max(0, maxMain - basis); - growItems.push({ - index: i, - flex: resolved.flex, - shrink: 0, - basis: 0, - min: growMin, - max: growCap, - }); + totalMain = 0; + for (let i = 0; i < mainSizes.length; i++) { + totalMain += mainSizes[i] ?? 0; } - } - let didResize = false; - const growRemaining = availableForChildren - totalMain; - if (growItems.length > 0 && growRemaining > 0) { - const alloc = distributeFlex(growRemaining, growItems); - for (let i = 0; i < growItems.length; i++) { - const item = growItems[i]; - if (!item) continue; - const add = alloc[i] ?? 0; - if (add <= 0) continue; - const current = mainSizes[item.index] ?? 0; - const next = Math.min(maxMains[item.index] ?? availableForChildren, current + add); - if (next !== current) didResize = true; - mainSizes[item.index] = next; + if (totalMain > availableForChildren) { + const shrinkItems: FlexItem[] = []; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (!child || childHasAbsolutePosition(child)) continue; + shrinkItems.push({ + index: i, + flex: 0, + shrink: shrinkFactors[i] ?? 0, + basis: mainSizes[i] ?? 0, + min: minMains[i] ?? 0, + max: maxMains[i] ?? availableForChildren, + }); + } + if (shrinkItems.length > 0) { + const shrunk = shrinkFlex(availableForChildren, shrinkItems); + for (let i = 0; i < shrinkItems.length; i++) { + const item = shrinkItems[i]; + if (!item) continue; + const current = mainSizes[item.index] ?? 0; + const next = clampWithin(shrunk[i] ?? 0, item.min, item.max); + if (next !== current) didResize = true; + mainSizes[item.index] = next; + } + releaseArray(shrunk); + } } - releaseArray(alloc); - } - - totalMain = 0; - for (let i = 0; i < mainSizes.length; i++) { - totalMain += mainSizes[i] ?? 0; - } - if (totalMain > availableForChildren) { - const shrinkItems: FlexItem[] = []; for (let i = 0; i < children.length; i++) { - const child = children[i]; - if (!child || childHasAbsolutePosition(child)) continue; - shrinkItems.push({ - index: i, - flex: 0, - shrink: shrinkFactors[i] ?? 0, - basis: mainSizes[i] ?? 0, - min: minMains[i] ?? 0, - max: maxMains[i] ?? availableForChildren, - }); + if (!children[i]) continue; + if (didResize && collectPrecomputed) precomputedSizes[i] = null; } - if (shrinkItems.length > 0) { - const shrunk = shrinkFlex(availableForChildren, shrinkItems); - for (let i = 0; i < shrinkItems.length; i++) { - const item = shrinkItems[i]; - if (!item) continue; - const current = mainSizes[item.index] ?? 0; - const next = clampWithin(shrunk[i] ?? 0, item.min, item.max); - if (next !== current) didResize = true; - mainSizes[item.index] = next; - } - } - } - for (let i = 0; i < children.length; i++) { - if (!children[i]) continue; - if (didResize && collectPrecomputed) precomputedSizes[i] = null; - } - - maybeRebalanceNearFullPercentChildren( - axis, - children, - mainSizes, - measureMaxMain, - availableForChildren, - parentRect, - ); + maybeRebalanceNearFullPercentChildren( + axis, + children, + mainSizes, + measureMaxMain, + availableForChildren, + parentRect, + ); - return ok({ - mainSizes, - measureMaxMain, - precomputedSizes, - }); + return ok({ + mainSizes, + measureMaxMain, + precomputedSizes, + }); + } finally { + releaseArray(minMains); + releaseArray(maxMains); + releaseArray(shrinkFactors); + } } function planConstraintCrossSizes( @@ -1683,82 +1701,87 @@ function layoutStack( } } } else if (!needsConstraintPass) { - const mainSizes = new Array(count).fill(0); - const crossSizes = new Array(count).fill(0); + const mainSizes = acquireArray(count); + const crossSizes = acquireArray(count); - let rem = mainLimit; - for (let i = 0; i < count; i++) { - const child = vnode.children[i]; - if (!child || childHasAbsolutePosition(child)) continue; - if (rem === 0) continue; + try { + let rem = mainLimit; + for (let i = 0; i < count; i++) { + const child = vnode.children[i]; + if (!child || childHasAbsolutePosition(child)) continue; + if (rem === 0) continue; - const childSizeRes = measureNodeOnAxis(axis, child, rem, crossLimit, measureNode); - if (!childSizeRes.ok) return childSizeRes; - mainSizes[i] = mainFromSize(axis, childSizeRes.value); - crossSizes[i] = crossFromSize(axis, childSizeRes.value); - rem = clampNonNegative(rem - (mainSizes[i] ?? 0) - gap); - } + const childSizeRes = measureNodeOnAxis(axis, child, rem, crossLimit, measureNode); + if (!childSizeRes.ok) return childSizeRes; + mainSizes[i] = mainFromSize(axis, childSizeRes.value); + crossSizes[i] = crossFromSize(axis, childSizeRes.value); + rem = clampNonNegative(rem - (mainSizes[i] ?? 0) - gap); + } - let usedMain = 0; - for (let i = 0; i < mainSizes.length; i++) { - usedMain += mainSizes[i] ?? 0; - } - usedMain += childCount <= 1 ? 0 : gap * (childCount - 1); - const extra = clampNonNegative(mainLimit - usedMain); - const startOffset = computeJustifyStartOffset(justify, extra, childCount); + let usedMain = 0; + for (let i = 0; i < count; i++) { + usedMain += mainSizes[i] ?? 0; + } + usedMain += childCount <= 1 ? 0 : gap * (childCount - 1); + const extra = clampNonNegative(mainLimit - usedMain); + const startOffset = computeJustifyStartOffset(justify, extra, childCount); - let cursorMain = mainOrigin + startOffset; - let remainingMain = clampNonNegative(mainLimit - startOffset); - let childOrdinal = 0; + let cursorMain = mainOrigin + startOffset; + let remainingMain = clampNonNegative(mainLimit - startOffset); + let childOrdinal = 0; - for (let i = 0; i < count; i++) { - const child = vnode.children[i]; - if (!child || childHasAbsolutePosition(child)) continue; + for (let i = 0; i < count; i++) { + const child = vnode.children[i]; + if (!child || childHasAbsolutePosition(child)) continue; - if (remainingMain === 0) { - const childRes = layoutNodeOnAxis(axis, child, cursorMain, crossOrigin, 0, 0, layoutNode); + if (remainingMain === 0) { + const childRes = layoutNodeOnAxis(axis, child, cursorMain, crossOrigin, 0, 0, layoutNode); + if (!childRes.ok) return childRes; + children.push(childRes.value); + childOrdinal++; + continue; + } + + const childMain = mainSizes[i] ?? 0; + const childCross = crossSizes[i] ?? 0; + + let childCrossPos = crossOrigin; + let forceCross: number | null = null; + const effectiveAlign = resolveEffectiveAlign(child, align); + if (effectiveAlign === "center") { + childCrossPos = crossOrigin + Math.floor((crossLimit - childCross) / 2); + } else if (effectiveAlign === "end") { + childCrossPos = crossOrigin + (crossLimit - childCross); + } else if (effectiveAlign === "stretch") { + forceCross = crossLimit; + } + + const childRes = layoutNodeOnAxis( + axis, + child, + cursorMain, + childCrossPos, + remainingMain, + crossLimit, + layoutNode, + null, + forceCross, + ); if (!childRes.ok) return childRes; children.push(childRes.value); - childOrdinal++; - continue; - } - const childMain = mainSizes[i] ?? 0; - const childCross = crossSizes[i] ?? 0; - - let childCrossPos = crossOrigin; - let forceCross: number | null = null; - const effectiveAlign = resolveEffectiveAlign(child, align); - if (effectiveAlign === "center") { - childCrossPos = crossOrigin + Math.floor((crossLimit - childCross) / 2); - } else if (effectiveAlign === "end") { - childCrossPos = crossOrigin + (crossLimit - childCross); - } else if (effectiveAlign === "stretch") { - forceCross = crossLimit; + const hasNextChild = childOrdinal < childCount - 1; + const extraGap = hasNextChild + ? computeJustifyExtraGap(justify, extra, childCount, childOrdinal) + : 0; + const step = childMain + (hasNextChild ? gap + extraGap : 0); + cursorMain = cursorMain + step; + remainingMain = clampNonNegative(remainingMain - step); + childOrdinal++; } - - const childRes = layoutNodeOnAxis( - axis, - child, - cursorMain, - childCrossPos, - remainingMain, - crossLimit, - layoutNode, - null, - forceCross, - ); - if (!childRes.ok) return childRes; - children.push(childRes.value); - - const hasNextChild = childOrdinal < childCount - 1; - const extraGap = hasNextChild - ? computeJustifyExtraGap(justify, extra, childCount, childOrdinal) - : 0; - const step = childMain + (hasNextChild ? gap + extraGap : 0); - cursorMain = cursorMain + step; - remainingMain = clampNonNegative(remainingMain - step); - childOrdinal++; + } finally { + releaseArray(mainSizes); + releaseArray(crossSizes); } } else { const parentRect: Rect = { x: 0, y: 0, w: cw, h: ch }; diff --git a/packages/core/src/renderer/renderToDrawlist/renderPackets.ts b/packages/core/src/renderer/renderToDrawlist/renderPackets.ts index ec9eb32f..a08cf754 100644 --- a/packages/core/src/renderer/renderToDrawlist/renderPackets.ts +++ b/packages/core/src/renderer/renderToDrawlist/renderPackets.ts @@ -259,7 +259,6 @@ function hashTextProps(hash: number, props: Readonly>): dim?: unknown; textOverflow?: unknown; }>; - const style = textProps.style; const maxWidth = textProps.maxWidth; const wrap = textProps.wrap; @@ -378,8 +377,8 @@ export function computeRenderPacketKey( } export class RenderPacketRecorder implements DrawlistBuilder { - private readonly ops: RenderPacketOp[] = []; - private readonly resources: Uint8Array[] = []; + private ops: RenderPacketOp[] = []; + private resources: Uint8Array[] = []; private readonly blobResourceById = new Map(); private readonly textRunByBlobId = new Map(); private valid = true; @@ -392,9 +391,15 @@ export class RenderPacketRecorder implements DrawlistBuilder { buildPacket(): RenderPacket | null { if (!this.valid) return null; + const ops = this.ops; + const resources = this.resources; + this.ops = []; + this.resources = []; + this.blobResourceById.clear(); + this.textRunByBlobId.clear(); return Object.freeze({ - ops: Object.freeze(this.ops.slice()), - resources: Object.freeze(this.resources.slice()), + ops: Object.freeze(ops), + resources: Object.freeze(resources), }); } @@ -428,8 +433,11 @@ export class RenderPacketRecorder implements DrawlistBuilder { style?: Parameters[4], ): void { this.target.fillRect(x, y, w, h, style); - const local = { op: "FILL_RECT", x: this.localX(x), y: this.localY(y), w, h } as const; - this.ops.push(style === undefined ? local : { ...local, style }); + if (style === undefined) { + this.ops.push({ op: "FILL_RECT", x: this.localX(x), y: this.localY(y), w, h }); + return; + } + this.ops.push({ op: "FILL_RECT", x: this.localX(x), y: this.localY(y), w, h, style }); } blitRect(srcX: number, srcY: number, w: number, h: number, dstX: number, dstY: number): void { @@ -444,13 +452,22 @@ export class RenderPacketRecorder implements DrawlistBuilder { style?: Parameters[3], ): void { this.target.drawText(x, y, text, style); - const local = { + if (style === undefined) { + this.ops.push({ + op: "DRAW_TEXT_SLICE", + x: this.localX(x), + y: this.localY(y), + text, + }); + return; + } + this.ops.push({ op: "DRAW_TEXT_SLICE", x: this.localX(x), y: this.localY(y), text, - } as const; - this.ops.push(style === undefined ? local : { ...local, style }); + style, + }); } pushClip(x: number, y: number, w: number, h: number): void { @@ -522,7 +539,17 @@ export class RenderPacketRecorder implements DrawlistBuilder { this.invalidatePacket(); return; } - this.ops.push({ + const op: { + op: "DRAW_CANVAS"; + x: number; + y: number; + w: number; + h: number; + resourceId: number; + blitter: Parameters[5]; + pxWidth?: number; + pxHeight?: number; + } = { op: "DRAW_CANVAS", x: this.localX(x), y: this.localY(y), @@ -530,9 +557,10 @@ export class RenderPacketRecorder implements DrawlistBuilder { h, resourceId, blitter, - ...(pxWidth !== undefined ? { pxWidth } : {}), - ...(pxHeight !== undefined ? { pxHeight } : {}), - }); + }; + if (pxWidth !== undefined) op.pxWidth = pxWidth; + if (pxHeight !== undefined) op.pxHeight = pxHeight; + this.ops.push(op); } drawImage(...args: Parameters): void { @@ -543,7 +571,21 @@ export class RenderPacketRecorder implements DrawlistBuilder { this.invalidatePacket(); return; } - this.ops.push({ + const op: { + op: "DRAW_IMAGE"; + x: number; + y: number; + w: number; + h: number; + resourceId: number; + format: Parameters[5]; + protocol: Parameters[6]; + zLayer: Parameters[7]; + fit: Parameters[8]; + imageId: Parameters[9]; + pxWidth?: number; + pxHeight?: number; + } = { op: "DRAW_IMAGE", x: this.localX(x), y: this.localY(y), @@ -555,9 +597,10 @@ export class RenderPacketRecorder implements DrawlistBuilder { zLayer, fit, imageId, - ...(pxWidth !== undefined ? { pxWidth } : {}), - ...(pxHeight !== undefined ? { pxHeight } : {}), - }); + }; + if (pxWidth !== undefined) op.pxWidth = pxWidth; + if (pxHeight !== undefined) op.pxHeight = pxHeight; + this.ops.push(op); } buildInto(dst: Uint8Array): DrawlistBuildResult { @@ -584,10 +627,13 @@ export function emitRenderPacket( originX: number, originY: number, ): void { - const blobByResourceId: (number | null)[] = new Array(packet.resources.length); - for (let i = 0; i < packet.resources.length; i++) { - const resource = packet.resources[i]; - blobByResourceId[i] = resource ? builder.addBlob(resource) : null; + let blobByResourceId: (number | null)[] | null = null; + if (packet.resources.length > 0) { + blobByResourceId = new Array(packet.resources.length); + for (let i = 0; i < packet.resources.length; i++) { + const resource = packet.resources[i]; + blobByResourceId[i] = resource ? builder.addBlob(resource) : null; + } } for (const op of packet.ops) { @@ -619,7 +665,7 @@ export function emitRenderPacket( builder.popClip(); break; case "DRAW_CANVAS": { - const blobId = blobByResourceId[op.resourceId]; + const blobId = blobByResourceId?.[op.resourceId]; if (blobId === null || blobId === undefined) break; builder.drawCanvas( originX + op.x, @@ -634,7 +680,7 @@ export function emitRenderPacket( break; } case "DRAW_IMAGE": { - const blobId = blobByResourceId[op.resourceId]; + const blobId = blobByResourceId?.[op.resourceId]; if (blobId === null || blobId === undefined) break; builder.drawImage( originX + op.x, diff --git a/packages/core/src/renderer/renderToDrawlist/renderTree.ts b/packages/core/src/renderer/renderToDrawlist/renderTree.ts index a021fbcb..66653526 100644 --- a/packages/core/src/renderer/renderToDrawlist/renderTree.ts +++ b/packages/core/src/renderer/renderToDrawlist/renderTree.ts @@ -151,7 +151,10 @@ export function renderTree( let renderTheme = currentTheme; if (vnode.kind === "themed") { const props = vnode.props as { theme?: unknown }; - renderTheme = mergeThemeOverride(currentTheme, props.theme); + const themeOverride = props.theme; + if (themeOverride !== undefined) { + renderTheme = mergeThemeOverride(currentTheme, themeOverride); + } } else if ( vnode.kind === "row" || vnode.kind === "column" || @@ -159,7 +162,10 @@ export function renderTree( vnode.kind === "box" ) { const props = vnode.props as { theme?: unknown }; - renderTheme = mergeThemeOverride(currentTheme, props.theme); + const themeOverride = props.theme; + if (themeOverride !== undefined) { + renderTheme = mergeThemeOverride(currentTheme, themeOverride); + } } const nodeStackLenBeforePush = nodeStack.length;