mirror of
http://10.0.2.1:3031/sauer/claude-code.git
synced 2026-06-30 15:46:58 +10:00
2579 lines
81 KiB
TypeScript
2579 lines
81 KiB
TypeScript
|
|
/**
|
|||
|
|
* Pure-TypeScript port of yoga-layout (Meta's flexbox engine).
|
|||
|
|
*
|
|||
|
|
* This matches the `yoga-layout/load` API surface used by src/ink/layout/yoga.ts.
|
|||
|
|
* The upstream C++ source is ~2500 lines in CalculateLayout.cpp alone; this port
|
|||
|
|
* is a simplified single-pass flexbox implementation that covers the subset of
|
|||
|
|
* features Ink actually uses:
|
|||
|
|
* - flex-direction (row/column + reverse)
|
|||
|
|
* - flex-grow / flex-shrink / flex-basis
|
|||
|
|
* - align-items / align-self (stretch, flex-start, center, flex-end)
|
|||
|
|
* - justify-content (all six values)
|
|||
|
|
* - margin / padding / border / gap
|
|||
|
|
* - width / height / min / max (point, percent, auto)
|
|||
|
|
* - position: relative / absolute
|
|||
|
|
* - display: flex / none
|
|||
|
|
* - measure functions (for text nodes)
|
|||
|
|
*
|
|||
|
|
* Also implemented for spec parity (not used by Ink):
|
|||
|
|
* - margin: auto (main + cross axis, overrides justify/align)
|
|||
|
|
* - multi-pass flex clamping when children hit min/max constraints
|
|||
|
|
* - flex-grow/shrink against container min/max when size is indefinite
|
|||
|
|
*
|
|||
|
|
* Also implemented for spec parity (not used by Ink):
|
|||
|
|
* - flex-wrap: wrap / wrap-reverse (multi-line flex)
|
|||
|
|
* - align-content (positions wrapped lines on cross axis)
|
|||
|
|
*
|
|||
|
|
* Also implemented for spec parity (not used by Ink):
|
|||
|
|
* - display: contents (children lifted to grandparent, box removed)
|
|||
|
|
*
|
|||
|
|
* Also implemented for spec parity (not used by Ink):
|
|||
|
|
* - baseline alignment (align-items/align-self: baseline)
|
|||
|
|
*
|
|||
|
|
* Not implemented (not used by Ink):
|
|||
|
|
* - aspect-ratio
|
|||
|
|
* - box-sizing: content-box
|
|||
|
|
* - RTL direction (Ink always passes Direction.LTR)
|
|||
|
|
*
|
|||
|
|
* Upstream: https://github.com/facebook/yoga
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import {
|
|||
|
|
Align,
|
|||
|
|
BoxSizing,
|
|||
|
|
Dimension,
|
|||
|
|
Direction,
|
|||
|
|
Display,
|
|||
|
|
Edge,
|
|||
|
|
Errata,
|
|||
|
|
ExperimentalFeature,
|
|||
|
|
FlexDirection,
|
|||
|
|
Gutter,
|
|||
|
|
Justify,
|
|||
|
|
MeasureMode,
|
|||
|
|
Overflow,
|
|||
|
|
PositionType,
|
|||
|
|
Unit,
|
|||
|
|
Wrap,
|
|||
|
|
} from './enums.js'
|
|||
|
|
|
|||
|
|
export {
|
|||
|
|
Align,
|
|||
|
|
BoxSizing,
|
|||
|
|
Dimension,
|
|||
|
|
Direction,
|
|||
|
|
Display,
|
|||
|
|
Edge,
|
|||
|
|
Errata,
|
|||
|
|
ExperimentalFeature,
|
|||
|
|
FlexDirection,
|
|||
|
|
Gutter,
|
|||
|
|
Justify,
|
|||
|
|
MeasureMode,
|
|||
|
|
Overflow,
|
|||
|
|
PositionType,
|
|||
|
|
Unit,
|
|||
|
|
Wrap,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --
|
|||
|
|
// Value types
|
|||
|
|
|
|||
|
|
export type Value = {
|
|||
|
|
unit: Unit
|
|||
|
|
value: number
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const UNDEFINED_VALUE: Value = { unit: Unit.Undefined, value: NaN }
|
|||
|
|
const AUTO_VALUE: Value = { unit: Unit.Auto, value: NaN }
|
|||
|
|
|
|||
|
|
function pointValue(v: number): Value {
|
|||
|
|
return { unit: Unit.Point, value: v }
|
|||
|
|
}
|
|||
|
|
function percentValue(v: number): Value {
|
|||
|
|
return { unit: Unit.Percent, value: v }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resolveValue(v: Value, ownerSize: number): number {
|
|||
|
|
switch (v.unit) {
|
|||
|
|
case Unit.Point:
|
|||
|
|
return v.value
|
|||
|
|
case Unit.Percent:
|
|||
|
|
return isNaN(ownerSize) ? NaN : (v.value * ownerSize) / 100
|
|||
|
|
default:
|
|||
|
|
return NaN
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function isDefined(n: number): boolean {
|
|||
|
|
return !isNaN(n)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NaN-safe equality for layout-cache input comparison
|
|||
|
|
function sameFloat(a: number, b: number): boolean {
|
|||
|
|
return a === b || (a !== a && b !== b)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --
|
|||
|
|
// Layout result (computed values)
|
|||
|
|
|
|||
|
|
type Layout = {
|
|||
|
|
left: number
|
|||
|
|
top: number
|
|||
|
|
width: number
|
|||
|
|
height: number
|
|||
|
|
// Computed per-edge values (resolved to physical edges)
|
|||
|
|
border: [number, number, number, number] // left, top, right, bottom
|
|||
|
|
padding: [number, number, number, number]
|
|||
|
|
margin: [number, number, number, number]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --
|
|||
|
|
// Style (input values)
|
|||
|
|
|
|||
|
|
type Style = {
|
|||
|
|
direction: Direction
|
|||
|
|
flexDirection: FlexDirection
|
|||
|
|
justifyContent: Justify
|
|||
|
|
alignItems: Align
|
|||
|
|
alignSelf: Align
|
|||
|
|
alignContent: Align
|
|||
|
|
flexWrap: Wrap
|
|||
|
|
overflow: Overflow
|
|||
|
|
display: Display
|
|||
|
|
positionType: PositionType
|
|||
|
|
|
|||
|
|
flexGrow: number
|
|||
|
|
flexShrink: number
|
|||
|
|
flexBasis: Value
|
|||
|
|
|
|||
|
|
// 9-edge arrays indexed by Edge enum
|
|||
|
|
margin: Value[]
|
|||
|
|
padding: Value[]
|
|||
|
|
border: Value[]
|
|||
|
|
position: Value[]
|
|||
|
|
|
|||
|
|
// 3-gutter array indexed by Gutter enum
|
|||
|
|
gap: Value[]
|
|||
|
|
|
|||
|
|
width: Value
|
|||
|
|
height: Value
|
|||
|
|
minWidth: Value
|
|||
|
|
minHeight: Value
|
|||
|
|
maxWidth: Value
|
|||
|
|
maxHeight: Value
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function defaultStyle(): Style {
|
|||
|
|
return {
|
|||
|
|
direction: Direction.Inherit,
|
|||
|
|
flexDirection: FlexDirection.Column,
|
|||
|
|
justifyContent: Justify.FlexStart,
|
|||
|
|
alignItems: Align.Stretch,
|
|||
|
|
alignSelf: Align.Auto,
|
|||
|
|
alignContent: Align.FlexStart,
|
|||
|
|
flexWrap: Wrap.NoWrap,
|
|||
|
|
overflow: Overflow.Visible,
|
|||
|
|
display: Display.Flex,
|
|||
|
|
positionType: PositionType.Relative,
|
|||
|
|
flexGrow: 0,
|
|||
|
|
flexShrink: 0,
|
|||
|
|
flexBasis: AUTO_VALUE,
|
|||
|
|
margin: new Array(9).fill(UNDEFINED_VALUE),
|
|||
|
|
padding: new Array(9).fill(UNDEFINED_VALUE),
|
|||
|
|
border: new Array(9).fill(UNDEFINED_VALUE),
|
|||
|
|
position: new Array(9).fill(UNDEFINED_VALUE),
|
|||
|
|
gap: new Array(3).fill(UNDEFINED_VALUE),
|
|||
|
|
width: AUTO_VALUE,
|
|||
|
|
height: AUTO_VALUE,
|
|||
|
|
minWidth: UNDEFINED_VALUE,
|
|||
|
|
minHeight: UNDEFINED_VALUE,
|
|||
|
|
maxWidth: UNDEFINED_VALUE,
|
|||
|
|
maxHeight: UNDEFINED_VALUE,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --
|
|||
|
|
// Edge resolution — yoga's 9-edge model collapsed to 4 physical edges
|
|||
|
|
|
|||
|
|
const EDGE_LEFT = 0
|
|||
|
|
const EDGE_TOP = 1
|
|||
|
|
const EDGE_RIGHT = 2
|
|||
|
|
const EDGE_BOTTOM = 3
|
|||
|
|
|
|||
|
|
function resolveEdge(
|
|||
|
|
edges: Value[],
|
|||
|
|
physicalEdge: number,
|
|||
|
|
ownerSize: number,
|
|||
|
|
// For margin/position we allow auto; for padding/border auto resolves to 0
|
|||
|
|
allowAuto = false,
|
|||
|
|
): number {
|
|||
|
|
// Precedence: specific edge > horizontal/vertical > all
|
|||
|
|
let v = edges[physicalEdge]!
|
|||
|
|
if (v.unit === Unit.Undefined) {
|
|||
|
|
if (physicalEdge === EDGE_LEFT || physicalEdge === EDGE_RIGHT) {
|
|||
|
|
v = edges[Edge.Horizontal]!
|
|||
|
|
} else {
|
|||
|
|
v = edges[Edge.Vertical]!
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (v.unit === Unit.Undefined) {
|
|||
|
|
v = edges[Edge.All]!
|
|||
|
|
}
|
|||
|
|
// Start/End map to Left/Right for LTR (Ink is always LTR)
|
|||
|
|
if (v.unit === Unit.Undefined) {
|
|||
|
|
if (physicalEdge === EDGE_LEFT) v = edges[Edge.Start]!
|
|||
|
|
if (physicalEdge === EDGE_RIGHT) v = edges[Edge.End]!
|
|||
|
|
}
|
|||
|
|
if (v.unit === Unit.Undefined) return 0
|
|||
|
|
if (v.unit === Unit.Auto) return allowAuto ? NaN : 0
|
|||
|
|
return resolveValue(v, ownerSize)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resolveEdgeRaw(edges: Value[], physicalEdge: number): Value {
|
|||
|
|
let v = edges[physicalEdge]!
|
|||
|
|
if (v.unit === Unit.Undefined) {
|
|||
|
|
if (physicalEdge === EDGE_LEFT || physicalEdge === EDGE_RIGHT) {
|
|||
|
|
v = edges[Edge.Horizontal]!
|
|||
|
|
} else {
|
|||
|
|
v = edges[Edge.Vertical]!
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (v.unit === Unit.Undefined) v = edges[Edge.All]!
|
|||
|
|
if (v.unit === Unit.Undefined) {
|
|||
|
|
if (physicalEdge === EDGE_LEFT) v = edges[Edge.Start]!
|
|||
|
|
if (physicalEdge === EDGE_RIGHT) v = edges[Edge.End]!
|
|||
|
|
}
|
|||
|
|
return v
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function isMarginAuto(edges: Value[], physicalEdge: number): boolean {
|
|||
|
|
return resolveEdgeRaw(edges, physicalEdge).unit === Unit.Auto
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Setter helpers for the _hasAutoMargin / _hasPosition fast-path flags.
|
|||
|
|
// Unit.Undefined = 0, Unit.Auto = 3.
|
|||
|
|
function hasAnyAutoEdge(edges: Value[]): boolean {
|
|||
|
|
for (let i = 0; i < 9; i++) if (edges[i]!.unit === 3) return true
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
function hasAnyDefinedEdge(edges: Value[]): boolean {
|
|||
|
|
for (let i = 0; i < 9; i++) if (edges[i]!.unit !== 0) return true
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Hot path: resolve all 4 physical edges in one pass, writing into `out`.
|
|||
|
|
// Equivalent to calling resolveEdge() 4× with allowAuto=false, but hoists the
|
|||
|
|
// shared fallback lookups (Horizontal/Vertical/All/Start/End) and avoids
|
|||
|
|
// allocating a fresh 4-array on every layoutNode() call.
|
|||
|
|
function resolveEdges4Into(
|
|||
|
|
edges: Value[],
|
|||
|
|
ownerSize: number,
|
|||
|
|
out: [number, number, number, number],
|
|||
|
|
): void {
|
|||
|
|
// Hoist fallbacks once — the 4 per-edge chains share these reads.
|
|||
|
|
const eH = edges[6]! // Edge.Horizontal
|
|||
|
|
const eV = edges[7]! // Edge.Vertical
|
|||
|
|
const eA = edges[8]! // Edge.All
|
|||
|
|
const eS = edges[4]! // Edge.Start
|
|||
|
|
const eE = edges[5]! // Edge.End
|
|||
|
|
const pctDenom = isNaN(ownerSize) ? NaN : ownerSize / 100
|
|||
|
|
|
|||
|
|
// Left: edges[0] → Horizontal → All → Start
|
|||
|
|
let v = edges[0]!
|
|||
|
|
if (v.unit === 0) v = eH
|
|||
|
|
if (v.unit === 0) v = eA
|
|||
|
|
if (v.unit === 0) v = eS
|
|||
|
|
out[0] = v.unit === 1 ? v.value : v.unit === 2 ? v.value * pctDenom : 0
|
|||
|
|
|
|||
|
|
// Top: edges[1] → Vertical → All
|
|||
|
|
v = edges[1]!
|
|||
|
|
if (v.unit === 0) v = eV
|
|||
|
|
if (v.unit === 0) v = eA
|
|||
|
|
out[1] = v.unit === 1 ? v.value : v.unit === 2 ? v.value * pctDenom : 0
|
|||
|
|
|
|||
|
|
// Right: edges[2] → Horizontal → All → End
|
|||
|
|
v = edges[2]!
|
|||
|
|
if (v.unit === 0) v = eH
|
|||
|
|
if (v.unit === 0) v = eA
|
|||
|
|
if (v.unit === 0) v = eE
|
|||
|
|
out[2] = v.unit === 1 ? v.value : v.unit === 2 ? v.value * pctDenom : 0
|
|||
|
|
|
|||
|
|
// Bottom: edges[3] → Vertical → All
|
|||
|
|
v = edges[3]!
|
|||
|
|
if (v.unit === 0) v = eV
|
|||
|
|
if (v.unit === 0) v = eA
|
|||
|
|
out[3] = v.unit === 1 ? v.value : v.unit === 2 ? v.value * pctDenom : 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --
|
|||
|
|
// Axis helpers
|
|||
|
|
|
|||
|
|
function isRow(dir: FlexDirection): boolean {
|
|||
|
|
return dir === FlexDirection.Row || dir === FlexDirection.RowReverse
|
|||
|
|
}
|
|||
|
|
function isReverse(dir: FlexDirection): boolean {
|
|||
|
|
return dir === FlexDirection.RowReverse || dir === FlexDirection.ColumnReverse
|
|||
|
|
}
|
|||
|
|
function crossAxis(dir: FlexDirection): FlexDirection {
|
|||
|
|
return isRow(dir) ? FlexDirection.Column : FlexDirection.Row
|
|||
|
|
}
|
|||
|
|
function leadingEdge(dir: FlexDirection): number {
|
|||
|
|
switch (dir) {
|
|||
|
|
case FlexDirection.Row:
|
|||
|
|
return EDGE_LEFT
|
|||
|
|
case FlexDirection.RowReverse:
|
|||
|
|
return EDGE_RIGHT
|
|||
|
|
case FlexDirection.Column:
|
|||
|
|
return EDGE_TOP
|
|||
|
|
case FlexDirection.ColumnReverse:
|
|||
|
|
return EDGE_BOTTOM
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
function trailingEdge(dir: FlexDirection): number {
|
|||
|
|
switch (dir) {
|
|||
|
|
case FlexDirection.Row:
|
|||
|
|
return EDGE_RIGHT
|
|||
|
|
case FlexDirection.RowReverse:
|
|||
|
|
return EDGE_LEFT
|
|||
|
|
case FlexDirection.Column:
|
|||
|
|
return EDGE_BOTTOM
|
|||
|
|
case FlexDirection.ColumnReverse:
|
|||
|
|
return EDGE_TOP
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --
|
|||
|
|
// Public types
|
|||
|
|
|
|||
|
|
export type MeasureFunction = (
|
|||
|
|
width: number,
|
|||
|
|
widthMode: MeasureMode,
|
|||
|
|
height: number,
|
|||
|
|
heightMode: MeasureMode,
|
|||
|
|
) => { width: number; height: number }
|
|||
|
|
|
|||
|
|
export type Size = { width: number; height: number }
|
|||
|
|
|
|||
|
|
// --
|
|||
|
|
// Config
|
|||
|
|
|
|||
|
|
export type Config = {
|
|||
|
|
pointScaleFactor: number
|
|||
|
|
errata: Errata
|
|||
|
|
useWebDefaults: boolean
|
|||
|
|
free(): void
|
|||
|
|
isExperimentalFeatureEnabled(_: ExperimentalFeature): boolean
|
|||
|
|
setExperimentalFeatureEnabled(_: ExperimentalFeature, __: boolean): void
|
|||
|
|
setPointScaleFactor(factor: number): void
|
|||
|
|
getErrata(): Errata
|
|||
|
|
setErrata(errata: Errata): void
|
|||
|
|
setUseWebDefaults(v: boolean): void
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function createConfig(): Config {
|
|||
|
|
const config: Config = {
|
|||
|
|
pointScaleFactor: 1,
|
|||
|
|
errata: Errata.None,
|
|||
|
|
useWebDefaults: false,
|
|||
|
|
free() {},
|
|||
|
|
isExperimentalFeatureEnabled() {
|
|||
|
|
return false
|
|||
|
|
},
|
|||
|
|
setExperimentalFeatureEnabled() {},
|
|||
|
|
setPointScaleFactor(f) {
|
|||
|
|
config.pointScaleFactor = f
|
|||
|
|
},
|
|||
|
|
getErrata() {
|
|||
|
|
return config.errata
|
|||
|
|
},
|
|||
|
|
setErrata(e) {
|
|||
|
|
config.errata = e
|
|||
|
|
},
|
|||
|
|
setUseWebDefaults(v) {
|
|||
|
|
config.useWebDefaults = v
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
return config
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --
|
|||
|
|
// Node implementation
|
|||
|
|
|
|||
|
|
export class Node {
|
|||
|
|
style: Style
|
|||
|
|
layout: Layout
|
|||
|
|
parent: Node | null
|
|||
|
|
children: Node[]
|
|||
|
|
measureFunc: MeasureFunction | null
|
|||
|
|
config: Config
|
|||
|
|
isDirty_: boolean
|
|||
|
|
isReferenceBaseline_: boolean
|
|||
|
|
|
|||
|
|
// Per-layout scratch (not public API)
|
|||
|
|
_flexBasis = 0
|
|||
|
|
_mainSize = 0
|
|||
|
|
_crossSize = 0
|
|||
|
|
_lineIndex = 0
|
|||
|
|
// Fast-path flags maintained by style setters. Per CPU profile, the
|
|||
|
|
// positioning loop calls isMarginAuto 6× and resolveEdgeRaw(position) 4×
|
|||
|
|
// per child per layout pass — ~11k calls for the 1000-node bench, nearly
|
|||
|
|
// all of which return false/undefined since most nodes have no auto
|
|||
|
|
// margins and no position insets. These flags let us skip straight to
|
|||
|
|
// the common case with a single branch.
|
|||
|
|
_hasAutoMargin = false
|
|||
|
|
_hasPosition = false
|
|||
|
|
// Same pattern for the 3× resolveEdges4Into calls at the top of every
|
|||
|
|
// layoutNode(). In the 1000-node bench ~67% of those calls operate on
|
|||
|
|
// all-undefined edge arrays (most nodes have no border; only cols have
|
|||
|
|
// padding; only leaf cells have margin) — a single-branch skip beats
|
|||
|
|
// ~20 property reads + ~15 compares + 4 writes of zeros.
|
|||
|
|
_hasPadding = false
|
|||
|
|
_hasBorder = false
|
|||
|
|
_hasMargin = false
|
|||
|
|
// -- Dirty-flag layout cache. Mirrors upstream CalculateLayout.cpp's
|
|||
|
|
// layoutNodeInternal: skip a subtree entirely when it's clean and we're
|
|||
|
|
// asking the same question we cached the answer to. Two slots since
|
|||
|
|
// each node typically sees a measure call (performLayout=false, from
|
|||
|
|
// computeFlexBasis) followed by a layout call (performLayout=true) with
|
|||
|
|
// different inputs per parent pass — a single slot thrashes. Re-layout
|
|||
|
|
// bench (dirty one leaf, recompute root) went 2.7x→1.1x with this:
|
|||
|
|
// clean siblings skip straight through, only the dirty chain recomputes.
|
|||
|
|
_lW = NaN
|
|||
|
|
_lH = NaN
|
|||
|
|
_lWM: MeasureMode = 0
|
|||
|
|
_lHM: MeasureMode = 0
|
|||
|
|
_lOW = NaN
|
|||
|
|
_lOH = NaN
|
|||
|
|
_lFW = false
|
|||
|
|
_lFH = false
|
|||
|
|
// _hasL stores INPUTS early (before compute) but layout.width/height are
|
|||
|
|
// mutated by the multi-entry cache and by subsequent compute calls with
|
|||
|
|
// different inputs. Without storing OUTPUTS, a _hasL hit returns whatever
|
|||
|
|
// layout.width/height happened to be left by the last call — the scrollbox
|
|||
|
|
// vpH=33→2624 bug. Store + restore outputs like the multi-entry cache does.
|
|||
|
|
_lOutW = NaN
|
|||
|
|
_lOutH = NaN
|
|||
|
|
_hasL = false
|
|||
|
|
_mW = NaN
|
|||
|
|
_mH = NaN
|
|||
|
|
_mWM: MeasureMode = 0
|
|||
|
|
_mHM: MeasureMode = 0
|
|||
|
|
_mOW = NaN
|
|||
|
|
_mOH = NaN
|
|||
|
|
_mOutW = NaN
|
|||
|
|
_mOutH = NaN
|
|||
|
|
_hasM = false
|
|||
|
|
// Cached computeFlexBasis result. For clean children, basis only depends
|
|||
|
|
// on the container's inner dimensions — if those haven't changed, skip the
|
|||
|
|
// layoutNode(performLayout=false) recursion entirely. This is the hot path
|
|||
|
|
// for scroll: 500-message content container is dirty, its 499 clean
|
|||
|
|
// children each get measured ~20× as the dirty chain's measure/layout
|
|||
|
|
// passes cascade. Basis cache short-circuits at the child boundary.
|
|||
|
|
_fbBasis = NaN
|
|||
|
|
_fbOwnerW = NaN
|
|||
|
|
_fbOwnerH = NaN
|
|||
|
|
_fbAvailMain = NaN
|
|||
|
|
_fbAvailCross = NaN
|
|||
|
|
_fbCrossMode: MeasureMode = 0
|
|||
|
|
// Generation at which _fbBasis was written. Dirty nodes from a PREVIOUS
|
|||
|
|
// generation have stale cache (subtree changed), but within the SAME
|
|||
|
|
// generation the cache is fresh — the dirty chain's measure→layout
|
|||
|
|
// cascade invokes computeFlexBasis ≥2^depth times per calculateLayout on
|
|||
|
|
// fresh-mounted items, and the subtree doesn't change between calls.
|
|||
|
|
// Gating on generation instead of isDirty_ lets fresh mounts (virtual
|
|||
|
|
// scroll) cache-hit after first compute: 105k visits → ~10k.
|
|||
|
|
_fbGen = -1
|
|||
|
|
// Multi-entry layout cache — stores (inputs → computed w,h) so hits with
|
|||
|
|
// different inputs than _hasL can restore the right dimensions. Upstream
|
|||
|
|
// yoga uses 16; 4 covers Ink's dirty-chain depth. Packed as flat arrays
|
|||
|
|
// to avoid per-entry object allocs. Slot i uses indices [i*8, i*8+8) in
|
|||
|
|
// _cIn (aW,aH,wM,hM,oW,oH,fW,fH) and [i*2, i*2+2) in _cOut (w,h).
|
|||
|
|
_cIn: Float64Array | null = null
|
|||
|
|
_cOut: Float64Array | null = null
|
|||
|
|
_cGen = -1
|
|||
|
|
_cN = 0
|
|||
|
|
_cWr = 0
|
|||
|
|
|
|||
|
|
constructor(config?: Config) {
|
|||
|
|
this.style = defaultStyle()
|
|||
|
|
this.layout = {
|
|||
|
|
left: 0,
|
|||
|
|
top: 0,
|
|||
|
|
width: 0,
|
|||
|
|
height: 0,
|
|||
|
|
border: [0, 0, 0, 0],
|
|||
|
|
padding: [0, 0, 0, 0],
|
|||
|
|
margin: [0, 0, 0, 0],
|
|||
|
|
}
|
|||
|
|
this.parent = null
|
|||
|
|
this.children = []
|
|||
|
|
this.measureFunc = null
|
|||
|
|
this.config = config ?? DEFAULT_CONFIG
|
|||
|
|
this.isDirty_ = true
|
|||
|
|
this.isReferenceBaseline_ = false
|
|||
|
|
_yogaLiveNodes++
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -- Tree
|
|||
|
|
|
|||
|
|
insertChild(child: Node, index: number): void {
|
|||
|
|
child.parent = this
|
|||
|
|
this.children.splice(index, 0, child)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
removeChild(child: Node): void {
|
|||
|
|
const idx = this.children.indexOf(child)
|
|||
|
|
if (idx >= 0) {
|
|||
|
|
this.children.splice(idx, 1)
|
|||
|
|
child.parent = null
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
getChild(index: number): Node {
|
|||
|
|
return this.children[index]!
|
|||
|
|
}
|
|||
|
|
getChildCount(): number {
|
|||
|
|
return this.children.length
|
|||
|
|
}
|
|||
|
|
getParent(): Node | null {
|
|||
|
|
return this.parent
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -- Lifecycle
|
|||
|
|
|
|||
|
|
free(): void {
|
|||
|
|
this.parent = null
|
|||
|
|
this.children = []
|
|||
|
|
this.measureFunc = null
|
|||
|
|
this._cIn = null
|
|||
|
|
this._cOut = null
|
|||
|
|
_yogaLiveNodes--
|
|||
|
|
}
|
|||
|
|
freeRecursive(): void {
|
|||
|
|
for (const c of this.children) c.freeRecursive()
|
|||
|
|
this.free()
|
|||
|
|
}
|
|||
|
|
reset(): void {
|
|||
|
|
this.style = defaultStyle()
|
|||
|
|
this.children = []
|
|||
|
|
this.parent = null
|
|||
|
|
this.measureFunc = null
|
|||
|
|
this.isDirty_ = true
|
|||
|
|
this._hasAutoMargin = false
|
|||
|
|
this._hasPosition = false
|
|||
|
|
this._hasPadding = false
|
|||
|
|
this._hasBorder = false
|
|||
|
|
this._hasMargin = false
|
|||
|
|
this._hasL = false
|
|||
|
|
this._hasM = false
|
|||
|
|
this._cN = 0
|
|||
|
|
this._cWr = 0
|
|||
|
|
this._fbBasis = NaN
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -- Dirty tracking
|
|||
|
|
|
|||
|
|
markDirty(): void {
|
|||
|
|
this.isDirty_ = true
|
|||
|
|
if (this.parent && !this.parent.isDirty_) this.parent.markDirty()
|
|||
|
|
}
|
|||
|
|
isDirty(): boolean {
|
|||
|
|
return this.isDirty_
|
|||
|
|
}
|
|||
|
|
hasNewLayout(): boolean {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
markLayoutSeen(): void {}
|
|||
|
|
|
|||
|
|
// -- Measure function
|
|||
|
|
|
|||
|
|
setMeasureFunc(fn: MeasureFunction | null): void {
|
|||
|
|
this.measureFunc = fn
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
unsetMeasureFunc(): void {
|
|||
|
|
this.measureFunc = null
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -- Computed layout getters
|
|||
|
|
|
|||
|
|
getComputedLeft(): number {
|
|||
|
|
return this.layout.left
|
|||
|
|
}
|
|||
|
|
getComputedTop(): number {
|
|||
|
|
return this.layout.top
|
|||
|
|
}
|
|||
|
|
getComputedWidth(): number {
|
|||
|
|
return this.layout.width
|
|||
|
|
}
|
|||
|
|
getComputedHeight(): number {
|
|||
|
|
return this.layout.height
|
|||
|
|
}
|
|||
|
|
getComputedRight(): number {
|
|||
|
|
const p = this.parent
|
|||
|
|
return p ? p.layout.width - this.layout.left - this.layout.width : 0
|
|||
|
|
}
|
|||
|
|
getComputedBottom(): number {
|
|||
|
|
const p = this.parent
|
|||
|
|
return p ? p.layout.height - this.layout.top - this.layout.height : 0
|
|||
|
|
}
|
|||
|
|
getComputedLayout(): {
|
|||
|
|
left: number
|
|||
|
|
top: number
|
|||
|
|
right: number
|
|||
|
|
bottom: number
|
|||
|
|
width: number
|
|||
|
|
height: number
|
|||
|
|
} {
|
|||
|
|
return {
|
|||
|
|
left: this.layout.left,
|
|||
|
|
top: this.layout.top,
|
|||
|
|
right: this.getComputedRight(),
|
|||
|
|
bottom: this.getComputedBottom(),
|
|||
|
|
width: this.layout.width,
|
|||
|
|
height: this.layout.height,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
getComputedBorder(edge: Edge): number {
|
|||
|
|
return this.layout.border[physicalEdge(edge)]!
|
|||
|
|
}
|
|||
|
|
getComputedPadding(edge: Edge): number {
|
|||
|
|
return this.layout.padding[physicalEdge(edge)]!
|
|||
|
|
}
|
|||
|
|
getComputedMargin(edge: Edge): number {
|
|||
|
|
return this.layout.margin[physicalEdge(edge)]!
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -- Style setters: dimensions
|
|||
|
|
|
|||
|
|
setWidth(v: number | 'auto' | string | undefined): void {
|
|||
|
|
this.style.width = parseDimension(v)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setWidthPercent(v: number): void {
|
|||
|
|
this.style.width = percentValue(v)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setWidthAuto(): void {
|
|||
|
|
this.style.width = AUTO_VALUE
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setHeight(v: number | 'auto' | string | undefined): void {
|
|||
|
|
this.style.height = parseDimension(v)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setHeightPercent(v: number): void {
|
|||
|
|
this.style.height = percentValue(v)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setHeightAuto(): void {
|
|||
|
|
this.style.height = AUTO_VALUE
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setMinWidth(v: number | string | undefined): void {
|
|||
|
|
this.style.minWidth = parseDimension(v)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setMinWidthPercent(v: number): void {
|
|||
|
|
this.style.minWidth = percentValue(v)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setMinHeight(v: number | string | undefined): void {
|
|||
|
|
this.style.minHeight = parseDimension(v)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setMinHeightPercent(v: number): void {
|
|||
|
|
this.style.minHeight = percentValue(v)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setMaxWidth(v: number | string | undefined): void {
|
|||
|
|
this.style.maxWidth = parseDimension(v)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setMaxWidthPercent(v: number): void {
|
|||
|
|
this.style.maxWidth = percentValue(v)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setMaxHeight(v: number | string | undefined): void {
|
|||
|
|
this.style.maxHeight = parseDimension(v)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setMaxHeightPercent(v: number): void {
|
|||
|
|
this.style.maxHeight = percentValue(v)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -- Style setters: flex
|
|||
|
|
|
|||
|
|
setFlexDirection(dir: FlexDirection): void {
|
|||
|
|
this.style.flexDirection = dir
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setFlexGrow(v: number | undefined): void {
|
|||
|
|
this.style.flexGrow = v ?? 0
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setFlexShrink(v: number | undefined): void {
|
|||
|
|
this.style.flexShrink = v ?? 0
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setFlex(v: number | undefined): void {
|
|||
|
|
if (v === undefined || isNaN(v)) {
|
|||
|
|
this.style.flexGrow = 0
|
|||
|
|
this.style.flexShrink = 0
|
|||
|
|
} else if (v > 0) {
|
|||
|
|
this.style.flexGrow = v
|
|||
|
|
this.style.flexShrink = 1
|
|||
|
|
this.style.flexBasis = pointValue(0)
|
|||
|
|
} else if (v < 0) {
|
|||
|
|
this.style.flexGrow = 0
|
|||
|
|
this.style.flexShrink = -v
|
|||
|
|
} else {
|
|||
|
|
this.style.flexGrow = 0
|
|||
|
|
this.style.flexShrink = 0
|
|||
|
|
}
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setFlexBasis(v: number | 'auto' | string | undefined): void {
|
|||
|
|
this.style.flexBasis = parseDimension(v)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setFlexBasisPercent(v: number): void {
|
|||
|
|
this.style.flexBasis = percentValue(v)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setFlexBasisAuto(): void {
|
|||
|
|
this.style.flexBasis = AUTO_VALUE
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setFlexWrap(wrap: Wrap): void {
|
|||
|
|
this.style.flexWrap = wrap
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -- Style setters: alignment
|
|||
|
|
|
|||
|
|
setAlignItems(a: Align): void {
|
|||
|
|
this.style.alignItems = a
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setAlignSelf(a: Align): void {
|
|||
|
|
this.style.alignSelf = a
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setAlignContent(a: Align): void {
|
|||
|
|
this.style.alignContent = a
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setJustifyContent(j: Justify): void {
|
|||
|
|
this.style.justifyContent = j
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -- Style setters: display / position / overflow
|
|||
|
|
|
|||
|
|
setDisplay(d: Display): void {
|
|||
|
|
this.style.display = d
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
getDisplay(): Display {
|
|||
|
|
return this.style.display
|
|||
|
|
}
|
|||
|
|
setPositionType(t: PositionType): void {
|
|||
|
|
this.style.positionType = t
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setPosition(edge: Edge, v: number | string | undefined): void {
|
|||
|
|
this.style.position[edge] = parseDimension(v)
|
|||
|
|
this._hasPosition = hasAnyDefinedEdge(this.style.position)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setPositionPercent(edge: Edge, v: number): void {
|
|||
|
|
this.style.position[edge] = percentValue(v)
|
|||
|
|
this._hasPosition = true
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setPositionAuto(edge: Edge): void {
|
|||
|
|
this.style.position[edge] = AUTO_VALUE
|
|||
|
|
this._hasPosition = true
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setOverflow(o: Overflow): void {
|
|||
|
|
this.style.overflow = o
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setDirection(d: Direction): void {
|
|||
|
|
this.style.direction = d
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setBoxSizing(_: BoxSizing): void {
|
|||
|
|
// Not implemented — Ink doesn't use content-box
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -- Style setters: spacing
|
|||
|
|
|
|||
|
|
setMargin(edge: Edge, v: number | 'auto' | string | undefined): void {
|
|||
|
|
const val = parseDimension(v)
|
|||
|
|
this.style.margin[edge] = val
|
|||
|
|
if (val.unit === Unit.Auto) this._hasAutoMargin = true
|
|||
|
|
else this._hasAutoMargin = hasAnyAutoEdge(this.style.margin)
|
|||
|
|
this._hasMargin =
|
|||
|
|
this._hasAutoMargin || hasAnyDefinedEdge(this.style.margin)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setMarginPercent(edge: Edge, v: number): void {
|
|||
|
|
this.style.margin[edge] = percentValue(v)
|
|||
|
|
this._hasAutoMargin = hasAnyAutoEdge(this.style.margin)
|
|||
|
|
this._hasMargin = true
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setMarginAuto(edge: Edge): void {
|
|||
|
|
this.style.margin[edge] = AUTO_VALUE
|
|||
|
|
this._hasAutoMargin = true
|
|||
|
|
this._hasMargin = true
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setPadding(edge: Edge, v: number | string | undefined): void {
|
|||
|
|
this.style.padding[edge] = parseDimension(v)
|
|||
|
|
this._hasPadding = hasAnyDefinedEdge(this.style.padding)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setPaddingPercent(edge: Edge, v: number): void {
|
|||
|
|
this.style.padding[edge] = percentValue(v)
|
|||
|
|
this._hasPadding = true
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setBorder(edge: Edge, v: number | undefined): void {
|
|||
|
|
this.style.border[edge] = v === undefined ? UNDEFINED_VALUE : pointValue(v)
|
|||
|
|
this._hasBorder = hasAnyDefinedEdge(this.style.border)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setGap(gutter: Gutter, v: number | string | undefined): void {
|
|||
|
|
this.style.gap[gutter] = parseDimension(v)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
setGapPercent(gutter: Gutter, v: number): void {
|
|||
|
|
this.style.gap[gutter] = percentValue(v)
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -- Style getters (partial — only what tests need)
|
|||
|
|
|
|||
|
|
getFlexDirection(): FlexDirection {
|
|||
|
|
return this.style.flexDirection
|
|||
|
|
}
|
|||
|
|
getJustifyContent(): Justify {
|
|||
|
|
return this.style.justifyContent
|
|||
|
|
}
|
|||
|
|
getAlignItems(): Align {
|
|||
|
|
return this.style.alignItems
|
|||
|
|
}
|
|||
|
|
getAlignSelf(): Align {
|
|||
|
|
return this.style.alignSelf
|
|||
|
|
}
|
|||
|
|
getAlignContent(): Align {
|
|||
|
|
return this.style.alignContent
|
|||
|
|
}
|
|||
|
|
getFlexGrow(): number {
|
|||
|
|
return this.style.flexGrow
|
|||
|
|
}
|
|||
|
|
getFlexShrink(): number {
|
|||
|
|
return this.style.flexShrink
|
|||
|
|
}
|
|||
|
|
getFlexBasis(): Value {
|
|||
|
|
return this.style.flexBasis
|
|||
|
|
}
|
|||
|
|
getFlexWrap(): Wrap {
|
|||
|
|
return this.style.flexWrap
|
|||
|
|
}
|
|||
|
|
getWidth(): Value {
|
|||
|
|
return this.style.width
|
|||
|
|
}
|
|||
|
|
getHeight(): Value {
|
|||
|
|
return this.style.height
|
|||
|
|
}
|
|||
|
|
getOverflow(): Overflow {
|
|||
|
|
return this.style.overflow
|
|||
|
|
}
|
|||
|
|
getPositionType(): PositionType {
|
|||
|
|
return this.style.positionType
|
|||
|
|
}
|
|||
|
|
getDirection(): Direction {
|
|||
|
|
return this.style.direction
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -- Unused API stubs (present for API parity)
|
|||
|
|
|
|||
|
|
copyStyle(_: Node): void {}
|
|||
|
|
setDirtiedFunc(_: unknown): void {}
|
|||
|
|
unsetDirtiedFunc(): void {}
|
|||
|
|
setIsReferenceBaseline(v: boolean): void {
|
|||
|
|
this.isReferenceBaseline_ = v
|
|||
|
|
this.markDirty()
|
|||
|
|
}
|
|||
|
|
isReferenceBaseline(): boolean {
|
|||
|
|
return this.isReferenceBaseline_
|
|||
|
|
}
|
|||
|
|
setAspectRatio(_: number | undefined): void {}
|
|||
|
|
getAspectRatio(): number {
|
|||
|
|
return NaN
|
|||
|
|
}
|
|||
|
|
setAlwaysFormsContainingBlock(_: boolean): void {}
|
|||
|
|
|
|||
|
|
// -- Layout entry point
|
|||
|
|
|
|||
|
|
calculateLayout(
|
|||
|
|
ownerWidth: number | undefined,
|
|||
|
|
ownerHeight: number | undefined,
|
|||
|
|
_direction?: Direction,
|
|||
|
|
): void {
|
|||
|
|
_yogaNodesVisited = 0
|
|||
|
|
_yogaMeasureCalls = 0
|
|||
|
|
_yogaCacheHits = 0
|
|||
|
|
_generation++
|
|||
|
|
const w = ownerWidth === undefined ? NaN : ownerWidth
|
|||
|
|
const h = ownerHeight === undefined ? NaN : ownerHeight
|
|||
|
|
layoutNode(
|
|||
|
|
this,
|
|||
|
|
w,
|
|||
|
|
h,
|
|||
|
|
isDefined(w) ? MeasureMode.Exactly : MeasureMode.Undefined,
|
|||
|
|
isDefined(h) ? MeasureMode.Exactly : MeasureMode.Undefined,
|
|||
|
|
w,
|
|||
|
|
h,
|
|||
|
|
true,
|
|||
|
|
)
|
|||
|
|
// Root's own position = margin + position insets (yoga applies position
|
|||
|
|
// to the root even without a parent container; this matters for rounding
|
|||
|
|
// since the root's abs top/left seeds the pixel-grid walk).
|
|||
|
|
const mar = this.layout.margin
|
|||
|
|
const posL = resolveValue(
|
|||
|
|
resolveEdgeRaw(this.style.position, EDGE_LEFT),
|
|||
|
|
isDefined(w) ? w : 0,
|
|||
|
|
)
|
|||
|
|
const posT = resolveValue(
|
|||
|
|
resolveEdgeRaw(this.style.position, EDGE_TOP),
|
|||
|
|
isDefined(w) ? w : 0,
|
|||
|
|
)
|
|||
|
|
this.layout.left = mar[EDGE_LEFT] + (isDefined(posL) ? posL : 0)
|
|||
|
|
this.layout.top = mar[EDGE_TOP] + (isDefined(posT) ? posT : 0)
|
|||
|
|
roundLayout(this, this.config.pointScaleFactor, 0, 0)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const DEFAULT_CONFIG = createConfig()
|
|||
|
|
|
|||
|
|
const CACHE_SLOTS = 4
|
|||
|
|
function cacheWrite(
|
|||
|
|
node: Node,
|
|||
|
|
aW: number,
|
|||
|
|
aH: number,
|
|||
|
|
wM: MeasureMode,
|
|||
|
|
hM: MeasureMode,
|
|||
|
|
oW: number,
|
|||
|
|
oH: number,
|
|||
|
|
fW: boolean,
|
|||
|
|
fH: boolean,
|
|||
|
|
wasDirty: boolean,
|
|||
|
|
): void {
|
|||
|
|
if (!node._cIn) {
|
|||
|
|
node._cIn = new Float64Array(CACHE_SLOTS * 8)
|
|||
|
|
node._cOut = new Float64Array(CACHE_SLOTS * 2)
|
|||
|
|
}
|
|||
|
|
// First write after a dirty clears stale entries from before the dirty.
|
|||
|
|
// _cGen < _generation means entries are from a previous calculateLayout;
|
|||
|
|
// if wasDirty, the subtree changed since then → old dimensions invalid.
|
|||
|
|
// Clean nodes' old entries stay — same subtree → same result for same
|
|||
|
|
// inputs, so cross-generation caching works (the scroll hot path where
|
|||
|
|
// 499 clean messages cache-hit while one dirty leaf recomputes).
|
|||
|
|
if (wasDirty && node._cGen !== _generation) {
|
|||
|
|
node._cN = 0
|
|||
|
|
node._cWr = 0
|
|||
|
|
}
|
|||
|
|
// LRU write index wraps; _cN stays at CACHE_SLOTS so the read scan always
|
|||
|
|
// checks all populated slots (not just those since last wrap).
|
|||
|
|
const i = node._cWr++ % CACHE_SLOTS
|
|||
|
|
if (node._cN < CACHE_SLOTS) node._cN = node._cWr
|
|||
|
|
const o = i * 8
|
|||
|
|
const cIn = node._cIn
|
|||
|
|
cIn[o] = aW
|
|||
|
|
cIn[o + 1] = aH
|
|||
|
|
cIn[o + 2] = wM
|
|||
|
|
cIn[o + 3] = hM
|
|||
|
|
cIn[o + 4] = oW
|
|||
|
|
cIn[o + 5] = oH
|
|||
|
|
cIn[o + 6] = fW ? 1 : 0
|
|||
|
|
cIn[o + 7] = fH ? 1 : 0
|
|||
|
|
node._cOut![i * 2] = node.layout.width
|
|||
|
|
node._cOut![i * 2 + 1] = node.layout.height
|
|||
|
|
node._cGen = _generation
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Store computed layout.width/height into the single-slot cache output fields.
|
|||
|
|
// _hasL/_hasM inputs are committed at the TOP of layoutNode (before compute);
|
|||
|
|
// outputs must be committed HERE (after compute) so a cache hit can restore
|
|||
|
|
// the correct dimensions. Without this, a _hasL hit returns whatever
|
|||
|
|
// layout.width/height was left by the last call — which may be the intrinsic
|
|||
|
|
// content height from a heightMode=Undefined measure pass rather than the
|
|||
|
|
// constrained viewport height from the layout pass. That's the scrollbox
|
|||
|
|
// vpH=33→2624 bug: scrollTop clamps to 0, viewport goes blank.
|
|||
|
|
function commitCacheOutputs(node: Node, performLayout: boolean): void {
|
|||
|
|
if (performLayout) {
|
|||
|
|
node._lOutW = node.layout.width
|
|||
|
|
node._lOutH = node.layout.height
|
|||
|
|
} else {
|
|||
|
|
node._mOutW = node.layout.width
|
|||
|
|
node._mOutH = node.layout.height
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --
|
|||
|
|
// Core flexbox algorithm
|
|||
|
|
|
|||
|
|
// Profiling counters — reset per calculateLayout, read via getYogaCounters.
|
|||
|
|
// Incremented on each calculateLayout(). Nodes stamp _fbGen/_cGen when
|
|||
|
|
// their cache is written; a cache entry with gen === _generation was
|
|||
|
|
// computed THIS pass and is fresh regardless of isDirty_ state.
|
|||
|
|
let _generation = 0
|
|||
|
|
let _yogaNodesVisited = 0
|
|||
|
|
let _yogaMeasureCalls = 0
|
|||
|
|
let _yogaCacheHits = 0
|
|||
|
|
let _yogaLiveNodes = 0
|
|||
|
|
export function getYogaCounters(): {
|
|||
|
|
visited: number
|
|||
|
|
measured: number
|
|||
|
|
cacheHits: number
|
|||
|
|
live: number
|
|||
|
|
} {
|
|||
|
|
return {
|
|||
|
|
visited: _yogaNodesVisited,
|
|||
|
|
measured: _yogaMeasureCalls,
|
|||
|
|
cacheHits: _yogaCacheHits,
|
|||
|
|
live: _yogaLiveNodes,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function layoutNode(
|
|||
|
|
node: Node,
|
|||
|
|
availableWidth: number,
|
|||
|
|
availableHeight: number,
|
|||
|
|
widthMode: MeasureMode,
|
|||
|
|
heightMode: MeasureMode,
|
|||
|
|
ownerWidth: number,
|
|||
|
|
ownerHeight: number,
|
|||
|
|
performLayout: boolean,
|
|||
|
|
// When true, ignore style dimension on this axis — the flex container
|
|||
|
|
// has already determined the main size (flex-basis + grow/shrink result).
|
|||
|
|
forceWidth = false,
|
|||
|
|
forceHeight = false,
|
|||
|
|
): void {
|
|||
|
|
_yogaNodesVisited++
|
|||
|
|
const style = node.style
|
|||
|
|
const layout = node.layout
|
|||
|
|
|
|||
|
|
// Dirty-flag skip: clean subtree + matching inputs → layout object already
|
|||
|
|
// holds the answer. A cached layout result also satisfies a measure request
|
|||
|
|
// (positions are a superset of dimensions); the reverse does not hold.
|
|||
|
|
// Same-generation entries are fresh regardless of isDirty_ — they were
|
|||
|
|
// computed THIS calculateLayout, the subtree hasn't changed since.
|
|||
|
|
// Previous-generation entries need !isDirty_ (a dirty node's cache from
|
|||
|
|
// before the dirty is stale).
|
|||
|
|
// sameGen bypass only for MEASURE calls — a layout-pass cache hit would
|
|||
|
|
// skip the child-positioning recursion (STEP 5), leaving children at
|
|||
|
|
// stale positions. Measure calls only need w/h which the cache stores.
|
|||
|
|
const sameGen = node._cGen === _generation && !performLayout
|
|||
|
|
if (!node.isDirty_ || sameGen) {
|
|||
|
|
if (
|
|||
|
|
!node.isDirty_ &&
|
|||
|
|
node._hasL &&
|
|||
|
|
node._lWM === widthMode &&
|
|||
|
|
node._lHM === heightMode &&
|
|||
|
|
node._lFW === forceWidth &&
|
|||
|
|
node._lFH === forceHeight &&
|
|||
|
|
sameFloat(node._lW, availableWidth) &&
|
|||
|
|
sameFloat(node._lH, availableHeight) &&
|
|||
|
|
sameFloat(node._lOW, ownerWidth) &&
|
|||
|
|
sameFloat(node._lOH, ownerHeight)
|
|||
|
|
) {
|
|||
|
|
_yogaCacheHits++
|
|||
|
|
layout.width = node._lOutW
|
|||
|
|
layout.height = node._lOutH
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
// Multi-entry cache: scan for matching inputs, restore cached w/h on hit.
|
|||
|
|
// Covers the scroll case where a dirty ancestor's measure→layout cascade
|
|||
|
|
// produces N>1 distinct input combos per clean child — the single _hasL
|
|||
|
|
// slot thrashed, forcing full subtree recursion. With 500-message
|
|||
|
|
// scrollbox and one dirty leaf, this took dirty-leaf relayout from
|
|||
|
|
// 76k layoutNode calls (21.7×nodes) to 4k (1.2×nodes), 6.86ms → 550µs.
|
|||
|
|
// Same-generation check covers fresh-mounted (dirty) nodes during
|
|||
|
|
// virtual scroll — the dirty chain invokes them ≥2^depth times, first
|
|||
|
|
// call writes cache, rest hit: 105k visits → ~10k for 1593-node tree.
|
|||
|
|
if (node._cN > 0 && (sameGen || !node.isDirty_)) {
|
|||
|
|
const cIn = node._cIn!
|
|||
|
|
for (let i = 0; i < node._cN; i++) {
|
|||
|
|
const o = i * 8
|
|||
|
|
if (
|
|||
|
|
cIn[o + 2] === widthMode &&
|
|||
|
|
cIn[o + 3] === heightMode &&
|
|||
|
|
cIn[o + 6] === (forceWidth ? 1 : 0) &&
|
|||
|
|
cIn[o + 7] === (forceHeight ? 1 : 0) &&
|
|||
|
|
sameFloat(cIn[o]!, availableWidth) &&
|
|||
|
|
sameFloat(cIn[o + 1]!, availableHeight) &&
|
|||
|
|
sameFloat(cIn[o + 4]!, ownerWidth) &&
|
|||
|
|
sameFloat(cIn[o + 5]!, ownerHeight)
|
|||
|
|
) {
|
|||
|
|
layout.width = node._cOut![i * 2]!
|
|||
|
|
layout.height = node._cOut![i * 2 + 1]!
|
|||
|
|
_yogaCacheHits++
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (
|
|||
|
|
!node.isDirty_ &&
|
|||
|
|
!performLayout &&
|
|||
|
|
node._hasM &&
|
|||
|
|
node._mWM === widthMode &&
|
|||
|
|
node._mHM === heightMode &&
|
|||
|
|
sameFloat(node._mW, availableWidth) &&
|
|||
|
|
sameFloat(node._mH, availableHeight) &&
|
|||
|
|
sameFloat(node._mOW, ownerWidth) &&
|
|||
|
|
sameFloat(node._mOH, ownerHeight)
|
|||
|
|
) {
|
|||
|
|
layout.width = node._mOutW
|
|||
|
|
layout.height = node._mOutH
|
|||
|
|
_yogaCacheHits++
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Commit cache inputs up front so every return path leaves a valid entry.
|
|||
|
|
// Only clear isDirty_ on the LAYOUT pass — the measure pass (computeFlexBasis
|
|||
|
|
// → layoutNode(performLayout=false)) runs before the layout pass in the same
|
|||
|
|
// calculateLayout call. Clearing dirty during measure lets the subsequent
|
|||
|
|
// layout pass hit the STALE _hasL cache from the previous calculateLayout
|
|||
|
|
// (before children were inserted), so ScrollBox content height never grows
|
|||
|
|
// and sticky-scroll never follows new content. A dirty node's _hasL entry is
|
|||
|
|
// stale by definition — invalidate it so the layout pass recomputes.
|
|||
|
|
const wasDirty = node.isDirty_
|
|||
|
|
if (performLayout) {
|
|||
|
|
node._lW = availableWidth
|
|||
|
|
node._lH = availableHeight
|
|||
|
|
node._lWM = widthMode
|
|||
|
|
node._lHM = heightMode
|
|||
|
|
node._lOW = ownerWidth
|
|||
|
|
node._lOH = ownerHeight
|
|||
|
|
node._lFW = forceWidth
|
|||
|
|
node._lFH = forceHeight
|
|||
|
|
node._hasL = true
|
|||
|
|
node.isDirty_ = false
|
|||
|
|
// Previous approach cleared _cN here to prevent stale pre-dirty entries
|
|||
|
|
// from hitting (long-continuous blank-screen bug). Now replaced by
|
|||
|
|
// generation stamping: the cache check requires sameGen || !isDirty_, so
|
|||
|
|
// previous-generation entries from a dirty node can't hit. Clearing here
|
|||
|
|
// would wipe fresh same-generation entries from an earlier measure call,
|
|||
|
|
// forcing recompute on the layout call.
|
|||
|
|
if (wasDirty) node._hasM = false
|
|||
|
|
} else {
|
|||
|
|
node._mW = availableWidth
|
|||
|
|
node._mH = availableHeight
|
|||
|
|
node._mWM = widthMode
|
|||
|
|
node._mHM = heightMode
|
|||
|
|
node._mOW = ownerWidth
|
|||
|
|
node._mOH = ownerHeight
|
|||
|
|
node._hasM = true
|
|||
|
|
// Don't clear isDirty_. For DIRTY nodes, invalidate _hasL so the upcoming
|
|||
|
|
// performLayout=true call recomputes with the new child set (otherwise
|
|||
|
|
// sticky-scroll never follows new content — the bug from 4557bc9f9c).
|
|||
|
|
// Clean nodes keep _hasL: their layout from the previous generation is
|
|||
|
|
// still valid, they're only here because an ancestor is dirty and called
|
|||
|
|
// with different inputs than cached.
|
|||
|
|
if (wasDirty) node._hasL = false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Resolve padding/border/margin against ownerWidth (yoga uses ownerWidth for %)
|
|||
|
|
// Write directly into the pre-allocated layout arrays — avoids 3 allocs per
|
|||
|
|
// layoutNode call and 12 resolveEdge calls (was the #1 hotspot per CPU profile).
|
|||
|
|
// Skip entirely when no edges are set — the 4-write zero is cheaper than
|
|||
|
|
// the ~20 reads + ~15 compares resolveEdges4Into does to produce zeros.
|
|||
|
|
const pad = layout.padding
|
|||
|
|
const bor = layout.border
|
|||
|
|
const mar = layout.margin
|
|||
|
|
if (node._hasPadding) resolveEdges4Into(style.padding, ownerWidth, pad)
|
|||
|
|
else pad[0] = pad[1] = pad[2] = pad[3] = 0
|
|||
|
|
if (node._hasBorder) resolveEdges4Into(style.border, ownerWidth, bor)
|
|||
|
|
else bor[0] = bor[1] = bor[2] = bor[3] = 0
|
|||
|
|
if (node._hasMargin) resolveEdges4Into(style.margin, ownerWidth, mar)
|
|||
|
|
else mar[0] = mar[1] = mar[2] = mar[3] = 0
|
|||
|
|
|
|||
|
|
const paddingBorderWidth = pad[0] + pad[2] + bor[0] + bor[2]
|
|||
|
|
const paddingBorderHeight = pad[1] + pad[3] + bor[1] + bor[3]
|
|||
|
|
|
|||
|
|
// Resolve style dimensions
|
|||
|
|
const styleWidth = forceWidth ? NaN : resolveValue(style.width, ownerWidth)
|
|||
|
|
const styleHeight = forceHeight
|
|||
|
|
? NaN
|
|||
|
|
: resolveValue(style.height, ownerHeight)
|
|||
|
|
|
|||
|
|
// If style dimension is defined, it overrides the available size
|
|||
|
|
let width = availableWidth
|
|||
|
|
let height = availableHeight
|
|||
|
|
let wMode = widthMode
|
|||
|
|
let hMode = heightMode
|
|||
|
|
if (isDefined(styleWidth)) {
|
|||
|
|
width = styleWidth
|
|||
|
|
wMode = MeasureMode.Exactly
|
|||
|
|
}
|
|||
|
|
if (isDefined(styleHeight)) {
|
|||
|
|
height = styleHeight
|
|||
|
|
hMode = MeasureMode.Exactly
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Apply min/max constraints to the node's own dimensions
|
|||
|
|
width = boundAxis(style, true, width, ownerWidth, ownerHeight)
|
|||
|
|
height = boundAxis(style, false, height, ownerWidth, ownerHeight)
|
|||
|
|
|
|||
|
|
// Measure-func leaf node
|
|||
|
|
if (node.measureFunc && node.children.length === 0) {
|
|||
|
|
const innerW =
|
|||
|
|
wMode === MeasureMode.Undefined
|
|||
|
|
? NaN
|
|||
|
|
: Math.max(0, width - paddingBorderWidth)
|
|||
|
|
const innerH =
|
|||
|
|
hMode === MeasureMode.Undefined
|
|||
|
|
? NaN
|
|||
|
|
: Math.max(0, height - paddingBorderHeight)
|
|||
|
|
_yogaMeasureCalls++
|
|||
|
|
const measured = node.measureFunc(innerW, wMode, innerH, hMode)
|
|||
|
|
node.layout.width =
|
|||
|
|
wMode === MeasureMode.Exactly
|
|||
|
|
? width
|
|||
|
|
: boundAxis(
|
|||
|
|
style,
|
|||
|
|
true,
|
|||
|
|
(measured.width ?? 0) + paddingBorderWidth,
|
|||
|
|
ownerWidth,
|
|||
|
|
ownerHeight,
|
|||
|
|
)
|
|||
|
|
node.layout.height =
|
|||
|
|
hMode === MeasureMode.Exactly
|
|||
|
|
? height
|
|||
|
|
: boundAxis(
|
|||
|
|
style,
|
|||
|
|
false,
|
|||
|
|
(measured.height ?? 0) + paddingBorderHeight,
|
|||
|
|
ownerWidth,
|
|||
|
|
ownerHeight,
|
|||
|
|
)
|
|||
|
|
commitCacheOutputs(node, performLayout)
|
|||
|
|
// Write cache even for dirty nodes — fresh-mounted items during virtual
|
|||
|
|
// scroll are dirty on first layout, but the dirty chain's measure→layout
|
|||
|
|
// cascade invokes them ≥2^depth times per calculateLayout. Writing here
|
|||
|
|
// lets the 2nd+ calls hit cache (isDirty_ was cleared in the layout pass
|
|||
|
|
// above). Measured: 105k visits → 10k for a 1593-node fresh-mount tree.
|
|||
|
|
cacheWrite(
|
|||
|
|
node,
|
|||
|
|
availableWidth,
|
|||
|
|
availableHeight,
|
|||
|
|
widthMode,
|
|||
|
|
heightMode,
|
|||
|
|
ownerWidth,
|
|||
|
|
ownerHeight,
|
|||
|
|
forceWidth,
|
|||
|
|
forceHeight,
|
|||
|
|
wasDirty,
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Leaf node with no children and no measure func
|
|||
|
|
if (node.children.length === 0) {
|
|||
|
|
node.layout.width =
|
|||
|
|
wMode === MeasureMode.Exactly
|
|||
|
|
? width
|
|||
|
|
: boundAxis(style, true, paddingBorderWidth, ownerWidth, ownerHeight)
|
|||
|
|
node.layout.height =
|
|||
|
|
hMode === MeasureMode.Exactly
|
|||
|
|
? height
|
|||
|
|
: boundAxis(style, false, paddingBorderHeight, ownerWidth, ownerHeight)
|
|||
|
|
commitCacheOutputs(node, performLayout)
|
|||
|
|
// Write cache even for dirty nodes — fresh-mounted items during virtual
|
|||
|
|
// scroll are dirty on first layout, but the dirty chain's measure→layout
|
|||
|
|
// cascade invokes them ≥2^depth times per calculateLayout. Writing here
|
|||
|
|
// lets the 2nd+ calls hit cache (isDirty_ was cleared in the layout pass
|
|||
|
|
// above). Measured: 105k visits → 10k for a 1593-node fresh-mount tree.
|
|||
|
|
cacheWrite(
|
|||
|
|
node,
|
|||
|
|
availableWidth,
|
|||
|
|
availableHeight,
|
|||
|
|
widthMode,
|
|||
|
|
heightMode,
|
|||
|
|
ownerWidth,
|
|||
|
|
ownerHeight,
|
|||
|
|
forceWidth,
|
|||
|
|
forceHeight,
|
|||
|
|
wasDirty,
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Container with children — run flexbox algorithm
|
|||
|
|
const mainAxis = style.flexDirection
|
|||
|
|
const crossAx = crossAxis(mainAxis)
|
|||
|
|
const isMainRow = isRow(mainAxis)
|
|||
|
|
|
|||
|
|
const mainSize = isMainRow ? width : height
|
|||
|
|
const crossSize = isMainRow ? height : width
|
|||
|
|
const mainMode = isMainRow ? wMode : hMode
|
|||
|
|
const crossMode = isMainRow ? hMode : wMode
|
|||
|
|
const mainPadBorder = isMainRow ? paddingBorderWidth : paddingBorderHeight
|
|||
|
|
const crossPadBorder = isMainRow ? paddingBorderHeight : paddingBorderWidth
|
|||
|
|
|
|||
|
|
const innerMainSize = isDefined(mainSize)
|
|||
|
|
? Math.max(0, mainSize - mainPadBorder)
|
|||
|
|
: NaN
|
|||
|
|
const innerCrossSize = isDefined(crossSize)
|
|||
|
|
? Math.max(0, crossSize - crossPadBorder)
|
|||
|
|
: NaN
|
|||
|
|
|
|||
|
|
// Resolve gap
|
|||
|
|
const gapMain = resolveGap(
|
|||
|
|
style,
|
|||
|
|
isMainRow ? Gutter.Column : Gutter.Row,
|
|||
|
|
innerMainSize,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Partition children into flow vs absolute. display:contents nodes are
|
|||
|
|
// transparent — their children are lifted into the grandparent's child list
|
|||
|
|
// (recursively), and the contents node itself gets zero layout.
|
|||
|
|
const flowChildren: Node[] = []
|
|||
|
|
const absChildren: Node[] = []
|
|||
|
|
collectLayoutChildren(node, flowChildren, absChildren)
|
|||
|
|
|
|||
|
|
// ownerW/H are the reference sizes for resolving children's percentage
|
|||
|
|
// values. Per CSS, a % width resolves against the parent's content-box
|
|||
|
|
// width. If this node's width is indefinite, children's % widths are also
|
|||
|
|
// indefinite — do NOT fall through to the grandparent's size.
|
|||
|
|
const ownerW = isDefined(width) ? width : NaN
|
|||
|
|
const ownerH = isDefined(height) ? height : NaN
|
|||
|
|
const isWrap = style.flexWrap !== Wrap.NoWrap
|
|||
|
|
const gapCross = resolveGap(
|
|||
|
|
style,
|
|||
|
|
isMainRow ? Gutter.Row : Gutter.Column,
|
|||
|
|
innerCrossSize,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// STEP 1: Compute flex-basis for each flow child and break into lines.
|
|||
|
|
// Single-line (NoWrap) containers always get one line; multi-line containers
|
|||
|
|
// break when accumulated basis+margin+gap exceeds innerMainSize.
|
|||
|
|
for (const c of flowChildren) {
|
|||
|
|
c._flexBasis = computeFlexBasis(
|
|||
|
|
c,
|
|||
|
|
mainAxis,
|
|||
|
|
innerMainSize,
|
|||
|
|
innerCrossSize,
|
|||
|
|
crossMode,
|
|||
|
|
ownerW,
|
|||
|
|
ownerH,
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
const lines: Node[][] = []
|
|||
|
|
if (!isWrap || !isDefined(innerMainSize) || flowChildren.length === 0) {
|
|||
|
|
for (const c of flowChildren) c._lineIndex = 0
|
|||
|
|
lines.push(flowChildren)
|
|||
|
|
} else {
|
|||
|
|
// Line-break decisions use the min/max-clamped basis (flexbox spec §9.3.5:
|
|||
|
|
// "hypothetical main size"), not the raw flex-basis.
|
|||
|
|
let lineStart = 0
|
|||
|
|
let lineLen = 0
|
|||
|
|
for (let i = 0; i < flowChildren.length; i++) {
|
|||
|
|
const c = flowChildren[i]!
|
|||
|
|
const hypo = boundAxis(c.style, isMainRow, c._flexBasis, ownerW, ownerH)
|
|||
|
|
const outer = Math.max(0, hypo) + childMarginForAxis(c, mainAxis, ownerW)
|
|||
|
|
const withGap = i > lineStart ? gapMain : 0
|
|||
|
|
if (i > lineStart && lineLen + withGap + outer > innerMainSize) {
|
|||
|
|
lines.push(flowChildren.slice(lineStart, i))
|
|||
|
|
lineStart = i
|
|||
|
|
lineLen = outer
|
|||
|
|
} else {
|
|||
|
|
lineLen += withGap + outer
|
|||
|
|
}
|
|||
|
|
c._lineIndex = lines.length
|
|||
|
|
}
|
|||
|
|
lines.push(flowChildren.slice(lineStart))
|
|||
|
|
}
|
|||
|
|
const lineCount = lines.length
|
|||
|
|
const isBaseline = isBaselineLayout(node, flowChildren)
|
|||
|
|
|
|||
|
|
// STEP 2+3: For each line, resolve flexible lengths and lay out children to
|
|||
|
|
// measure cross sizes. Track per-line consumed main and max cross.
|
|||
|
|
const lineConsumedMain: number[] = new Array(lineCount)
|
|||
|
|
const lineCrossSizes: number[] = new Array(lineCount)
|
|||
|
|
// Baseline layout tracks max ascent (baseline + leading margin) per line so
|
|||
|
|
// baseline-aligned items can be positioned at maxAscent - childBaseline.
|
|||
|
|
const lineMaxAscent: number[] = isBaseline ? new Array(lineCount).fill(0) : []
|
|||
|
|
let maxLineMain = 0
|
|||
|
|
let totalLinesCross = 0
|
|||
|
|
for (let li = 0; li < lineCount; li++) {
|
|||
|
|
const line = lines[li]!
|
|||
|
|
const lineGap = line.length > 1 ? gapMain * (line.length - 1) : 0
|
|||
|
|
let lineBasis = lineGap
|
|||
|
|
for (const c of line) {
|
|||
|
|
lineBasis += c._flexBasis + childMarginForAxis(c, mainAxis, ownerW)
|
|||
|
|
}
|
|||
|
|
// Resolve flexible lengths against available inner main. For indefinite
|
|||
|
|
// containers with min/max, flex against the clamped size.
|
|||
|
|
let availMain = innerMainSize
|
|||
|
|
if (!isDefined(availMain)) {
|
|||
|
|
const mainOwner = isMainRow ? ownerWidth : ownerHeight
|
|||
|
|
const minM = resolveValue(
|
|||
|
|
isMainRow ? style.minWidth : style.minHeight,
|
|||
|
|
mainOwner,
|
|||
|
|
)
|
|||
|
|
const maxM = resolveValue(
|
|||
|
|
isMainRow ? style.maxWidth : style.maxHeight,
|
|||
|
|
mainOwner,
|
|||
|
|
)
|
|||
|
|
if (isDefined(maxM) && lineBasis > maxM - mainPadBorder) {
|
|||
|
|
availMain = Math.max(0, maxM - mainPadBorder)
|
|||
|
|
} else if (isDefined(minM) && lineBasis < minM - mainPadBorder) {
|
|||
|
|
availMain = Math.max(0, minM - mainPadBorder)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
resolveFlexibleLengths(
|
|||
|
|
line,
|
|||
|
|
availMain,
|
|||
|
|
lineBasis,
|
|||
|
|
isMainRow,
|
|||
|
|
ownerW,
|
|||
|
|
ownerH,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Lay out each child in this line to measure cross
|
|||
|
|
let lineCross = 0
|
|||
|
|
for (const c of line) {
|
|||
|
|
const cStyle = c.style
|
|||
|
|
const childAlign =
|
|||
|
|
cStyle.alignSelf === Align.Auto ? style.alignItems : cStyle.alignSelf
|
|||
|
|
const cMarginCross = childMarginForAxis(c, crossAx, ownerW)
|
|||
|
|
let childCrossSize = NaN
|
|||
|
|
let childCrossMode: MeasureMode = MeasureMode.Undefined
|
|||
|
|
const resolvedCrossStyle = resolveValue(
|
|||
|
|
isMainRow ? cStyle.height : cStyle.width,
|
|||
|
|
isMainRow ? ownerH : ownerW,
|
|||
|
|
)
|
|||
|
|
const crossLeadE = isMainRow ? EDGE_TOP : EDGE_LEFT
|
|||
|
|
const crossTrailE = isMainRow ? EDGE_BOTTOM : EDGE_RIGHT
|
|||
|
|
const hasCrossAutoMargin =
|
|||
|
|
c._hasAutoMargin &&
|
|||
|
|
(isMarginAuto(cStyle.margin, crossLeadE) ||
|
|||
|
|
isMarginAuto(cStyle.margin, crossTrailE))
|
|||
|
|
// Single-line stretch goes directly to the container cross size.
|
|||
|
|
// Multi-line wrap measures intrinsic cross (Undefined mode) so
|
|||
|
|
// flex-grow grandchildren don't expand to the container — the line
|
|||
|
|
// cross size is determined first, then items are re-stretched.
|
|||
|
|
if (isDefined(resolvedCrossStyle)) {
|
|||
|
|
childCrossSize = resolvedCrossStyle
|
|||
|
|
childCrossMode = MeasureMode.Exactly
|
|||
|
|
} else if (
|
|||
|
|
childAlign === Align.Stretch &&
|
|||
|
|
!hasCrossAutoMargin &&
|
|||
|
|
!isWrap &&
|
|||
|
|
isDefined(innerCrossSize) &&
|
|||
|
|
crossMode === MeasureMode.Exactly
|
|||
|
|
) {
|
|||
|
|
childCrossSize = Math.max(0, innerCrossSize - cMarginCross)
|
|||
|
|
childCrossMode = MeasureMode.Exactly
|
|||
|
|
} else if (!isWrap && isDefined(innerCrossSize)) {
|
|||
|
|
childCrossSize = Math.max(0, innerCrossSize - cMarginCross)
|
|||
|
|
childCrossMode = MeasureMode.AtMost
|
|||
|
|
}
|
|||
|
|
const cw = isMainRow ? c._mainSize : childCrossSize
|
|||
|
|
const ch = isMainRow ? childCrossSize : c._mainSize
|
|||
|
|
layoutNode(
|
|||
|
|
c,
|
|||
|
|
cw,
|
|||
|
|
ch,
|
|||
|
|
isMainRow ? MeasureMode.Exactly : childCrossMode,
|
|||
|
|
isMainRow ? childCrossMode : MeasureMode.Exactly,
|
|||
|
|
ownerW,
|
|||
|
|
ownerH,
|
|||
|
|
performLayout,
|
|||
|
|
isMainRow,
|
|||
|
|
!isMainRow,
|
|||
|
|
)
|
|||
|
|
c._crossSize = isMainRow ? c.layout.height : c.layout.width
|
|||
|
|
lineCross = Math.max(lineCross, c._crossSize + cMarginCross)
|
|||
|
|
}
|
|||
|
|
// Baseline layout: line cross size must fit maxAscent + maxDescent of
|
|||
|
|
// baseline-aligned children (yoga STEP 8). Only applies to row direction.
|
|||
|
|
if (isBaseline) {
|
|||
|
|
let maxAscent = 0
|
|||
|
|
let maxDescent = 0
|
|||
|
|
for (const c of line) {
|
|||
|
|
if (resolveChildAlign(node, c) !== Align.Baseline) continue
|
|||
|
|
const mTop = resolveEdge(c.style.margin, EDGE_TOP, ownerW)
|
|||
|
|
const mBot = resolveEdge(c.style.margin, EDGE_BOTTOM, ownerW)
|
|||
|
|
const ascent = calculateBaseline(c) + mTop
|
|||
|
|
const descent = c.layout.height + mTop + mBot - ascent
|
|||
|
|
if (ascent > maxAscent) maxAscent = ascent
|
|||
|
|
if (descent > maxDescent) maxDescent = descent
|
|||
|
|
}
|
|||
|
|
lineMaxAscent[li] = maxAscent
|
|||
|
|
if (maxAscent + maxDescent > lineCross) {
|
|||
|
|
lineCross = maxAscent + maxDescent
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// layoutNode(c) at line ~1117 above already resolved c.layout.margin[] via
|
|||
|
|
// resolveEdges4Into with the same ownerW — read directly instead of
|
|||
|
|
// re-resolving through childMarginForAxis → 2× resolveEdge.
|
|||
|
|
const mainLead = leadingEdge(mainAxis)
|
|||
|
|
const mainTrail = trailingEdge(mainAxis)
|
|||
|
|
let consumed = lineGap
|
|||
|
|
for (const c of line) {
|
|||
|
|
const cm = c.layout.margin
|
|||
|
|
consumed += c._mainSize + cm[mainLead]! + cm[mainTrail]!
|
|||
|
|
}
|
|||
|
|
lineConsumedMain[li] = consumed
|
|||
|
|
lineCrossSizes[li] = lineCross
|
|||
|
|
maxLineMain = Math.max(maxLineMain, consumed)
|
|||
|
|
totalLinesCross += lineCross
|
|||
|
|
}
|
|||
|
|
const totalCrossGap = lineCount > 1 ? gapCross * (lineCount - 1) : 0
|
|||
|
|
totalLinesCross += totalCrossGap
|
|||
|
|
|
|||
|
|
// STEP 4: Determine container dimensions. Per yoga's STEP 9, for both
|
|||
|
|
// AtMost (FitContent) and Undefined (MaxContent) the node sizes to its
|
|||
|
|
// content — AtMost is NOT a hard clamp, items may overflow the available
|
|||
|
|
// space (CSS "fit-content" behavior). Only Scroll overflow clamps to the
|
|||
|
|
// available size. Wrap containers that broke into multiple lines under
|
|||
|
|
// AtMost fill the available main size since they wrapped at that boundary.
|
|||
|
|
const isScroll = style.overflow === Overflow.Scroll
|
|||
|
|
const contentMain = maxLineMain + mainPadBorder
|
|||
|
|
const finalMainSize =
|
|||
|
|
mainMode === MeasureMode.Exactly
|
|||
|
|
? mainSize
|
|||
|
|
: mainMode === MeasureMode.AtMost && isScroll
|
|||
|
|
? Math.max(Math.min(mainSize, contentMain), mainPadBorder)
|
|||
|
|
: isWrap && lineCount > 1 && mainMode === MeasureMode.AtMost
|
|||
|
|
? mainSize
|
|||
|
|
: contentMain
|
|||
|
|
const contentCross = totalLinesCross + crossPadBorder
|
|||
|
|
const finalCrossSize =
|
|||
|
|
crossMode === MeasureMode.Exactly
|
|||
|
|
? crossSize
|
|||
|
|
: crossMode === MeasureMode.AtMost && isScroll
|
|||
|
|
? Math.max(Math.min(crossSize, contentCross), crossPadBorder)
|
|||
|
|
: contentCross
|
|||
|
|
node.layout.width = boundAxis(
|
|||
|
|
style,
|
|||
|
|
true,
|
|||
|
|
isMainRow ? finalMainSize : finalCrossSize,
|
|||
|
|
ownerWidth,
|
|||
|
|
ownerHeight,
|
|||
|
|
)
|
|||
|
|
node.layout.height = boundAxis(
|
|||
|
|
style,
|
|||
|
|
false,
|
|||
|
|
isMainRow ? finalCrossSize : finalMainSize,
|
|||
|
|
ownerWidth,
|
|||
|
|
ownerHeight,
|
|||
|
|
)
|
|||
|
|
commitCacheOutputs(node, performLayout)
|
|||
|
|
// Write cache even for dirty nodes — fresh-mounted items during virtual scroll
|
|||
|
|
cacheWrite(
|
|||
|
|
node,
|
|||
|
|
availableWidth,
|
|||
|
|
availableHeight,
|
|||
|
|
widthMode,
|
|||
|
|
heightMode,
|
|||
|
|
ownerWidth,
|
|||
|
|
ownerHeight,
|
|||
|
|
forceWidth,
|
|||
|
|
forceHeight,
|
|||
|
|
wasDirty,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if (!performLayout) return
|
|||
|
|
|
|||
|
|
// STEP 5: Position lines (align-content) and children (justify-content +
|
|||
|
|
// align-items + auto margins).
|
|||
|
|
const actualInnerMain =
|
|||
|
|
(isMainRow ? node.layout.width : node.layout.height) - mainPadBorder
|
|||
|
|
const actualInnerCross =
|
|||
|
|
(isMainRow ? node.layout.height : node.layout.width) - crossPadBorder
|
|||
|
|
const mainLeadEdgePhys = leadingEdge(mainAxis)
|
|||
|
|
const mainTrailEdgePhys = trailingEdge(mainAxis)
|
|||
|
|
const crossLeadEdgePhys = isMainRow ? EDGE_TOP : EDGE_LEFT
|
|||
|
|
const crossTrailEdgePhys = isMainRow ? EDGE_BOTTOM : EDGE_RIGHT
|
|||
|
|
const reversed = isReverse(mainAxis)
|
|||
|
|
const mainContainerSize = isMainRow ? node.layout.width : node.layout.height
|
|||
|
|
const crossLead = pad[crossLeadEdgePhys]! + bor[crossLeadEdgePhys]!
|
|||
|
|
|
|||
|
|
// Align-content: distribute free cross space among lines. Single-line
|
|||
|
|
// containers use the full cross size for the one line (align-items handles
|
|||
|
|
// positioning within it).
|
|||
|
|
let lineCrossOffset = crossLead
|
|||
|
|
let betweenLines = gapCross
|
|||
|
|
const freeCross = actualInnerCross - totalLinesCross
|
|||
|
|
if (lineCount === 1 && !isWrap && !isBaseline) {
|
|||
|
|
lineCrossSizes[0] = actualInnerCross
|
|||
|
|
} else {
|
|||
|
|
const remCross = Math.max(0, freeCross)
|
|||
|
|
switch (style.alignContent) {
|
|||
|
|
case Align.FlexStart:
|
|||
|
|
break
|
|||
|
|
case Align.Center:
|
|||
|
|
lineCrossOffset += freeCross / 2
|
|||
|
|
break
|
|||
|
|
case Align.FlexEnd:
|
|||
|
|
lineCrossOffset += freeCross
|
|||
|
|
break
|
|||
|
|
case Align.Stretch:
|
|||
|
|
if (lineCount > 0 && remCross > 0) {
|
|||
|
|
const add = remCross / lineCount
|
|||
|
|
for (let i = 0; i < lineCount; i++) lineCrossSizes[i]! += add
|
|||
|
|
}
|
|||
|
|
break
|
|||
|
|
case Align.SpaceBetween:
|
|||
|
|
if (lineCount > 1) betweenLines += remCross / (lineCount - 1)
|
|||
|
|
break
|
|||
|
|
case Align.SpaceAround:
|
|||
|
|
if (lineCount > 0) {
|
|||
|
|
betweenLines += remCross / lineCount
|
|||
|
|
lineCrossOffset += remCross / lineCount / 2
|
|||
|
|
}
|
|||
|
|
break
|
|||
|
|
case Align.SpaceEvenly:
|
|||
|
|
if (lineCount > 0) {
|
|||
|
|
betweenLines += remCross / (lineCount + 1)
|
|||
|
|
lineCrossOffset += remCross / (lineCount + 1)
|
|||
|
|
}
|
|||
|
|
break
|
|||
|
|
default:
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// For wrap-reverse, lines stack from the trailing cross edge. Walk lines in
|
|||
|
|
// order but flip the cross position within the container.
|
|||
|
|
const wrapReverse = style.flexWrap === Wrap.WrapReverse
|
|||
|
|
const crossContainerSize = isMainRow ? node.layout.height : node.layout.width
|
|||
|
|
let lineCrossPos = lineCrossOffset
|
|||
|
|
for (let li = 0; li < lineCount; li++) {
|
|||
|
|
const line = lines[li]!
|
|||
|
|
const lineCross = lineCrossSizes[li]!
|
|||
|
|
const consumedMain = lineConsumedMain[li]!
|
|||
|
|
const n = line.length
|
|||
|
|
|
|||
|
|
// Re-stretch children whose cross is auto and align is stretch, now that
|
|||
|
|
// the line cross size is known. Needed for multi-line wrap (line cross
|
|||
|
|
// wasn't known during initial measure) AND single-line when the container
|
|||
|
|
// cross was not Exactly (initial stretch at ~line 1250 was skipped because
|
|||
|
|
// innerCrossSize wasn't defined — the container sized to max child cross).
|
|||
|
|
if (isWrap || crossMode !== MeasureMode.Exactly) {
|
|||
|
|
for (const c of line) {
|
|||
|
|
const cStyle = c.style
|
|||
|
|
const childAlign =
|
|||
|
|
cStyle.alignSelf === Align.Auto ? style.alignItems : cStyle.alignSelf
|
|||
|
|
const crossStyleDef = isDefined(
|
|||
|
|
resolveValue(
|
|||
|
|
isMainRow ? cStyle.height : cStyle.width,
|
|||
|
|
isMainRow ? ownerH : ownerW,
|
|||
|
|
),
|
|||
|
|
)
|
|||
|
|
const hasCrossAutoMargin =
|
|||
|
|
c._hasAutoMargin &&
|
|||
|
|
(isMarginAuto(cStyle.margin, crossLeadEdgePhys) ||
|
|||
|
|
isMarginAuto(cStyle.margin, crossTrailEdgePhys))
|
|||
|
|
if (
|
|||
|
|
childAlign === Align.Stretch &&
|
|||
|
|
!crossStyleDef &&
|
|||
|
|
!hasCrossAutoMargin
|
|||
|
|
) {
|
|||
|
|
const cMarginCross = childMarginForAxis(c, crossAx, ownerW)
|
|||
|
|
const target = Math.max(0, lineCross - cMarginCross)
|
|||
|
|
if (c._crossSize !== target) {
|
|||
|
|
const cw = isMainRow ? c._mainSize : target
|
|||
|
|
const ch = isMainRow ? target : c._mainSize
|
|||
|
|
layoutNode(
|
|||
|
|
c,
|
|||
|
|
cw,
|
|||
|
|
ch,
|
|||
|
|
MeasureMode.Exactly,
|
|||
|
|
MeasureMode.Exactly,
|
|||
|
|
ownerW,
|
|||
|
|
ownerH,
|
|||
|
|
performLayout,
|
|||
|
|
isMainRow,
|
|||
|
|
!isMainRow,
|
|||
|
|
)
|
|||
|
|
c._crossSize = target
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Justify-content + auto margins for this line
|
|||
|
|
let mainOffset = pad[mainLeadEdgePhys]! + bor[mainLeadEdgePhys]!
|
|||
|
|
let betweenMain = gapMain
|
|||
|
|
let numAutoMarginsMain = 0
|
|||
|
|
for (const c of line) {
|
|||
|
|
if (!c._hasAutoMargin) continue
|
|||
|
|
if (isMarginAuto(c.style.margin, mainLeadEdgePhys)) numAutoMarginsMain++
|
|||
|
|
if (isMarginAuto(c.style.margin, mainTrailEdgePhys)) numAutoMarginsMain++
|
|||
|
|
}
|
|||
|
|
const freeMain = actualInnerMain - consumedMain
|
|||
|
|
const remainingMain = Math.max(0, freeMain)
|
|||
|
|
const autoMarginMainSize =
|
|||
|
|
numAutoMarginsMain > 0 && remainingMain > 0
|
|||
|
|
? remainingMain / numAutoMarginsMain
|
|||
|
|
: 0
|
|||
|
|
if (numAutoMarginsMain === 0) {
|
|||
|
|
switch (style.justifyContent) {
|
|||
|
|
case Justify.FlexStart:
|
|||
|
|
break
|
|||
|
|
case Justify.Center:
|
|||
|
|
mainOffset += freeMain / 2
|
|||
|
|
break
|
|||
|
|
case Justify.FlexEnd:
|
|||
|
|
mainOffset += freeMain
|
|||
|
|
break
|
|||
|
|
case Justify.SpaceBetween:
|
|||
|
|
if (n > 1) betweenMain += remainingMain / (n - 1)
|
|||
|
|
break
|
|||
|
|
case Justify.SpaceAround:
|
|||
|
|
if (n > 0) {
|
|||
|
|
betweenMain += remainingMain / n
|
|||
|
|
mainOffset += remainingMain / n / 2
|
|||
|
|
}
|
|||
|
|
break
|
|||
|
|
case Justify.SpaceEvenly:
|
|||
|
|
if (n > 0) {
|
|||
|
|
betweenMain += remainingMain / (n + 1)
|
|||
|
|
mainOffset += remainingMain / (n + 1)
|
|||
|
|
}
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const effectiveLineCrossPos = wrapReverse
|
|||
|
|
? crossContainerSize - lineCrossPos - lineCross
|
|||
|
|
: lineCrossPos
|
|||
|
|
|
|||
|
|
let pos = mainOffset
|
|||
|
|
for (const c of line) {
|
|||
|
|
const cMargin = c.style.margin
|
|||
|
|
// c.layout.margin[] was populated by resolveEdges4Into inside the
|
|||
|
|
// layoutNode(c) call above (same ownerW). Read resolved values directly
|
|||
|
|
// instead of re-running the edge fallback chain 4× via resolveEdge.
|
|||
|
|
// Auto margins resolve to 0 in layout.margin, so autoMarginMainSize
|
|||
|
|
// substitution still uses the isMarginAuto check against style.
|
|||
|
|
const cLayoutMargin = c.layout.margin
|
|||
|
|
let autoMainLead = false
|
|||
|
|
let autoMainTrail = false
|
|||
|
|
let autoCrossLead = false
|
|||
|
|
let autoCrossTrail = false
|
|||
|
|
let mMainLead: number
|
|||
|
|
let mMainTrail: number
|
|||
|
|
let mCrossLead: number
|
|||
|
|
let mCrossTrail: number
|
|||
|
|
if (c._hasAutoMargin) {
|
|||
|
|
autoMainLead = isMarginAuto(cMargin, mainLeadEdgePhys)
|
|||
|
|
autoMainTrail = isMarginAuto(cMargin, mainTrailEdgePhys)
|
|||
|
|
autoCrossLead = isMarginAuto(cMargin, crossLeadEdgePhys)
|
|||
|
|
autoCrossTrail = isMarginAuto(cMargin, crossTrailEdgePhys)
|
|||
|
|
mMainLead = autoMainLead
|
|||
|
|
? autoMarginMainSize
|
|||
|
|
: cLayoutMargin[mainLeadEdgePhys]!
|
|||
|
|
mMainTrail = autoMainTrail
|
|||
|
|
? autoMarginMainSize
|
|||
|
|
: cLayoutMargin[mainTrailEdgePhys]!
|
|||
|
|
mCrossLead = autoCrossLead ? 0 : cLayoutMargin[crossLeadEdgePhys]!
|
|||
|
|
mCrossTrail = autoCrossTrail ? 0 : cLayoutMargin[crossTrailEdgePhys]!
|
|||
|
|
} else {
|
|||
|
|
// Fast path: no auto margins — read resolved values directly.
|
|||
|
|
mMainLead = cLayoutMargin[mainLeadEdgePhys]!
|
|||
|
|
mMainTrail = cLayoutMargin[mainTrailEdgePhys]!
|
|||
|
|
mCrossLead = cLayoutMargin[crossLeadEdgePhys]!
|
|||
|
|
mCrossTrail = cLayoutMargin[crossTrailEdgePhys]!
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mainPos = reversed
|
|||
|
|
? mainContainerSize - (pos + mMainLead) - c._mainSize
|
|||
|
|
: pos + mMainLead
|
|||
|
|
|
|||
|
|
const childAlign =
|
|||
|
|
c.style.alignSelf === Align.Auto ? style.alignItems : c.style.alignSelf
|
|||
|
|
let crossPos = effectiveLineCrossPos + mCrossLead
|
|||
|
|
const crossFree = lineCross - c._crossSize - mCrossLead - mCrossTrail
|
|||
|
|
if (autoCrossLead && autoCrossTrail) {
|
|||
|
|
crossPos += Math.max(0, crossFree) / 2
|
|||
|
|
} else if (autoCrossLead) {
|
|||
|
|
crossPos += Math.max(0, crossFree)
|
|||
|
|
} else if (autoCrossTrail) {
|
|||
|
|
// stays at leading
|
|||
|
|
} else {
|
|||
|
|
switch (childAlign) {
|
|||
|
|
case Align.FlexStart:
|
|||
|
|
case Align.Stretch:
|
|||
|
|
if (wrapReverse) crossPos += crossFree
|
|||
|
|
break
|
|||
|
|
case Align.Center:
|
|||
|
|
crossPos += crossFree / 2
|
|||
|
|
break
|
|||
|
|
case Align.FlexEnd:
|
|||
|
|
if (!wrapReverse) crossPos += crossFree
|
|||
|
|
break
|
|||
|
|
case Align.Baseline:
|
|||
|
|
// Row direction only (isBaselineLayout checked this). Position so
|
|||
|
|
// the child's baseline aligns with the line's max ascent. Per
|
|||
|
|
// yoga: top = currentLead + maxAscent - childBaseline + leadingPosition.
|
|||
|
|
if (isBaseline) {
|
|||
|
|
crossPos =
|
|||
|
|
effectiveLineCrossPos +
|
|||
|
|
lineMaxAscent[li]! -
|
|||
|
|
calculateBaseline(c)
|
|||
|
|
}
|
|||
|
|
break
|
|||
|
|
default:
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Relative position offsets. Fast path: no position insets set →
|
|||
|
|
// skip 4× resolveEdgeRaw + 4× resolveValue + 4× isDefined.
|
|||
|
|
let relX = 0
|
|||
|
|
let relY = 0
|
|||
|
|
if (c._hasPosition) {
|
|||
|
|
const relLeft = resolveValue(
|
|||
|
|
resolveEdgeRaw(c.style.position, EDGE_LEFT),
|
|||
|
|
ownerW,
|
|||
|
|
)
|
|||
|
|
const relRight = resolveValue(
|
|||
|
|
resolveEdgeRaw(c.style.position, EDGE_RIGHT),
|
|||
|
|
ownerW,
|
|||
|
|
)
|
|||
|
|
const relTop = resolveValue(
|
|||
|
|
resolveEdgeRaw(c.style.position, EDGE_TOP),
|
|||
|
|
ownerW,
|
|||
|
|
)
|
|||
|
|
const relBottom = resolveValue(
|
|||
|
|
resolveEdgeRaw(c.style.position, EDGE_BOTTOM),
|
|||
|
|
ownerW,
|
|||
|
|
)
|
|||
|
|
relX = isDefined(relLeft)
|
|||
|
|
? relLeft
|
|||
|
|
: isDefined(relRight)
|
|||
|
|
? -relRight
|
|||
|
|
: 0
|
|||
|
|
relY = isDefined(relTop)
|
|||
|
|
? relTop
|
|||
|
|
: isDefined(relBottom)
|
|||
|
|
? -relBottom
|
|||
|
|
: 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (isMainRow) {
|
|||
|
|
c.layout.left = mainPos + relX
|
|||
|
|
c.layout.top = crossPos + relY
|
|||
|
|
} else {
|
|||
|
|
c.layout.left = crossPos + relX
|
|||
|
|
c.layout.top = mainPos + relY
|
|||
|
|
}
|
|||
|
|
pos += c._mainSize + mMainLead + mMainTrail + betweenMain
|
|||
|
|
}
|
|||
|
|
lineCrossPos += lineCross + betweenLines
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// STEP 6: Absolute-positioned children
|
|||
|
|
for (const c of absChildren) {
|
|||
|
|
layoutAbsoluteChild(
|
|||
|
|
node,
|
|||
|
|
c,
|
|||
|
|
node.layout.width,
|
|||
|
|
node.layout.height,
|
|||
|
|
pad,
|
|||
|
|
bor,
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function layoutAbsoluteChild(
|
|||
|
|
parent: Node,
|
|||
|
|
child: Node,
|
|||
|
|
parentWidth: number,
|
|||
|
|
parentHeight: number,
|
|||
|
|
pad: [number, number, number, number],
|
|||
|
|
bor: [number, number, number, number],
|
|||
|
|
): void {
|
|||
|
|
const cs = child.style
|
|||
|
|
const posLeft = resolveEdgeRaw(cs.position, EDGE_LEFT)
|
|||
|
|
const posRight = resolveEdgeRaw(cs.position, EDGE_RIGHT)
|
|||
|
|
const posTop = resolveEdgeRaw(cs.position, EDGE_TOP)
|
|||
|
|
const posBottom = resolveEdgeRaw(cs.position, EDGE_BOTTOM)
|
|||
|
|
|
|||
|
|
const rLeft = resolveValue(posLeft, parentWidth)
|
|||
|
|
const rRight = resolveValue(posRight, parentWidth)
|
|||
|
|
const rTop = resolveValue(posTop, parentHeight)
|
|||
|
|
const rBottom = resolveValue(posBottom, parentHeight)
|
|||
|
|
|
|||
|
|
// Absolute children's percentage dimensions resolve against the containing
|
|||
|
|
// block's padding-box (parent size minus border), per CSS §10.1.
|
|||
|
|
const paddingBoxW = parentWidth - bor[0] - bor[2]
|
|||
|
|
const paddingBoxH = parentHeight - bor[1] - bor[3]
|
|||
|
|
let cw = resolveValue(cs.width, paddingBoxW)
|
|||
|
|
let ch = resolveValue(cs.height, paddingBoxH)
|
|||
|
|
|
|||
|
|
// If both left+right defined and width not, derive width
|
|||
|
|
if (!isDefined(cw) && isDefined(rLeft) && isDefined(rRight)) {
|
|||
|
|
cw = paddingBoxW - rLeft - rRight
|
|||
|
|
}
|
|||
|
|
if (!isDefined(ch) && isDefined(rTop) && isDefined(rBottom)) {
|
|||
|
|
ch = paddingBoxH - rTop - rBottom
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
layoutNode(
|
|||
|
|
child,
|
|||
|
|
cw,
|
|||
|
|
ch,
|
|||
|
|
isDefined(cw) ? MeasureMode.Exactly : MeasureMode.Undefined,
|
|||
|
|
isDefined(ch) ? MeasureMode.Exactly : MeasureMode.Undefined,
|
|||
|
|
paddingBoxW,
|
|||
|
|
paddingBoxH,
|
|||
|
|
true,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Margin of absolute child (applied in addition to insets)
|
|||
|
|
const mL = resolveEdge(cs.margin, EDGE_LEFT, parentWidth)
|
|||
|
|
const mT = resolveEdge(cs.margin, EDGE_TOP, parentWidth)
|
|||
|
|
const mR = resolveEdge(cs.margin, EDGE_RIGHT, parentWidth)
|
|||
|
|
const mB = resolveEdge(cs.margin, EDGE_BOTTOM, parentWidth)
|
|||
|
|
|
|||
|
|
const mainAxis = parent.style.flexDirection
|
|||
|
|
const reversed = isReverse(mainAxis)
|
|||
|
|
const mainRow = isRow(mainAxis)
|
|||
|
|
const wrapReverse = parent.style.flexWrap === Wrap.WrapReverse
|
|||
|
|
// alignSelf overrides alignItems for absolute children (same as flow items)
|
|||
|
|
const alignment =
|
|||
|
|
cs.alignSelf === Align.Auto ? parent.style.alignItems : cs.alignSelf
|
|||
|
|
|
|||
|
|
// Position
|
|||
|
|
let left: number
|
|||
|
|
if (isDefined(rLeft)) {
|
|||
|
|
left = bor[0] + rLeft + mL
|
|||
|
|
} else if (isDefined(rRight)) {
|
|||
|
|
left = parentWidth - bor[2] - rRight - child.layout.width - mR
|
|||
|
|
} else if (mainRow) {
|
|||
|
|
// Main axis — justify-content, flipped for reversed
|
|||
|
|
const lead = pad[0] + bor[0]
|
|||
|
|
const trail = parentWidth - pad[2] - bor[2]
|
|||
|
|
left = reversed
|
|||
|
|
? trail - child.layout.width - mR
|
|||
|
|
: justifyAbsolute(
|
|||
|
|
parent.style.justifyContent,
|
|||
|
|
lead,
|
|||
|
|
trail,
|
|||
|
|
child.layout.width,
|
|||
|
|
) + mL
|
|||
|
|
} else {
|
|||
|
|
left =
|
|||
|
|
alignAbsolute(
|
|||
|
|
alignment,
|
|||
|
|
pad[0] + bor[0],
|
|||
|
|
parentWidth - pad[2] - bor[2],
|
|||
|
|
child.layout.width,
|
|||
|
|
wrapReverse,
|
|||
|
|
) + mL
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let top: number
|
|||
|
|
if (isDefined(rTop)) {
|
|||
|
|
top = bor[1] + rTop + mT
|
|||
|
|
} else if (isDefined(rBottom)) {
|
|||
|
|
top = parentHeight - bor[3] - rBottom - child.layout.height - mB
|
|||
|
|
} else if (mainRow) {
|
|||
|
|
top =
|
|||
|
|
alignAbsolute(
|
|||
|
|
alignment,
|
|||
|
|
pad[1] + bor[1],
|
|||
|
|
parentHeight - pad[3] - bor[3],
|
|||
|
|
child.layout.height,
|
|||
|
|
wrapReverse,
|
|||
|
|
) + mT
|
|||
|
|
} else {
|
|||
|
|
const lead = pad[1] + bor[1]
|
|||
|
|
const trail = parentHeight - pad[3] - bor[3]
|
|||
|
|
top = reversed
|
|||
|
|
? trail - child.layout.height - mB
|
|||
|
|
: justifyAbsolute(
|
|||
|
|
parent.style.justifyContent,
|
|||
|
|
lead,
|
|||
|
|
trail,
|
|||
|
|
child.layout.height,
|
|||
|
|
) + mT
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
child.layout.left = left
|
|||
|
|
child.layout.top = top
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function justifyAbsolute(
|
|||
|
|
justify: Justify,
|
|||
|
|
leadEdge: number,
|
|||
|
|
trailEdge: number,
|
|||
|
|
childSize: number,
|
|||
|
|
): number {
|
|||
|
|
switch (justify) {
|
|||
|
|
case Justify.Center:
|
|||
|
|
return leadEdge + (trailEdge - leadEdge - childSize) / 2
|
|||
|
|
case Justify.FlexEnd:
|
|||
|
|
return trailEdge - childSize
|
|||
|
|
default:
|
|||
|
|
return leadEdge
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function alignAbsolute(
|
|||
|
|
align: Align,
|
|||
|
|
leadEdge: number,
|
|||
|
|
trailEdge: number,
|
|||
|
|
childSize: number,
|
|||
|
|
wrapReverse: boolean,
|
|||
|
|
): number {
|
|||
|
|
// Wrap-reverse flips the cross axis: flex-start/stretch go to trailing,
|
|||
|
|
// flex-end goes to leading (yoga's absoluteLayoutChild flips the align value
|
|||
|
|
// when the containing block has wrap-reverse).
|
|||
|
|
switch (align) {
|
|||
|
|
case Align.Center:
|
|||
|
|
return leadEdge + (trailEdge - leadEdge - childSize) / 2
|
|||
|
|
case Align.FlexEnd:
|
|||
|
|
return wrapReverse ? leadEdge : trailEdge - childSize
|
|||
|
|
default:
|
|||
|
|
return wrapReverse ? trailEdge - childSize : leadEdge
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function computeFlexBasis(
|
|||
|
|
child: Node,
|
|||
|
|
mainAxis: FlexDirection,
|
|||
|
|
availableMain: number,
|
|||
|
|
availableCross: number,
|
|||
|
|
crossMode: MeasureMode,
|
|||
|
|
ownerWidth: number,
|
|||
|
|
ownerHeight: number,
|
|||
|
|
): number {
|
|||
|
|
// Same-generation cache hit: basis was computed THIS calculateLayout, so
|
|||
|
|
// it's fresh regardless of isDirty_. Covers both clean children (scrolling
|
|||
|
|
// past unchanged messages) AND fresh-mounted dirty children (virtual
|
|||
|
|
// scroll mounts new items — the dirty chain's measure→layout cascade
|
|||
|
|
// invokes this ≥2^depth times, but the child's subtree doesn't change
|
|||
|
|
// between calls within one calculateLayout). For clean children with
|
|||
|
|
// cache from a PREVIOUS generation, also hit if inputs match — isDirty_
|
|||
|
|
// gates since a dirty child's previous-gen cache is stale.
|
|||
|
|
const sameGen = child._fbGen === _generation
|
|||
|
|
if (
|
|||
|
|
(sameGen || !child.isDirty_) &&
|
|||
|
|
child._fbCrossMode === crossMode &&
|
|||
|
|
sameFloat(child._fbOwnerW, ownerWidth) &&
|
|||
|
|
sameFloat(child._fbOwnerH, ownerHeight) &&
|
|||
|
|
sameFloat(child._fbAvailMain, availableMain) &&
|
|||
|
|
sameFloat(child._fbAvailCross, availableCross)
|
|||
|
|
) {
|
|||
|
|
return child._fbBasis
|
|||
|
|
}
|
|||
|
|
const cs = child.style
|
|||
|
|
const isMainRow = isRow(mainAxis)
|
|||
|
|
|
|||
|
|
// Explicit flex-basis
|
|||
|
|
const basis = resolveValue(cs.flexBasis, availableMain)
|
|||
|
|
if (isDefined(basis)) {
|
|||
|
|
const b = Math.max(0, basis)
|
|||
|
|
child._fbBasis = b
|
|||
|
|
child._fbOwnerW = ownerWidth
|
|||
|
|
child._fbOwnerH = ownerHeight
|
|||
|
|
child._fbAvailMain = availableMain
|
|||
|
|
child._fbAvailCross = availableCross
|
|||
|
|
child._fbCrossMode = crossMode
|
|||
|
|
child._fbGen = _generation
|
|||
|
|
return b
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Style dimension on main axis
|
|||
|
|
const mainStyleDim = isMainRow ? cs.width : cs.height
|
|||
|
|
const mainOwner = isMainRow ? ownerWidth : ownerHeight
|
|||
|
|
const resolved = resolveValue(mainStyleDim, mainOwner)
|
|||
|
|
if (isDefined(resolved)) {
|
|||
|
|
const b = Math.max(0, resolved)
|
|||
|
|
child._fbBasis = b
|
|||
|
|
child._fbOwnerW = ownerWidth
|
|||
|
|
child._fbOwnerH = ownerHeight
|
|||
|
|
child._fbAvailMain = availableMain
|
|||
|
|
child._fbAvailCross = availableCross
|
|||
|
|
child._fbCrossMode = crossMode
|
|||
|
|
child._fbGen = _generation
|
|||
|
|
return b
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Need to measure the child to get its natural size
|
|||
|
|
const crossStyleDim = isMainRow ? cs.height : cs.width
|
|||
|
|
const crossOwner = isMainRow ? ownerHeight : ownerWidth
|
|||
|
|
let crossConstraint = resolveValue(crossStyleDim, crossOwner)
|
|||
|
|
let crossConstraintMode: MeasureMode = isDefined(crossConstraint)
|
|||
|
|
? MeasureMode.Exactly
|
|||
|
|
: MeasureMode.Undefined
|
|||
|
|
if (!isDefined(crossConstraint) && isDefined(availableCross)) {
|
|||
|
|
crossConstraint = availableCross
|
|||
|
|
crossConstraintMode =
|
|||
|
|
crossMode === MeasureMode.Exactly && isStretchAlign(child)
|
|||
|
|
? MeasureMode.Exactly
|
|||
|
|
: MeasureMode.AtMost
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Upstream yoga (YGNodeComputeFlexBasisForChild) passes the available inner
|
|||
|
|
// width with mode AtMost when the subtree will call a measure-func — so text
|
|||
|
|
// nodes don't report unconstrained intrinsic width as flex-basis, which
|
|||
|
|
// would force siblings to shrink and the text to wrap at the wrong width.
|
|||
|
|
// Passing Undefined here made Ink's <Text> inside <Box flexGrow={1}> get
|
|||
|
|
// width = intrinsic instead of available, dropping chars at wrap boundaries.
|
|||
|
|
//
|
|||
|
|
// Two constraints on when this applies:
|
|||
|
|
// - Width only. Height is never constrained during basis measurement —
|
|||
|
|
// column containers must measure children at natural height so
|
|||
|
|
// scrollable content can overflow (constraining height clips ScrollBox).
|
|||
|
|
// - Subtree has a measure-func. Pure layout subtrees (no measure-func)
|
|||
|
|
// with flex-grow children would grow into the AtMost constraint,
|
|||
|
|
// inflating the basis (breaks YGMinMaxDimensionTest flex_grow_in_at_most
|
|||
|
|
// where a flexGrow:1 child should stay at basis 0, not grow to 100).
|
|||
|
|
let mainConstraint = NaN
|
|||
|
|
let mainConstraintMode: MeasureMode = MeasureMode.Undefined
|
|||
|
|
if (isMainRow && isDefined(availableMain) && hasMeasureFuncInSubtree(child)) {
|
|||
|
|
mainConstraint = availableMain
|
|||
|
|
mainConstraintMode = MeasureMode.AtMost
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mw = isMainRow ? mainConstraint : crossConstraint
|
|||
|
|
const mh = isMainRow ? crossConstraint : mainConstraint
|
|||
|
|
const mwMode = isMainRow ? mainConstraintMode : crossConstraintMode
|
|||
|
|
const mhMode = isMainRow ? crossConstraintMode : mainConstraintMode
|
|||
|
|
|
|||
|
|
layoutNode(child, mw, mh, mwMode, mhMode, ownerWidth, ownerHeight, false)
|
|||
|
|
const b = isMainRow ? child.layout.width : child.layout.height
|
|||
|
|
child._fbBasis = b
|
|||
|
|
child._fbOwnerW = ownerWidth
|
|||
|
|
child._fbOwnerH = ownerHeight
|
|||
|
|
child._fbAvailMain = availableMain
|
|||
|
|
child._fbAvailCross = availableCross
|
|||
|
|
child._fbCrossMode = crossMode
|
|||
|
|
child._fbGen = _generation
|
|||
|
|
return b
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function hasMeasureFuncInSubtree(node: Node): boolean {
|
|||
|
|
if (node.measureFunc) return true
|
|||
|
|
for (const c of node.children) {
|
|||
|
|
if (hasMeasureFuncInSubtree(c)) return true
|
|||
|
|
}
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resolveFlexibleLengths(
|
|||
|
|
children: Node[],
|
|||
|
|
availableInnerMain: number,
|
|||
|
|
totalFlexBasis: number,
|
|||
|
|
isMainRow: boolean,
|
|||
|
|
ownerW: number,
|
|||
|
|
ownerH: number,
|
|||
|
|
): void {
|
|||
|
|
// Multi-pass flex distribution per CSS flexbox spec §9.7 "Resolving Flexible
|
|||
|
|
// Lengths": distribute free space, detect min/max violations, freeze all
|
|||
|
|
// violators, redistribute among unfrozen children. Repeat until stable.
|
|||
|
|
const n = children.length
|
|||
|
|
const frozen: boolean[] = new Array(n).fill(false)
|
|||
|
|
const initialFree = isDefined(availableInnerMain)
|
|||
|
|
? availableInnerMain - totalFlexBasis
|
|||
|
|
: 0
|
|||
|
|
// Freeze inflexible items at their clamped basis
|
|||
|
|
for (let i = 0; i < n; i++) {
|
|||
|
|
const c = children[i]!
|
|||
|
|
const clamped = boundAxis(c.style, isMainRow, c._flexBasis, ownerW, ownerH)
|
|||
|
|
const inflexible =
|
|||
|
|
!isDefined(availableInnerMain) ||
|
|||
|
|
(initialFree >= 0 ? c.style.flexGrow === 0 : c.style.flexShrink === 0)
|
|||
|
|
if (inflexible) {
|
|||
|
|
c._mainSize = Math.max(0, clamped)
|
|||
|
|
frozen[i] = true
|
|||
|
|
} else {
|
|||
|
|
c._mainSize = c._flexBasis
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Iteratively distribute until no violations. Free space is recomputed each
|
|||
|
|
// pass: initial free space minus the delta frozen children consumed beyond
|
|||
|
|
// (or below) their basis.
|
|||
|
|
const unclamped: number[] = new Array(n)
|
|||
|
|
for (let iter = 0; iter <= n; iter++) {
|
|||
|
|
let frozenDelta = 0
|
|||
|
|
let totalGrow = 0
|
|||
|
|
let totalShrinkScaled = 0
|
|||
|
|
let unfrozenCount = 0
|
|||
|
|
for (let i = 0; i < n; i++) {
|
|||
|
|
const c = children[i]!
|
|||
|
|
if (frozen[i]) {
|
|||
|
|
frozenDelta += c._mainSize - c._flexBasis
|
|||
|
|
} else {
|
|||
|
|
totalGrow += c.style.flexGrow
|
|||
|
|
totalShrinkScaled += c.style.flexShrink * c._flexBasis
|
|||
|
|
unfrozenCount++
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (unfrozenCount === 0) break
|
|||
|
|
let remaining = initialFree - frozenDelta
|
|||
|
|
// Spec §9.7 step 4c: if sum of flex factors < 1, only distribute
|
|||
|
|
// initialFree × sum, not the full remaining space (partial flex).
|
|||
|
|
if (remaining > 0 && totalGrow > 0 && totalGrow < 1) {
|
|||
|
|
const scaled = initialFree * totalGrow
|
|||
|
|
if (scaled < remaining) remaining = scaled
|
|||
|
|
} else if (remaining < 0 && totalShrinkScaled > 0) {
|
|||
|
|
let totalShrink = 0
|
|||
|
|
for (let i = 0; i < n; i++) {
|
|||
|
|
if (!frozen[i]) totalShrink += children[i]!.style.flexShrink
|
|||
|
|
}
|
|||
|
|
if (totalShrink < 1) {
|
|||
|
|
const scaled = initialFree * totalShrink
|
|||
|
|
if (scaled > remaining) remaining = scaled
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Compute targets + violations for all unfrozen children
|
|||
|
|
let totalViolation = 0
|
|||
|
|
for (let i = 0; i < n; i++) {
|
|||
|
|
if (frozen[i]) continue
|
|||
|
|
const c = children[i]!
|
|||
|
|
let t = c._flexBasis
|
|||
|
|
if (remaining > 0 && totalGrow > 0) {
|
|||
|
|
t += (remaining * c.style.flexGrow) / totalGrow
|
|||
|
|
} else if (remaining < 0 && totalShrinkScaled > 0) {
|
|||
|
|
t +=
|
|||
|
|
(remaining * (c.style.flexShrink * c._flexBasis)) / totalShrinkScaled
|
|||
|
|
}
|
|||
|
|
unclamped[i] = t
|
|||
|
|
const clamped = Math.max(
|
|||
|
|
0,
|
|||
|
|
boundAxis(c.style, isMainRow, t, ownerW, ownerH),
|
|||
|
|
)
|
|||
|
|
c._mainSize = clamped
|
|||
|
|
totalViolation += clamped - t
|
|||
|
|
}
|
|||
|
|
// Freeze per spec §9.7 step 5: if totalViolation is zero freeze all; if
|
|||
|
|
// positive freeze min-violators; if negative freeze max-violators.
|
|||
|
|
if (totalViolation === 0) break
|
|||
|
|
let anyFrozen = false
|
|||
|
|
for (let i = 0; i < n; i++) {
|
|||
|
|
if (frozen[i]) continue
|
|||
|
|
const v = children[i]!._mainSize - unclamped[i]!
|
|||
|
|
if ((totalViolation > 0 && v > 0) || (totalViolation < 0 && v < 0)) {
|
|||
|
|
frozen[i] = true
|
|||
|
|
anyFrozen = true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (!anyFrozen) break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function isStretchAlign(child: Node): boolean {
|
|||
|
|
const p = child.parent
|
|||
|
|
if (!p) return false
|
|||
|
|
const align =
|
|||
|
|
child.style.alignSelf === Align.Auto
|
|||
|
|
? p.style.alignItems
|
|||
|
|
: child.style.alignSelf
|
|||
|
|
return align === Align.Stretch
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resolveChildAlign(parent: Node, child: Node): Align {
|
|||
|
|
return child.style.alignSelf === Align.Auto
|
|||
|
|
? parent.style.alignItems
|
|||
|
|
: child.style.alignSelf
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Baseline of a node per CSS Flexbox §8.5 / yoga's YGBaseline. Leaf nodes
|
|||
|
|
// (no children) use their own height. Containers recurse into the first
|
|||
|
|
// baseline-aligned child on the first line (or the first flow child if none
|
|||
|
|
// are baseline-aligned), returning that child's baseline + its top offset.
|
|||
|
|
function calculateBaseline(node: Node): number {
|
|||
|
|
let baselineChild: Node | null = null
|
|||
|
|
for (const c of node.children) {
|
|||
|
|
if (c._lineIndex > 0) break
|
|||
|
|
if (c.style.positionType === PositionType.Absolute) continue
|
|||
|
|
if (c.style.display === Display.None) continue
|
|||
|
|
if (
|
|||
|
|
resolveChildAlign(node, c) === Align.Baseline ||
|
|||
|
|
c.isReferenceBaseline_
|
|||
|
|
) {
|
|||
|
|
baselineChild = c
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
if (baselineChild === null) baselineChild = c
|
|||
|
|
}
|
|||
|
|
if (baselineChild === null) return node.layout.height
|
|||
|
|
return calculateBaseline(baselineChild) + baselineChild.layout.top
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// A container uses baseline layout only for row direction, when either
|
|||
|
|
// align-items is baseline or any flow child has align-self: baseline.
|
|||
|
|
function isBaselineLayout(node: Node, flowChildren: Node[]): boolean {
|
|||
|
|
if (!isRow(node.style.flexDirection)) return false
|
|||
|
|
if (node.style.alignItems === Align.Baseline) return true
|
|||
|
|
for (const c of flowChildren) {
|
|||
|
|
if (c.style.alignSelf === Align.Baseline) return true
|
|||
|
|
}
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function childMarginForAxis(
|
|||
|
|
child: Node,
|
|||
|
|
axis: FlexDirection,
|
|||
|
|
ownerWidth: number,
|
|||
|
|
): number {
|
|||
|
|
if (!child._hasMargin) return 0
|
|||
|
|
const lead = resolveEdge(child.style.margin, leadingEdge(axis), ownerWidth)
|
|||
|
|
const trail = resolveEdge(child.style.margin, trailingEdge(axis), ownerWidth)
|
|||
|
|
return lead + trail
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resolveGap(style: Style, gutter: Gutter, ownerSize: number): number {
|
|||
|
|
let v = style.gap[gutter]!
|
|||
|
|
if (v.unit === Unit.Undefined) v = style.gap[Gutter.All]!
|
|||
|
|
const r = resolveValue(v, ownerSize)
|
|||
|
|
return isDefined(r) ? Math.max(0, r) : 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function boundAxis(
|
|||
|
|
style: Style,
|
|||
|
|
isWidth: boolean,
|
|||
|
|
value: number,
|
|||
|
|
ownerWidth: number,
|
|||
|
|
ownerHeight: number,
|
|||
|
|
): number {
|
|||
|
|
const minV = isWidth ? style.minWidth : style.minHeight
|
|||
|
|
const maxV = isWidth ? style.maxWidth : style.maxHeight
|
|||
|
|
const minU = minV.unit
|
|||
|
|
const maxU = maxV.unit
|
|||
|
|
// Fast path: no min/max constraints set. Per CPU profile this is the
|
|||
|
|
// overwhelmingly common case (~32k calls/layout on the 1000-node bench,
|
|||
|
|
// nearly all with undefined min/max) — skipping 2× resolveValue + 2× isNaN
|
|||
|
|
// that always no-op. Unit.Undefined = 0.
|
|||
|
|
if (minU === 0 && maxU === 0) return value
|
|||
|
|
const owner = isWidth ? ownerWidth : ownerHeight
|
|||
|
|
let v = value
|
|||
|
|
// Inlined resolveValue: Unit.Point=1, Unit.Percent=2. `m === m` is !isNaN.
|
|||
|
|
if (maxU === 1) {
|
|||
|
|
if (v > maxV.value) v = maxV.value
|
|||
|
|
} else if (maxU === 2) {
|
|||
|
|
const m = (maxV.value * owner) / 100
|
|||
|
|
if (m === m && v > m) v = m
|
|||
|
|
}
|
|||
|
|
if (minU === 1) {
|
|||
|
|
if (v < minV.value) v = minV.value
|
|||
|
|
} else if (minU === 2) {
|
|||
|
|
const m = (minV.value * owner) / 100
|
|||
|
|
if (m === m && v < m) v = m
|
|||
|
|
}
|
|||
|
|
return v
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function zeroLayoutRecursive(node: Node): void {
|
|||
|
|
for (const c of node.children) {
|
|||
|
|
c.layout.left = 0
|
|||
|
|
c.layout.top = 0
|
|||
|
|
c.layout.width = 0
|
|||
|
|
c.layout.height = 0
|
|||
|
|
// Invalidate layout cache — without this, unhide → calculateLayout finds
|
|||
|
|
// the child clean (!isDirty_) with _hasL intact, hits the cache at line
|
|||
|
|
// ~1086, restores stale _lOutW/_lOutH, and returns early — skipping the
|
|||
|
|
// child-positioning recursion. Grandchildren stay at (0,0,0,0) from the
|
|||
|
|
// zeroing above and render invisible. isDirty_=true also gates _cN and
|
|||
|
|
// _fbBasis via their (sameGen || !isDirty_) checks — _cGen/_fbGen freeze
|
|||
|
|
// during hide so sameGen is false on unhide.
|
|||
|
|
c.isDirty_ = true
|
|||
|
|
c._hasL = false
|
|||
|
|
c._hasM = false
|
|||
|
|
zeroLayoutRecursive(c)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function collectLayoutChildren(node: Node, flow: Node[], abs: Node[]): void {
|
|||
|
|
// Partition a node's children into flow and absolute lists, flattening
|
|||
|
|
// display:contents subtrees so their children are laid out as direct
|
|||
|
|
// children of this node (per CSS display:contents spec — the box is removed
|
|||
|
|
// from the layout tree but its children remain, lifted to the grandparent).
|
|||
|
|
for (const c of node.children) {
|
|||
|
|
const disp = c.style.display
|
|||
|
|
if (disp === Display.None) {
|
|||
|
|
c.layout.left = 0
|
|||
|
|
c.layout.top = 0
|
|||
|
|
c.layout.width = 0
|
|||
|
|
c.layout.height = 0
|
|||
|
|
zeroLayoutRecursive(c)
|
|||
|
|
} else if (disp === Display.Contents) {
|
|||
|
|
c.layout.left = 0
|
|||
|
|
c.layout.top = 0
|
|||
|
|
c.layout.width = 0
|
|||
|
|
c.layout.height = 0
|
|||
|
|
// Recurse — nested display:contents lifts all the way up. The contents
|
|||
|
|
// node's own margin/padding/position/dimensions are ignored.
|
|||
|
|
collectLayoutChildren(c, flow, abs)
|
|||
|
|
} else if (c.style.positionType === PositionType.Absolute) {
|
|||
|
|
abs.push(c)
|
|||
|
|
} else {
|
|||
|
|
flow.push(c)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function roundLayout(
|
|||
|
|
node: Node,
|
|||
|
|
scale: number,
|
|||
|
|
absLeft: number,
|
|||
|
|
absTop: number,
|
|||
|
|
): void {
|
|||
|
|
if (scale === 0) return
|
|||
|
|
const l = node.layout
|
|||
|
|
const nodeLeft = l.left
|
|||
|
|
const nodeTop = l.top
|
|||
|
|
const nodeWidth = l.width
|
|||
|
|
const nodeHeight = l.height
|
|||
|
|
|
|||
|
|
const absNodeLeft = absLeft + nodeLeft
|
|||
|
|
const absNodeTop = absTop + nodeTop
|
|||
|
|
|
|||
|
|
// Upstream YGRoundValueToPixelGrid: text nodes (has measureFunc) floor their
|
|||
|
|
// positions so wrapped text never starts past its allocated column. Width
|
|||
|
|
// uses ceil-if-fractional to avoid clipping the last glyph. Non-text nodes
|
|||
|
|
// use standard round. Matches yoga's PixelGrid.cpp — without this, justify
|
|||
|
|
// center/space-evenly positions are off-by-one vs WASM and flex-shrink
|
|||
|
|
// overflow places siblings at the wrong column.
|
|||
|
|
const isText = node.measureFunc !== null
|
|||
|
|
l.left = roundValue(nodeLeft, scale, false, isText)
|
|||
|
|
l.top = roundValue(nodeTop, scale, false, isText)
|
|||
|
|
|
|||
|
|
// Width/height rounded via absolute edges to avoid cumulative drift
|
|||
|
|
const absRight = absNodeLeft + nodeWidth
|
|||
|
|
const absBottom = absNodeTop + nodeHeight
|
|||
|
|
const hasFracW = !isWholeNumber(nodeWidth * scale)
|
|||
|
|
const hasFracH = !isWholeNumber(nodeHeight * scale)
|
|||
|
|
l.width =
|
|||
|
|
roundValue(absRight, scale, isText && hasFracW, isText && !hasFracW) -
|
|||
|
|
roundValue(absNodeLeft, scale, false, isText)
|
|||
|
|
l.height =
|
|||
|
|
roundValue(absBottom, scale, isText && hasFracH, isText && !hasFracH) -
|
|||
|
|
roundValue(absNodeTop, scale, false, isText)
|
|||
|
|
|
|||
|
|
for (const c of node.children) {
|
|||
|
|
roundLayout(c, scale, absNodeLeft, absNodeTop)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function isWholeNumber(v: number): boolean {
|
|||
|
|
const frac = v - Math.floor(v)
|
|||
|
|
return frac < 0.0001 || frac > 0.9999
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function roundValue(
|
|||
|
|
v: number,
|
|||
|
|
scale: number,
|
|||
|
|
forceCeil: boolean,
|
|||
|
|
forceFloor: boolean,
|
|||
|
|
): number {
|
|||
|
|
let scaled = v * scale
|
|||
|
|
let frac = scaled - Math.floor(scaled)
|
|||
|
|
if (frac < 0) frac += 1
|
|||
|
|
// Float-epsilon tolerance matches upstream YGDoubleEqual (1e-4)
|
|||
|
|
if (frac < 0.0001) {
|
|||
|
|
scaled = Math.floor(scaled)
|
|||
|
|
} else if (frac > 0.9999) {
|
|||
|
|
scaled = Math.ceil(scaled)
|
|||
|
|
} else if (forceCeil) {
|
|||
|
|
scaled = Math.ceil(scaled)
|
|||
|
|
} else if (forceFloor) {
|
|||
|
|
scaled = Math.floor(scaled)
|
|||
|
|
} else {
|
|||
|
|
// Round half-up (>= 0.5 goes up), per upstream
|
|||
|
|
scaled = Math.floor(scaled) + (frac >= 0.4999 ? 1 : 0)
|
|||
|
|
}
|
|||
|
|
return scaled / scale
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --
|
|||
|
|
// Helpers
|
|||
|
|
|
|||
|
|
function parseDimension(v: number | string | undefined): Value {
|
|||
|
|
if (v === undefined) return UNDEFINED_VALUE
|
|||
|
|
if (v === 'auto') return AUTO_VALUE
|
|||
|
|
if (typeof v === 'number') {
|
|||
|
|
// WASM yoga's YGFloatIsUndefined treats NaN and ±Infinity as undefined.
|
|||
|
|
// Ink passes height={Infinity} (e.g. LogSelector maxHeight default) and
|
|||
|
|
// expects it to mean "unconstrained" — storing it as a literal point value
|
|||
|
|
// makes the node height Infinity and breaks all downstream layout.
|
|||
|
|
return Number.isFinite(v) ? pointValue(v) : UNDEFINED_VALUE
|
|||
|
|
}
|
|||
|
|
if (typeof v === 'string' && v.endsWith('%')) {
|
|||
|
|
return percentValue(parseFloat(v))
|
|||
|
|
}
|
|||
|
|
const n = parseFloat(v)
|
|||
|
|
return isNaN(n) ? UNDEFINED_VALUE : pointValue(n)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function physicalEdge(edge: Edge): number {
|
|||
|
|
switch (edge) {
|
|||
|
|
case Edge.Left:
|
|||
|
|
case Edge.Start:
|
|||
|
|
return EDGE_LEFT
|
|||
|
|
case Edge.Top:
|
|||
|
|
return EDGE_TOP
|
|||
|
|
case Edge.Right:
|
|||
|
|
case Edge.End:
|
|||
|
|
return EDGE_RIGHT
|
|||
|
|
case Edge.Bottom:
|
|||
|
|
return EDGE_BOTTOM
|
|||
|
|
default:
|
|||
|
|
return EDGE_LEFT
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --
|
|||
|
|
// Module API matching yoga-layout/load
|
|||
|
|
|
|||
|
|
export type Yoga = {
|
|||
|
|
Config: {
|
|||
|
|
create(): Config
|
|||
|
|
destroy(config: Config): void
|
|||
|
|
}
|
|||
|
|
Node: {
|
|||
|
|
create(config?: Config): Node
|
|||
|
|
createDefault(): Node
|
|||
|
|
createWithConfig(config: Config): Node
|
|||
|
|
destroy(node: Node): void
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const YOGA_INSTANCE: Yoga = {
|
|||
|
|
Config: {
|
|||
|
|
create: createConfig,
|
|||
|
|
destroy() {},
|
|||
|
|
},
|
|||
|
|
Node: {
|
|||
|
|
create: (config?: Config) => new Node(config),
|
|||
|
|
createDefault: () => new Node(),
|
|||
|
|
createWithConfig: (config: Config) => new Node(config),
|
|||
|
|
destroy() {},
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function loadYoga(): Promise<Yoga> {
|
|||
|
|
return Promise.resolve(YOGA_INSTANCE)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default YOGA_INSTANCE
|