flexily 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +200 -0
- package/dist/classic/layout.d.ts +57 -0
- package/dist/classic/layout.d.ts.map +1 -0
- package/dist/classic/layout.js +1558 -0
- package/dist/classic/layout.js.map +1 -0
- package/dist/classic/node.d.ts +648 -0
- package/dist/classic/node.d.ts.map +1 -0
- package/dist/classic/node.js +1002 -0
- package/dist/classic/node.js.map +1 -0
- package/dist/constants.d.ts +58 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +70 -0
- package/dist/constants.js.map +1 -0
- package/dist/index-classic.d.ts +30 -0
- package/dist/index-classic.d.ts.map +1 -0
- package/dist/index-classic.js +57 -0
- package/dist/index-classic.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/layout-flex-lines.d.ts +77 -0
- package/dist/layout-flex-lines.d.ts.map +1 -0
- package/dist/layout-flex-lines.js +317 -0
- package/dist/layout-flex-lines.js.map +1 -0
- package/dist/layout-helpers.d.ts +48 -0
- package/dist/layout-helpers.d.ts.map +1 -0
- package/dist/layout-helpers.js +108 -0
- package/dist/layout-helpers.js.map +1 -0
- package/dist/layout-measure.d.ts +25 -0
- package/dist/layout-measure.d.ts.map +1 -0
- package/dist/layout-measure.js +231 -0
- package/dist/layout-measure.js.map +1 -0
- package/dist/layout-stats.d.ts +19 -0
- package/dist/layout-stats.d.ts.map +1 -0
- package/dist/layout-stats.js +37 -0
- package/dist/layout-stats.js.map +1 -0
- package/dist/layout-traversal.d.ts +28 -0
- package/dist/layout-traversal.d.ts.map +1 -0
- package/dist/layout-traversal.js +65 -0
- package/dist/layout-traversal.js.map +1 -0
- package/dist/layout-zero.d.ts +26 -0
- package/dist/layout-zero.d.ts.map +1 -0
- package/dist/layout-zero.js +1601 -0
- package/dist/layout-zero.js.map +1 -0
- package/dist/logger.d.ts +14 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +61 -0
- package/dist/logger.js.map +1 -0
- package/dist/node-zero.d.ts +702 -0
- package/dist/node-zero.d.ts.map +1 -0
- package/dist/node-zero.js +1268 -0
- package/dist/node-zero.js.map +1 -0
- package/dist/testing.d.ts +69 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +179 -0
- package/dist/testing.js.map +1 -0
- package/dist/trace.d.ts +74 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +191 -0
- package/dist/trace.js.map +1 -0
- package/dist/types.d.ts +170 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +43 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +41 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +197 -0
- package/dist/utils.js.map +1 -0
- package/package.json +58 -3
- package/src/CLAUDE.md +512 -0
- package/src/beorn-logger.d.ts +10 -0
- package/src/classic/layout.ts +1783 -0
- package/src/classic/node.ts +1121 -0
- package/src/constants.ts +81 -0
- package/src/index-classic.ts +110 -0
- package/src/index.ts +110 -0
- package/src/layout-flex-lines.ts +346 -0
- package/src/layout-helpers.ts +140 -0
- package/src/layout-measure.ts +259 -0
- package/src/layout-stats.ts +43 -0
- package/src/layout-traversal.ts +70 -0
- package/src/layout-zero.ts +1792 -0
- package/src/logger.ts +67 -0
- package/src/node-zero.ts +1412 -0
- package/src/testing.ts +209 -0
- package/src/trace.ts +252 -0
- package/src/types.ts +229 -0
- package/src/utils.ts +217 -0
|
@@ -0,0 +1,1601 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flexture Layout Algorithm — Main Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Core flexbox layout computation. This file contains:
|
|
5
|
+
* - computeLayout(): top-level entry point
|
|
6
|
+
* - layoutNode(): recursive layout algorithm (11 phases)
|
|
7
|
+
*
|
|
8
|
+
* Helper modules (split for maintainability, zero-allocation preserved):
|
|
9
|
+
* - layout-helpers.ts: Edge resolution (margins, padding, borders)
|
|
10
|
+
* - layout-traversal.ts: Tree traversal (markSubtreeLayoutSeen, countNodes)
|
|
11
|
+
* - layout-flex-lines.ts: Pre-allocated arrays, line breaking, flex distribution
|
|
12
|
+
* - layout-measure.ts: Intrinsic sizing (measureNode)
|
|
13
|
+
* - layout-stats.ts: Debug/benchmark counters
|
|
14
|
+
*
|
|
15
|
+
* Based on Planning-nl/flexbox.js reference implementation.
|
|
16
|
+
*/
|
|
17
|
+
import * as C from "./constants.js";
|
|
18
|
+
import { resolveValue, applyMinMax } from "./utils.js";
|
|
19
|
+
import { log } from "./logger.js";
|
|
20
|
+
import { getTrace } from "./trace.js";
|
|
21
|
+
// Re-export helpers for backward compatibility
|
|
22
|
+
export { isRowDirection, isReverseDirection, resolveEdgeValue, isEdgeAuto, resolveEdgeBorderValue, } from "./layout-helpers.js";
|
|
23
|
+
// Re-export traversal utilities for backward compatibility
|
|
24
|
+
export { markSubtreeLayoutSeen, countNodes } from "./layout-traversal.js";
|
|
25
|
+
// Re-export stats for backward compatibility
|
|
26
|
+
export { layoutNodeCalls, measureNodeCalls, resolveEdgeCalls, layoutSizingCalls, layoutPositioningCalls, layoutCacheHits, resetLayoutStats, } from "./layout-stats.js";
|
|
27
|
+
// Re-export measureNode for backward compatibility
|
|
28
|
+
export { measureNode } from "./layout-measure.js";
|
|
29
|
+
// Import what we need internally
|
|
30
|
+
import { isRowDirection, isReverseDirection, resolveEdgeValue, isEdgeAuto, resolveEdgeBorderValue, } from "./layout-helpers.js";
|
|
31
|
+
import { propagatePositionDelta } from "./layout-traversal.js";
|
|
32
|
+
import { resetLayoutStats, incLayoutNodeCalls, incLayoutSizingCalls, incLayoutPositioningCalls, incLayoutCacheHits, } from "./layout-stats.js";
|
|
33
|
+
import { measureNode } from "./layout-measure.js";
|
|
34
|
+
import { MAX_FLEX_LINES, _lineCrossSizes, _lineCrossOffsets, _lineChildren, _lineJustifyStarts, _lineItemSpacings, breakIntoLines, distributeFlexSpaceForLine, } from "./layout-flex-lines.js";
|
|
35
|
+
/**
|
|
36
|
+
* Compute layout for a node tree.
|
|
37
|
+
*/
|
|
38
|
+
export function computeLayout(root, availableWidth, availableHeight, direction = C.DIRECTION_LTR) {
|
|
39
|
+
resetLayoutStats();
|
|
40
|
+
getTrace()?.resetCounter();
|
|
41
|
+
// Clear layout cache from previous pass (important for correct layout after tree changes)
|
|
42
|
+
root.resetLayoutCache();
|
|
43
|
+
// Pass absolute position (0,0) for root node - used for Yoga-compatible edge rounding
|
|
44
|
+
layoutNode(root, availableWidth, availableHeight, 0, 0, 0, 0, direction);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Layout a node and its children.
|
|
48
|
+
*
|
|
49
|
+
* @param absX - Absolute X position from document root (for Yoga-compatible edge rounding)
|
|
50
|
+
* @param absY - Absolute Y position from document root (for Yoga-compatible edge rounding)
|
|
51
|
+
*/
|
|
52
|
+
function layoutNode(node, availableWidth, availableHeight, offsetX, offsetY, absX, absY, direction = C.DIRECTION_LTR) {
|
|
53
|
+
incLayoutNodeCalls();
|
|
54
|
+
// Track sizing vs positioning calls
|
|
55
|
+
const isSizingPass = offsetX === 0 && offsetY === 0 && absX === 0 && absY === 0;
|
|
56
|
+
if (isSizingPass && node.children.length > 0) {
|
|
57
|
+
incLayoutSizingCalls();
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
incLayoutPositioningCalls();
|
|
61
|
+
}
|
|
62
|
+
log.debug?.("layoutNode called: availW=%d, availH=%d, offsetX=%d, offsetY=%d, absX=%d, absY=%d, children=%d", availableWidth, availableHeight, offsetX, offsetY, absX, absY, node.children.length);
|
|
63
|
+
const _t = getTrace();
|
|
64
|
+
const _tn = _t?.nextNode() ?? 0;
|
|
65
|
+
_t?.layoutEnter(_tn, availableWidth, availableHeight, node.isDirty(), node.children.length);
|
|
66
|
+
const style = node.style;
|
|
67
|
+
const layout = node.layout;
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// PHASE 1: Early Exit Checks
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Handle display: none
|
|
72
|
+
if (style.display === C.DISPLAY_NONE) {
|
|
73
|
+
layout.left = 0;
|
|
74
|
+
layout.top = 0;
|
|
75
|
+
layout.width = 0;
|
|
76
|
+
layout.height = 0;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Constraint fingerprinting: skip layout if constraints unchanged and node not dirty
|
|
80
|
+
// Use Object.is() for NaN-safe comparison (NaN === NaN is false, Object.is(NaN, NaN) is true)
|
|
81
|
+
const flex = node.flex;
|
|
82
|
+
if (flex.layoutValid &&
|
|
83
|
+
!node.isDirty() &&
|
|
84
|
+
Object.is(flex.lastAvailW, availableWidth) &&
|
|
85
|
+
Object.is(flex.lastAvailH, availableHeight) &&
|
|
86
|
+
flex.lastDir === direction) {
|
|
87
|
+
// Constraints unchanged - just update position based on offset delta
|
|
88
|
+
_t?.fingerprintHit(_tn, availableWidth, availableHeight);
|
|
89
|
+
const deltaX = offsetX - flex.lastOffsetX;
|
|
90
|
+
const deltaY = offsetY - flex.lastOffsetY;
|
|
91
|
+
if (deltaX !== 0 || deltaY !== 0) {
|
|
92
|
+
layout.left += deltaX;
|
|
93
|
+
layout.top += deltaY;
|
|
94
|
+
flex.lastOffsetX = offsetX;
|
|
95
|
+
flex.lastOffsetY = offsetY;
|
|
96
|
+
// Propagate position delta to all children
|
|
97
|
+
propagatePositionDelta(node, deltaX, deltaY);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
_t?.fingerprintMiss(_tn, availableWidth, availableHeight, {
|
|
102
|
+
layoutValid: flex.layoutValid,
|
|
103
|
+
isDirty: node.isDirty(),
|
|
104
|
+
sameW: Object.is(flex.lastAvailW, availableWidth),
|
|
105
|
+
sameH: Object.is(flex.lastAvailH, availableHeight),
|
|
106
|
+
sameDir: flex.lastDir === direction,
|
|
107
|
+
});
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// PHASE 2: Resolve Spacing (margins, padding, borders)
|
|
110
|
+
// CSS spec: percentage margins AND padding resolve against containing block's WIDTH only
|
|
111
|
+
// ============================================================================
|
|
112
|
+
const marginLeft = resolveEdgeValue(style.margin, 0, style.flexDirection, availableWidth, direction);
|
|
113
|
+
const marginTop = resolveEdgeValue(style.margin, 1, style.flexDirection, availableWidth, direction);
|
|
114
|
+
const marginRight = resolveEdgeValue(style.margin, 2, style.flexDirection, availableWidth, direction);
|
|
115
|
+
const marginBottom = resolveEdgeValue(style.margin, 3, style.flexDirection, availableWidth, direction);
|
|
116
|
+
const paddingLeft = resolveEdgeValue(style.padding, 0, style.flexDirection, availableWidth, direction);
|
|
117
|
+
const paddingTop = resolveEdgeValue(style.padding, 1, style.flexDirection, availableWidth, direction);
|
|
118
|
+
const paddingRight = resolveEdgeValue(style.padding, 2, style.flexDirection, availableWidth, direction);
|
|
119
|
+
const paddingBottom = resolveEdgeValue(style.padding, 3, style.flexDirection, availableWidth, direction);
|
|
120
|
+
const borderLeft = resolveEdgeBorderValue(style.border, 0, style.flexDirection, direction);
|
|
121
|
+
const borderTop = resolveEdgeBorderValue(style.border, 1, style.flexDirection, direction);
|
|
122
|
+
const borderRight = resolveEdgeBorderValue(style.border, 2, style.flexDirection, direction);
|
|
123
|
+
const borderBottom = resolveEdgeBorderValue(style.border, 3, style.flexDirection, direction);
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// PHASE 3: Calculate Node Dimensions
|
|
126
|
+
// When available dimension is NaN (unconstrained), auto-sized nodes use NaN
|
|
127
|
+
// and will be sized by shrink-wrap logic based on children
|
|
128
|
+
// ============================================================================
|
|
129
|
+
let nodeWidth;
|
|
130
|
+
if (style.width.unit === C.UNIT_POINT) {
|
|
131
|
+
nodeWidth = style.width.value;
|
|
132
|
+
}
|
|
133
|
+
else if (style.width.unit === C.UNIT_PERCENT) {
|
|
134
|
+
// Percentage against NaN (auto-sized parent) resolves to 0 via resolveValue
|
|
135
|
+
nodeWidth = resolveValue(style.width, availableWidth);
|
|
136
|
+
}
|
|
137
|
+
else if (Number.isNaN(availableWidth)) {
|
|
138
|
+
// Unconstrained: use NaN to signal shrink-wrap (will be computed from children)
|
|
139
|
+
nodeWidth = NaN;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
nodeWidth = availableWidth - marginLeft - marginRight;
|
|
143
|
+
}
|
|
144
|
+
// Apply min/max constraints (works even with NaN available for point-based constraints)
|
|
145
|
+
nodeWidth = applyMinMax(nodeWidth, style.minWidth, style.maxWidth, availableWidth);
|
|
146
|
+
let nodeHeight;
|
|
147
|
+
if (style.height.unit === C.UNIT_POINT) {
|
|
148
|
+
nodeHeight = style.height.value;
|
|
149
|
+
}
|
|
150
|
+
else if (style.height.unit === C.UNIT_PERCENT) {
|
|
151
|
+
// Percentage against NaN (auto-sized parent) resolves to 0 via resolveValue
|
|
152
|
+
nodeHeight = resolveValue(style.height, availableHeight);
|
|
153
|
+
}
|
|
154
|
+
else if (Number.isNaN(availableHeight)) {
|
|
155
|
+
// Unconstrained: use NaN to signal shrink-wrap (will be computed from children)
|
|
156
|
+
nodeHeight = NaN;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
nodeHeight = availableHeight - marginTop - marginBottom;
|
|
160
|
+
}
|
|
161
|
+
// Apply aspect ratio constraint (CSS aspect-ratio spec)
|
|
162
|
+
// If aspectRatio is set and one dimension is auto (NaN), derive it from the other.
|
|
163
|
+
// Re-apply min/max constraints on the derived dimension to respect CSS box model.
|
|
164
|
+
const aspectRatio = style.aspectRatio;
|
|
165
|
+
if (!Number.isNaN(aspectRatio) && aspectRatio > 0) {
|
|
166
|
+
const widthIsAuto = Number.isNaN(nodeWidth) || style.width.unit === C.UNIT_AUTO;
|
|
167
|
+
const heightIsAuto = Number.isNaN(nodeHeight) || style.height.unit === C.UNIT_AUTO;
|
|
168
|
+
if (widthIsAuto && !heightIsAuto && !Number.isNaN(nodeHeight)) {
|
|
169
|
+
// Height is defined, width is auto: width = height * aspectRatio
|
|
170
|
+
nodeWidth = nodeHeight * aspectRatio;
|
|
171
|
+
// Re-apply min/max for derived width
|
|
172
|
+
nodeWidth = applyMinMax(nodeWidth, style.minWidth, style.maxWidth, availableWidth);
|
|
173
|
+
}
|
|
174
|
+
else if (heightIsAuto && !widthIsAuto && !Number.isNaN(nodeWidth)) {
|
|
175
|
+
// Width is defined, height is auto: height = width / aspectRatio
|
|
176
|
+
nodeHeight = nodeWidth / aspectRatio;
|
|
177
|
+
}
|
|
178
|
+
// If both are defined or both are auto, aspectRatio doesn't apply at this stage
|
|
179
|
+
}
|
|
180
|
+
// Apply min/max constraints (works even with NaN available for point-based constraints)
|
|
181
|
+
nodeHeight = applyMinMax(nodeHeight, style.minHeight, style.maxHeight, availableHeight);
|
|
182
|
+
// Content area (inside border and padding)
|
|
183
|
+
// When node dimensions are NaN (unconstrained), content dimensions are also NaN
|
|
184
|
+
const innerLeft = borderLeft + paddingLeft;
|
|
185
|
+
const innerTop = borderTop + paddingTop;
|
|
186
|
+
const innerRight = borderRight + paddingRight;
|
|
187
|
+
const innerBottom = borderBottom + paddingBottom;
|
|
188
|
+
// Enforce box model constraint: minimum size = padding + border
|
|
189
|
+
const minInnerWidth = innerLeft + innerRight;
|
|
190
|
+
const minInnerHeight = innerTop + innerBottom;
|
|
191
|
+
if (!Number.isNaN(nodeWidth) && nodeWidth < minInnerWidth) {
|
|
192
|
+
nodeWidth = minInnerWidth;
|
|
193
|
+
}
|
|
194
|
+
if (!Number.isNaN(nodeHeight) && nodeHeight < minInnerHeight) {
|
|
195
|
+
nodeHeight = minInnerHeight;
|
|
196
|
+
}
|
|
197
|
+
const contentWidth = Number.isNaN(nodeWidth) ? NaN : Math.max(0, nodeWidth - innerLeft - innerRight);
|
|
198
|
+
const contentHeight = Number.isNaN(nodeHeight) ? NaN : Math.max(0, nodeHeight - innerTop - innerBottom);
|
|
199
|
+
// Compute position offsets early (needed for children's absolute position calculation)
|
|
200
|
+
// This ensures children's absolute positions include parent's position offset
|
|
201
|
+
let parentPosOffsetX = 0;
|
|
202
|
+
let parentPosOffsetY = 0;
|
|
203
|
+
if (style.positionType === C.POSITION_TYPE_STATIC || style.positionType === C.POSITION_TYPE_RELATIVE) {
|
|
204
|
+
const leftPos = style.position[0];
|
|
205
|
+
const topPos = style.position[1];
|
|
206
|
+
const rightPos = style.position[2];
|
|
207
|
+
const bottomPos = style.position[3];
|
|
208
|
+
if (leftPos.unit !== C.UNIT_UNDEFINED) {
|
|
209
|
+
parentPosOffsetX = resolveValue(leftPos, availableWidth);
|
|
210
|
+
}
|
|
211
|
+
else if (rightPos.unit !== C.UNIT_UNDEFINED) {
|
|
212
|
+
parentPosOffsetX = -resolveValue(rightPos, availableWidth);
|
|
213
|
+
}
|
|
214
|
+
if (topPos.unit !== C.UNIT_UNDEFINED) {
|
|
215
|
+
parentPosOffsetY = resolveValue(topPos, availableHeight);
|
|
216
|
+
}
|
|
217
|
+
else if (bottomPos.unit !== C.UNIT_UNDEFINED) {
|
|
218
|
+
parentPosOffsetY = -resolveValue(bottomPos, availableHeight);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// =========================================================================
|
|
222
|
+
// PHASE 4: Handle Leaf Nodes (nodes without children)
|
|
223
|
+
// =========================================================================
|
|
224
|
+
// Two cases:
|
|
225
|
+
// - With measureFunc: Call measure to get intrinsic size (text nodes)
|
|
226
|
+
// - Without measureFunc: Use padding+border as intrinsic size (empty boxes)
|
|
227
|
+
// Handle measure function (text nodes)
|
|
228
|
+
if (node.hasMeasureFunc() && node.children.length === 0) {
|
|
229
|
+
// For unconstrained dimensions (NaN), treat as auto-sizing
|
|
230
|
+
const widthIsAuto = style.width.unit === C.UNIT_AUTO || style.width.unit === C.UNIT_UNDEFINED || Number.isNaN(nodeWidth);
|
|
231
|
+
const heightIsAuto = style.height.unit === C.UNIT_AUTO || style.height.unit === C.UNIT_UNDEFINED || Number.isNaN(nodeHeight);
|
|
232
|
+
const widthMode = widthIsAuto ? C.MEASURE_MODE_AT_MOST : C.MEASURE_MODE_EXACTLY;
|
|
233
|
+
const heightMode = heightIsAuto ? C.MEASURE_MODE_UNDEFINED : C.MEASURE_MODE_EXACTLY;
|
|
234
|
+
// For unconstrained width, use a large value; measureFunc should return intrinsic size
|
|
235
|
+
const measureWidth = Number.isNaN(contentWidth) ? Infinity : contentWidth;
|
|
236
|
+
const measureHeight = Number.isNaN(contentHeight) ? Infinity : contentHeight;
|
|
237
|
+
// Use cached measure to avoid redundant calls within a layout pass
|
|
238
|
+
const measured = node.cachedMeasure(measureWidth, widthMode, measureHeight, heightMode);
|
|
239
|
+
if (widthIsAuto) {
|
|
240
|
+
nodeWidth = measured.width + innerLeft + innerRight;
|
|
241
|
+
}
|
|
242
|
+
if (heightIsAuto) {
|
|
243
|
+
nodeHeight = measured.height + innerTop + innerBottom;
|
|
244
|
+
}
|
|
245
|
+
layout.width = Math.round(nodeWidth);
|
|
246
|
+
layout.height = Math.round(nodeHeight);
|
|
247
|
+
layout.left = Math.round(offsetX + marginLeft);
|
|
248
|
+
layout.top = Math.round(offsetY + marginTop);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// Handle leaf nodes without measureFunc - when unconstrained, use padding+border as intrinsic size
|
|
252
|
+
if (node.children.length === 0) {
|
|
253
|
+
// For leaf nodes without measureFunc, intrinsic size is just padding+border
|
|
254
|
+
if (Number.isNaN(nodeWidth)) {
|
|
255
|
+
nodeWidth = innerLeft + innerRight;
|
|
256
|
+
}
|
|
257
|
+
if (Number.isNaN(nodeHeight)) {
|
|
258
|
+
nodeHeight = innerTop + innerBottom;
|
|
259
|
+
}
|
|
260
|
+
layout.width = Math.round(nodeWidth);
|
|
261
|
+
layout.height = Math.round(nodeHeight);
|
|
262
|
+
layout.left = Math.round(offsetX + marginLeft);
|
|
263
|
+
layout.top = Math.round(offsetY + marginTop);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
// =========================================================================
|
|
267
|
+
// PHASE 5: Flex Layout - Collect Children and Compute Base Sizes
|
|
268
|
+
// =========================================================================
|
|
269
|
+
// Single pass over children:
|
|
270
|
+
// - Skip absolute/hidden children (relativeIndex = -1)
|
|
271
|
+
// - Cache resolved margins for each relative child
|
|
272
|
+
// - Compute base main size from flex-basis, explicit size, or intrinsic size
|
|
273
|
+
// - Track flex grow/shrink factors and min/max constraints
|
|
274
|
+
// - Count auto margins for later distribution
|
|
275
|
+
const isRow = isRowDirection(style.flexDirection);
|
|
276
|
+
const isReverse = isReverseDirection(style.flexDirection);
|
|
277
|
+
// For RTL, row direction is reversed (XOR with isReverse)
|
|
278
|
+
const isRTL = direction === C.DIRECTION_RTL;
|
|
279
|
+
const effectiveReverse = isRow ? isRTL !== isReverse : isReverse;
|
|
280
|
+
const mainAxisSize = isRow ? contentWidth : contentHeight;
|
|
281
|
+
const crossAxisSize = isRow ? contentHeight : contentWidth;
|
|
282
|
+
const mainGap = isRow ? style.gap[0] : style.gap[1];
|
|
283
|
+
// Prepare child flex info (stored on each child node - zero allocation)
|
|
284
|
+
let totalBaseMain = 0;
|
|
285
|
+
let relativeCount = 0;
|
|
286
|
+
let totalAutoMargins = 0; // Count auto margins during this pass
|
|
287
|
+
let hasBaselineAlignment = style.alignItems === C.ALIGN_BASELINE;
|
|
288
|
+
for (const child of node.children) {
|
|
289
|
+
// Mark relativeIndex (-1 for absolute/hidden, 0+ for relative)
|
|
290
|
+
if (child.style.display === C.DISPLAY_NONE || child.style.positionType === C.POSITION_TYPE_ABSOLUTE) {
|
|
291
|
+
child.flex.relativeIndex = -1;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
child.flex.relativeIndex = relativeCount++;
|
|
295
|
+
const childStyle = child.style;
|
|
296
|
+
const cflex = child.flex;
|
|
297
|
+
// Check for auto margins on main axis
|
|
298
|
+
// Physical indices depend on axis and effective reverse (including RTL):
|
|
299
|
+
// - Row LTR: main-start=left(0), main-end=right(2)
|
|
300
|
+
// - Row RTL: main-start=right(2), main-end=left(0)
|
|
301
|
+
// - Row-reverse LTR: main-start=right(2), main-end=left(0)
|
|
302
|
+
// - Row-reverse RTL: main-start=left(0), main-end=right(2)
|
|
303
|
+
// - Column: main-start=top(1), main-end=bottom(3) (RTL doesn't affect column)
|
|
304
|
+
// - Column-reverse: main-start=bottom(3), main-end=top(1)
|
|
305
|
+
// For row layouts, use effectiveReverse (which XORs RTL with isReverse)
|
|
306
|
+
const mainStartIndex = isRow ? (effectiveReverse ? 2 : 0) : isReverse ? 3 : 1;
|
|
307
|
+
const mainEndIndex = isRow ? (effectiveReverse ? 0 : 2) : isReverse ? 1 : 3;
|
|
308
|
+
cflex.mainStartMarginAuto = isEdgeAuto(childStyle.margin, mainStartIndex, style.flexDirection, direction);
|
|
309
|
+
cflex.mainEndMarginAuto = isEdgeAuto(childStyle.margin, mainEndIndex, style.flexDirection, direction);
|
|
310
|
+
// Cache all 4 resolved margins once (CSS spec: percentages resolve against containing block's WIDTH)
|
|
311
|
+
// This avoids repeated resolveEdgeValue calls throughout the layout pass
|
|
312
|
+
cflex.marginL = resolveEdgeValue(childStyle.margin, 0, style.flexDirection, contentWidth, direction);
|
|
313
|
+
cflex.marginT = resolveEdgeValue(childStyle.margin, 1, style.flexDirection, contentWidth, direction);
|
|
314
|
+
cflex.marginR = resolveEdgeValue(childStyle.margin, 2, style.flexDirection, contentWidth, direction);
|
|
315
|
+
cflex.marginB = resolveEdgeValue(childStyle.margin, 3, style.flexDirection, contentWidth, direction);
|
|
316
|
+
// Resolve non-auto margins (auto margins resolve to 0 initially)
|
|
317
|
+
// Use effectiveReverse for row layouts (accounts for RTL)
|
|
318
|
+
cflex.mainStartMarginValue = cflex.mainStartMarginAuto
|
|
319
|
+
? 0
|
|
320
|
+
: isRow
|
|
321
|
+
? effectiveReverse
|
|
322
|
+
? cflex.marginR
|
|
323
|
+
: cflex.marginL
|
|
324
|
+
: isReverse
|
|
325
|
+
? cflex.marginB
|
|
326
|
+
: cflex.marginT;
|
|
327
|
+
cflex.mainEndMarginValue = cflex.mainEndMarginAuto
|
|
328
|
+
? 0
|
|
329
|
+
: isRow
|
|
330
|
+
? effectiveReverse
|
|
331
|
+
? cflex.marginL
|
|
332
|
+
: cflex.marginR
|
|
333
|
+
: isReverse
|
|
334
|
+
? cflex.marginT
|
|
335
|
+
: cflex.marginB;
|
|
336
|
+
// Total non-auto margin for flex calculations
|
|
337
|
+
cflex.mainMargin = cflex.mainStartMarginValue + cflex.mainEndMarginValue;
|
|
338
|
+
// Determine base size (flex-basis or explicit size)
|
|
339
|
+
// Guard: percent against NaN (unconstrained) resolves to 0 (CSS/Yoga behavior)
|
|
340
|
+
let baseSize = 0;
|
|
341
|
+
if (childStyle.flexBasis.unit === C.UNIT_POINT) {
|
|
342
|
+
baseSize = childStyle.flexBasis.value;
|
|
343
|
+
}
|
|
344
|
+
else if (childStyle.flexBasis.unit === C.UNIT_PERCENT) {
|
|
345
|
+
baseSize = Number.isNaN(mainAxisSize) ? 0 : mainAxisSize * (childStyle.flexBasis.value / 100);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
const sizeVal = isRow ? childStyle.width : childStyle.height;
|
|
349
|
+
if (sizeVal.unit === C.UNIT_POINT) {
|
|
350
|
+
baseSize = sizeVal.value;
|
|
351
|
+
}
|
|
352
|
+
else if (sizeVal.unit === C.UNIT_PERCENT) {
|
|
353
|
+
baseSize = Number.isNaN(mainAxisSize) ? 0 : mainAxisSize * (sizeVal.value / 100);
|
|
354
|
+
}
|
|
355
|
+
else if (child.hasMeasureFunc() && childStyle.flexGrow === 0) {
|
|
356
|
+
// For auto-sized children with measureFunc but no flexGrow,
|
|
357
|
+
// pre-measure to get intrinsic size for justify-content calculation
|
|
358
|
+
// Use cached margins
|
|
359
|
+
const crossMargin = isRow ? cflex.marginT + cflex.marginB : cflex.marginL + cflex.marginR;
|
|
360
|
+
const availCross = crossAxisSize - crossMargin;
|
|
361
|
+
// Use cached measure to avoid redundant calls within a layout pass
|
|
362
|
+
// Measure function takes PHYSICAL (width, height), not logical (main, cross).
|
|
363
|
+
// For row: main=width, cross=height. For column: main=height, cross=width.
|
|
364
|
+
const mW = isRow ? mainAxisSize : availCross;
|
|
365
|
+
const mH = isRow ? availCross : mainAxisSize;
|
|
366
|
+
const mWMode = isRow
|
|
367
|
+
? C.MEASURE_MODE_AT_MOST
|
|
368
|
+
: Number.isNaN(availCross)
|
|
369
|
+
? C.MEASURE_MODE_UNDEFINED
|
|
370
|
+
: C.MEASURE_MODE_AT_MOST;
|
|
371
|
+
const mHMode = isRow ? C.MEASURE_MODE_UNDEFINED : C.MEASURE_MODE_AT_MOST;
|
|
372
|
+
const measured = child.cachedMeasure(Number.isNaN(mW) ? Infinity : mW, mWMode, Number.isNaN(mH) ? Infinity : mH, mHMode);
|
|
373
|
+
baseSize = isRow ? measured.width : measured.height;
|
|
374
|
+
}
|
|
375
|
+
else if (child.children.length > 0) {
|
|
376
|
+
// For auto-sized children WITH children but no measureFunc,
|
|
377
|
+
// recursively compute intrinsic size by laying out with unconstrained main axis
|
|
378
|
+
// Check cache first to avoid redundant recursive calls
|
|
379
|
+
const sizingW = isRow ? NaN : crossAxisSize;
|
|
380
|
+
const sizingH = isRow ? crossAxisSize : NaN;
|
|
381
|
+
const cached = child.getCachedLayout(sizingW, sizingH);
|
|
382
|
+
if (cached) {
|
|
383
|
+
incLayoutCacheHits();
|
|
384
|
+
baseSize = isRow ? cached.width : cached.height;
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
// Use measureNode for sizing-only pass (faster than full layoutNode)
|
|
388
|
+
// Save layout before measureNode — it overwrites node.layout.width/height
|
|
389
|
+
// with intrinsic measurements (unconstrained widths -> text doesn't wrap ->
|
|
390
|
+
// shorter height). Without save/restore, layoutNode's fingerprint check
|
|
391
|
+
// in Phase 9 would skip re-computation and preserve the corrupted values.
|
|
392
|
+
const savedW = child.layout.width;
|
|
393
|
+
const savedH = child.layout.height;
|
|
394
|
+
measureNode(child, sizingW, sizingH);
|
|
395
|
+
const measuredW = child.layout.width;
|
|
396
|
+
const measuredH = child.layout.height;
|
|
397
|
+
child.layout.width = savedW;
|
|
398
|
+
child.layout.height = savedH;
|
|
399
|
+
baseSize = isRow ? measuredW : measuredH;
|
|
400
|
+
// Cache the result for potential reuse
|
|
401
|
+
child.setCachedLayout(sizingW, sizingH, measuredW, measuredH);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
// For auto-sized LEAF children without measureFunc, use padding + border as minimum
|
|
406
|
+
// This ensures elements with only padding still have proper size
|
|
407
|
+
// CSS spec: percentage padding resolves against containing block's WIDTH only
|
|
408
|
+
// Use resolveEdgeValue to respect logical EDGE_START/END
|
|
409
|
+
// For row: mainAxisSize is contentWidth; for column: crossAxisSize is contentWidth
|
|
410
|
+
const parentWidth = isRow ? mainAxisSize : crossAxisSize;
|
|
411
|
+
const childPadding = isRow
|
|
412
|
+
? resolveEdgeValue(childStyle.padding, 0, childStyle.flexDirection, parentWidth, direction) +
|
|
413
|
+
resolveEdgeValue(childStyle.padding, 2, childStyle.flexDirection, parentWidth, direction)
|
|
414
|
+
: resolveEdgeValue(childStyle.padding, 1, childStyle.flexDirection, parentWidth, direction) +
|
|
415
|
+
resolveEdgeValue(childStyle.padding, 3, childStyle.flexDirection, parentWidth, direction);
|
|
416
|
+
const childBorder = isRow
|
|
417
|
+
? resolveEdgeBorderValue(childStyle.border, 0, childStyle.flexDirection, direction) +
|
|
418
|
+
resolveEdgeBorderValue(childStyle.border, 2, childStyle.flexDirection, direction)
|
|
419
|
+
: resolveEdgeBorderValue(childStyle.border, 1, childStyle.flexDirection, direction) +
|
|
420
|
+
resolveEdgeBorderValue(childStyle.border, 3, childStyle.flexDirection, direction);
|
|
421
|
+
baseSize = childPadding + childBorder;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Min/max on main axis
|
|
425
|
+
const minVal = isRow ? childStyle.minWidth : childStyle.minHeight;
|
|
426
|
+
const maxVal = isRow ? childStyle.maxWidth : childStyle.maxHeight;
|
|
427
|
+
cflex.minMain = minVal.unit !== C.UNIT_UNDEFINED ? resolveValue(minVal, mainAxisSize) : 0;
|
|
428
|
+
cflex.maxMain = maxVal.unit !== C.UNIT_UNDEFINED ? resolveValue(maxVal, mainAxisSize) : Infinity;
|
|
429
|
+
// Store flex factors from style
|
|
430
|
+
cflex.flexGrow = childStyle.flexGrow;
|
|
431
|
+
// CSS spec 4.5: overflow containers have automatic min-size = 0 and can shrink
|
|
432
|
+
// below content size. Yoga defaults flexShrink to 0, preventing this. For
|
|
433
|
+
// overflow:hidden/scroll children, ensure flexShrink >= 1 so they participate
|
|
434
|
+
// in negative free space distribution (matching CSS behavior).
|
|
435
|
+
cflex.flexShrink =
|
|
436
|
+
childStyle.overflow !== C.OVERFLOW_VISIBLE ? Math.max(childStyle.flexShrink, 1) : childStyle.flexShrink;
|
|
437
|
+
// Store base and main size (start from base size - distribution happens from here)
|
|
438
|
+
cflex.baseSize = baseSize;
|
|
439
|
+
cflex.mainSize = baseSize;
|
|
440
|
+
cflex.frozen = false; // Will be set during distribution
|
|
441
|
+
// Free space calculation uses BASE sizes (per Yoga/CSS spec algorithm)
|
|
442
|
+
// The freeze loop handles min/max clamping iteratively
|
|
443
|
+
totalBaseMain += baseSize + cflex.mainMargin;
|
|
444
|
+
// Count auto margins for later distribution
|
|
445
|
+
if (cflex.mainStartMarginAuto)
|
|
446
|
+
totalAutoMargins++;
|
|
447
|
+
if (cflex.mainEndMarginAuto)
|
|
448
|
+
totalAutoMargins++;
|
|
449
|
+
// Check for baseline alignment
|
|
450
|
+
if (!hasBaselineAlignment && childStyle.alignSelf === C.ALIGN_BASELINE) {
|
|
451
|
+
hasBaselineAlignment = true;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
log.debug?.("layoutNode: node.children=%d, relativeCount=%d", node.children.length, relativeCount);
|
|
455
|
+
if (relativeCount > 0) {
|
|
456
|
+
// -----------------------------------------------------------------------
|
|
457
|
+
// PHASE 6a: Flex Line Breaking and Space Distribution
|
|
458
|
+
// -----------------------------------------------------------------------
|
|
459
|
+
// Break children into lines (for flex-wrap).
|
|
460
|
+
// Distribute free space using grow/shrink factors.
|
|
461
|
+
// Apply min/max constraints.
|
|
462
|
+
// Break children into flex lines for wrap support (zero allocation - sets child.flex.lineIndex)
|
|
463
|
+
const numLines = breakIntoLines(node, relativeCount, mainAxisSize, mainGap, style.flexWrap);
|
|
464
|
+
const crossGap = isRow ? style.gap[1] : style.gap[0];
|
|
465
|
+
// Process each line: distribute flex space
|
|
466
|
+
// Uses pre-collected _lineChildren to avoid O(n*m) iteration
|
|
467
|
+
for (let lineIdx = 0; lineIdx < numLines; lineIdx++) {
|
|
468
|
+
const lineChildren = _lineChildren[lineIdx];
|
|
469
|
+
const lineLength = lineChildren.length;
|
|
470
|
+
if (lineLength === 0)
|
|
471
|
+
continue;
|
|
472
|
+
// Calculate total base main and gaps for this line
|
|
473
|
+
let lineTotalBaseMain = 0;
|
|
474
|
+
for (let i = 0; i < lineLength; i++) {
|
|
475
|
+
const c = lineChildren[i];
|
|
476
|
+
lineTotalBaseMain += c.flex.baseSize + c.flex.mainMargin;
|
|
477
|
+
}
|
|
478
|
+
const lineTotalGaps = lineLength > 1 ? mainGap * (lineLength - 1) : 0;
|
|
479
|
+
// Distribute free space using grow or shrink factors
|
|
480
|
+
let effectiveMainSize = mainAxisSize;
|
|
481
|
+
if (Number.isNaN(mainAxisSize)) {
|
|
482
|
+
// Shrink-wrap mode - check if max constraint applies
|
|
483
|
+
const maxMainVal = isRow ? style.maxWidth : style.maxHeight;
|
|
484
|
+
if (maxMainVal.unit !== C.UNIT_UNDEFINED) {
|
|
485
|
+
const maxMain = resolveValue(maxMainVal, isRow ? availableWidth : availableHeight);
|
|
486
|
+
if (!Number.isNaN(maxMain) && lineTotalBaseMain + lineTotalGaps > maxMain) {
|
|
487
|
+
const innerMain = isRow ? innerLeft + innerRight : innerTop + innerBottom;
|
|
488
|
+
effectiveMainSize = maxMain - innerMain;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (!Number.isNaN(effectiveMainSize)) {
|
|
493
|
+
const adjustedFreeSpace = effectiveMainSize - lineTotalBaseMain - lineTotalGaps;
|
|
494
|
+
distributeFlexSpaceForLine(lineChildren, adjustedFreeSpace);
|
|
495
|
+
}
|
|
496
|
+
// Apply min/max constraints to final sizes
|
|
497
|
+
for (let i = 0; i < lineLength; i++) {
|
|
498
|
+
const f = lineChildren[i].flex;
|
|
499
|
+
f.mainSize = Math.max(f.minMain, Math.min(f.maxMain, f.mainSize));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// -----------------------------------------------------------------------
|
|
503
|
+
// PHASE 6b: Justify-Content and Auto Margins (Per-Line)
|
|
504
|
+
// -----------------------------------------------------------------------
|
|
505
|
+
// Distribute remaining free space on main axis PER LINE.
|
|
506
|
+
// Auto margins absorb space first, then justify-content applies.
|
|
507
|
+
// This fixes multi-line wrap layouts to match CSS spec behavior.
|
|
508
|
+
// Compute per-line justify-content and auto margins
|
|
509
|
+
for (let lineIdx = 0; lineIdx < numLines; lineIdx++) {
|
|
510
|
+
const lineChildren = _lineChildren[lineIdx];
|
|
511
|
+
const lineLength = lineChildren.length;
|
|
512
|
+
if (lineLength === 0) {
|
|
513
|
+
_lineJustifyStarts[lineIdx] = 0;
|
|
514
|
+
_lineItemSpacings[lineIdx] = mainGap;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
// Calculate used main axis space for this line
|
|
518
|
+
let lineUsedMain = 0;
|
|
519
|
+
let lineAutoMargins = 0;
|
|
520
|
+
for (let i = 0; i < lineLength; i++) {
|
|
521
|
+
const c = lineChildren[i];
|
|
522
|
+
lineUsedMain += c.flex.mainSize + c.flex.mainMargin;
|
|
523
|
+
if (c.flex.mainStartMarginAuto)
|
|
524
|
+
lineAutoMargins++;
|
|
525
|
+
if (c.flex.mainEndMarginAuto)
|
|
526
|
+
lineAutoMargins++;
|
|
527
|
+
}
|
|
528
|
+
const lineGaps = lineLength > 1 ? mainGap * (lineLength - 1) : 0;
|
|
529
|
+
lineUsedMain += lineGaps;
|
|
530
|
+
// For auto-sized containers (NaN mainAxisSize), there's no remaining space to justify
|
|
531
|
+
const lineRemainingSpace = Number.isNaN(mainAxisSize) ? 0 : mainAxisSize - lineUsedMain;
|
|
532
|
+
// Handle auto margins on main axis (per-line)
|
|
533
|
+
// Auto margins absorb free space BEFORE justify-content
|
|
534
|
+
const lineHasAutoMargins = lineAutoMargins > 0;
|
|
535
|
+
if (lineHasAutoMargins) {
|
|
536
|
+
// Auto margins absorb remaining space for this line
|
|
537
|
+
// CSS spec: auto margins don't absorb negative free space (overflow case)
|
|
538
|
+
const positiveRemaining = Math.max(0, lineRemainingSpace);
|
|
539
|
+
const autoMarginValue = positiveRemaining / lineAutoMargins;
|
|
540
|
+
for (let i = 0; i < lineLength; i++) {
|
|
541
|
+
const child = lineChildren[i];
|
|
542
|
+
if (child.flex.mainStartMarginAuto) {
|
|
543
|
+
child.flex.mainStartMarginValue = autoMarginValue;
|
|
544
|
+
}
|
|
545
|
+
if (child.flex.mainEndMarginAuto) {
|
|
546
|
+
child.flex.mainEndMarginValue = autoMarginValue;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Calculate justify-content offset and spacing for this line
|
|
551
|
+
let lineStartOffset = 0;
|
|
552
|
+
let lineItemSpacing = mainGap;
|
|
553
|
+
// justify-content is ignored when any auto margins exist on this line
|
|
554
|
+
if (!lineHasAutoMargins) {
|
|
555
|
+
// CSS spec behavior for overflow (negative remaining space):
|
|
556
|
+
// - flex-end/center: allow negative offset (items can overflow at start)
|
|
557
|
+
// - space-between/around/evenly: fall back to flex-start (no negative spacing)
|
|
558
|
+
switch (style.justifyContent) {
|
|
559
|
+
case C.JUSTIFY_FLEX_END:
|
|
560
|
+
// Allow negative offset for overflow (matches Yoga/CSS behavior)
|
|
561
|
+
lineStartOffset = lineRemainingSpace;
|
|
562
|
+
break;
|
|
563
|
+
case C.JUSTIFY_CENTER:
|
|
564
|
+
// Allow negative offset for overflow (matches Yoga/CSS behavior)
|
|
565
|
+
lineStartOffset = lineRemainingSpace / 2;
|
|
566
|
+
break;
|
|
567
|
+
case C.JUSTIFY_SPACE_BETWEEN:
|
|
568
|
+
// Only apply space-between when remaining space is positive
|
|
569
|
+
if (lineLength > 1 && lineRemainingSpace > 0) {
|
|
570
|
+
lineItemSpacing = mainGap + lineRemainingSpace / (lineLength - 1);
|
|
571
|
+
}
|
|
572
|
+
break;
|
|
573
|
+
case C.JUSTIFY_SPACE_AROUND:
|
|
574
|
+
// Only apply space-around when remaining space is positive
|
|
575
|
+
if (lineLength > 0 && lineRemainingSpace > 0) {
|
|
576
|
+
const extraSpace = lineRemainingSpace / lineLength;
|
|
577
|
+
lineStartOffset = extraSpace / 2;
|
|
578
|
+
lineItemSpacing = mainGap + extraSpace;
|
|
579
|
+
}
|
|
580
|
+
break;
|
|
581
|
+
case C.JUSTIFY_SPACE_EVENLY:
|
|
582
|
+
// Only apply space-evenly when remaining space is positive
|
|
583
|
+
if (lineLength > 0 && lineRemainingSpace > 0) {
|
|
584
|
+
const extraSpace = lineRemainingSpace / (lineLength + 1);
|
|
585
|
+
lineStartOffset = extraSpace;
|
|
586
|
+
lineItemSpacing = mainGap + extraSpace;
|
|
587
|
+
}
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
_lineJustifyStarts[lineIdx] = lineStartOffset;
|
|
592
|
+
_lineItemSpacings[lineIdx] = lineItemSpacing;
|
|
593
|
+
}
|
|
594
|
+
// For backwards compatibility, set global values for single-line case
|
|
595
|
+
const startOffset = _lineJustifyStarts[0];
|
|
596
|
+
const itemSpacing = _lineItemSpacings[0];
|
|
597
|
+
// NOTE: We do NOT round sizes here. Instead, we use edge-based rounding below.
|
|
598
|
+
// This ensures adjacent elements share exact boundaries without gaps.
|
|
599
|
+
// The key insight: round(pos) gives the edge position, width = round(end) - round(start)
|
|
600
|
+
// -----------------------------------------------------------------------
|
|
601
|
+
// PHASE 6c: Baseline Alignment (Pre-computation)
|
|
602
|
+
// -----------------------------------------------------------------------
|
|
603
|
+
// For align-items: baseline, compute each child's baseline and find max.
|
|
604
|
+
// Uses baselineFunc if provided, otherwise falls back to content box bottom.
|
|
605
|
+
// Compute baseline alignment info if needed (hasBaselineAlignment computed in flex info pass)
|
|
606
|
+
// For ALIGN_BASELINE in row direction, we need to know the max baseline first
|
|
607
|
+
// Zero-alloc: store baseline in child.flex.baseline, not a temporary array
|
|
608
|
+
let maxBaseline = 0;
|
|
609
|
+
if (hasBaselineAlignment && isRow) {
|
|
610
|
+
// First pass: compute each child's baseline and find the maximum
|
|
611
|
+
for (const child of node.children) {
|
|
612
|
+
if (child.flex.relativeIndex < 0)
|
|
613
|
+
continue;
|
|
614
|
+
const childStyle = child.style;
|
|
615
|
+
// Get cross-axis (top) margin for this child - use cached value
|
|
616
|
+
const topMargin = child.flex.marginT;
|
|
617
|
+
// Compute child's dimensions - need to do a mini-layout or use the cached size
|
|
618
|
+
// For children with explicit dimensions, use those
|
|
619
|
+
// For auto-sized children, we need to layout them first
|
|
620
|
+
let childWidth;
|
|
621
|
+
let childHeight;
|
|
622
|
+
const widthDim = childStyle.width;
|
|
623
|
+
const heightDim = childStyle.height;
|
|
624
|
+
// Get width for baseline function
|
|
625
|
+
if (widthDim.unit === C.UNIT_POINT) {
|
|
626
|
+
childWidth = widthDim.value;
|
|
627
|
+
}
|
|
628
|
+
else if (widthDim.unit === C.UNIT_PERCENT && !Number.isNaN(mainAxisSize)) {
|
|
629
|
+
childWidth = mainAxisSize * (widthDim.value / 100);
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
childWidth = child.flex.mainSize;
|
|
633
|
+
}
|
|
634
|
+
// Get height for baseline
|
|
635
|
+
if (heightDim.unit === C.UNIT_POINT) {
|
|
636
|
+
childHeight = heightDim.value;
|
|
637
|
+
}
|
|
638
|
+
else if (heightDim.unit === C.UNIT_PERCENT && !Number.isNaN(crossAxisSize)) {
|
|
639
|
+
childHeight = crossAxisSize * (heightDim.value / 100);
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
// Auto height - need to layout to get intrinsic size
|
|
643
|
+
// Check cache first to avoid redundant recursive calls
|
|
644
|
+
const cached = child.getCachedLayout(child.flex.mainSize, NaN);
|
|
645
|
+
if (cached) {
|
|
646
|
+
incLayoutCacheHits();
|
|
647
|
+
childWidth = cached.width;
|
|
648
|
+
childHeight = cached.height;
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
// Use measureNode for sizing-only pass (faster than full layoutNode)
|
|
652
|
+
// Save layout before measureNode — it overwrites node.layout.width/height
|
|
653
|
+
// with intrinsic measurements. Without save/restore, layoutNode's fingerprint
|
|
654
|
+
// check in Phase 9 would skip re-computation and preserve corrupted values.
|
|
655
|
+
const savedW = child.layout.width;
|
|
656
|
+
const savedH = child.layout.height;
|
|
657
|
+
measureNode(child, child.flex.mainSize, NaN);
|
|
658
|
+
childWidth = child.layout.width;
|
|
659
|
+
childHeight = child.layout.height;
|
|
660
|
+
child.layout.width = savedW;
|
|
661
|
+
child.layout.height = savedH;
|
|
662
|
+
child.setCachedLayout(child.flex.mainSize, NaN, childWidth, childHeight);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// Compute baseline: use baselineFunc if available, otherwise use bottom of content box
|
|
666
|
+
// Store directly in child.flex.baseline (zero-alloc)
|
|
667
|
+
if (child.baselineFunc !== null) {
|
|
668
|
+
// Custom baseline function provided (e.g., for text nodes)
|
|
669
|
+
child.flex.baseline = topMargin + child.baselineFunc(childWidth, childHeight);
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
// Fallback: bottom of content box (default for non-text elements)
|
|
673
|
+
// Note: We don't recursively propagate first-child baselines to avoid O(n^depth) cost
|
|
674
|
+
// This is a simplification from CSS spec but acceptable for TUI use cases
|
|
675
|
+
child.flex.baseline = topMargin + childHeight;
|
|
676
|
+
}
|
|
677
|
+
maxBaseline = Math.max(maxBaseline, child.flex.baseline);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// -----------------------------------------------------------------------
|
|
681
|
+
// PHASE 7a: Estimate Flex Line Cross-Axis Sizes (Tentative)
|
|
682
|
+
// -----------------------------------------------------------------------
|
|
683
|
+
// Estimate cross-axis size of each flex line from definite child dimensions.
|
|
684
|
+
// Auto-sized children use 0 here; actual sizes computed during Phase 8.
|
|
685
|
+
// These are tentative values used for alignContent distribution.
|
|
686
|
+
// Compute line cross-axis sizes and offsets for flex-wrap
|
|
687
|
+
// Each child already has lineIndex set by breakIntoLines
|
|
688
|
+
// Use pre-allocated _lineCrossOffsets and _lineCrossSizes arrays
|
|
689
|
+
let cumulativeCrossOffset = 0;
|
|
690
|
+
const isWrapReverse = style.flexWrap === C.WRAP_WRAP_REVERSE;
|
|
691
|
+
for (let lineIdx = 0; lineIdx < numLines; lineIdx++) {
|
|
692
|
+
_lineCrossOffsets[lineIdx] = cumulativeCrossOffset;
|
|
693
|
+
// Calculate max cross size for this line using pre-collected _lineChildren
|
|
694
|
+
const lineChildren = _lineChildren[lineIdx];
|
|
695
|
+
const lineLength = lineChildren.length;
|
|
696
|
+
let maxLineCross = 0;
|
|
697
|
+
for (let i = 0; i < lineLength; i++) {
|
|
698
|
+
const child = lineChildren[i];
|
|
699
|
+
// Estimate child cross size (will be computed more precisely during layout)
|
|
700
|
+
const childStyle = child.style;
|
|
701
|
+
const crossDim = isRow ? childStyle.height : childStyle.width;
|
|
702
|
+
// Use cached margins
|
|
703
|
+
const crossMarginStart = isRow ? child.flex.marginT : child.flex.marginL;
|
|
704
|
+
const crossMarginEnd = isRow ? child.flex.marginB : child.flex.marginR;
|
|
705
|
+
let childCross = 0;
|
|
706
|
+
if (crossDim.unit === C.UNIT_POINT) {
|
|
707
|
+
childCross = crossDim.value;
|
|
708
|
+
}
|
|
709
|
+
else if (crossDim.unit === C.UNIT_PERCENT && !Number.isNaN(crossAxisSize)) {
|
|
710
|
+
childCross = crossAxisSize * (crossDim.value / 100);
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
// Auto - use a default or measure. For now, use 0 and let stretch handle it.
|
|
714
|
+
childCross = 0;
|
|
715
|
+
}
|
|
716
|
+
maxLineCross = Math.max(maxLineCross, childCross + crossMarginStart + crossMarginEnd);
|
|
717
|
+
}
|
|
718
|
+
// Fallback cross size: use measured max, or divide available space among lines
|
|
719
|
+
// Guard against NaN/division-by-zero: if crossAxisSize is NaN or numLines is 0, use 0
|
|
720
|
+
const fallbackCross = numLines > 0 && !Number.isNaN(crossAxisSize) ? crossAxisSize / numLines : 0;
|
|
721
|
+
const lineCrossSize = maxLineCross > 0 ? maxLineCross : fallbackCross;
|
|
722
|
+
_lineCrossSizes[lineIdx] = lineCrossSize;
|
|
723
|
+
cumulativeCrossOffset += lineCrossSize + crossGap;
|
|
724
|
+
}
|
|
725
|
+
// -----------------------------------------------------------------------
|
|
726
|
+
// PHASE 7b: Apply alignContent
|
|
727
|
+
// -----------------------------------------------------------------------
|
|
728
|
+
// Distribute flex lines within the container's cross-axis.
|
|
729
|
+
// Only applies when flex-wrap creates multiple lines.
|
|
730
|
+
// Apply alignContent to distribute lines in the cross axis
|
|
731
|
+
// Note: While CSS spec says alignContent only applies to multi-line containers,
|
|
732
|
+
// Yoga applies ALIGN_STRETCH to single-line layouts as well. We match Yoga behavior.
|
|
733
|
+
if (!Number.isNaN(crossAxisSize) && numLines > 0) {
|
|
734
|
+
const totalLineCrossSize = cumulativeCrossOffset - crossGap; // Remove trailing gap
|
|
735
|
+
const freeSpace = crossAxisSize - totalLineCrossSize;
|
|
736
|
+
const alignContent = style.alignContent;
|
|
737
|
+
// Reset offsets based on alignContent
|
|
738
|
+
if (freeSpace > 0 || alignContent === C.ALIGN_STRETCH) {
|
|
739
|
+
switch (alignContent) {
|
|
740
|
+
case C.ALIGN_FLEX_END:
|
|
741
|
+
// Lines packed at end
|
|
742
|
+
for (let i = 0; i < numLines; i++) {
|
|
743
|
+
_lineCrossOffsets[i] += freeSpace;
|
|
744
|
+
}
|
|
745
|
+
break;
|
|
746
|
+
case C.ALIGN_CENTER:
|
|
747
|
+
// Lines centered
|
|
748
|
+
{
|
|
749
|
+
const centerOffset = freeSpace / 2;
|
|
750
|
+
for (let i = 0; i < numLines; i++) {
|
|
751
|
+
_lineCrossOffsets[i] += centerOffset;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
break;
|
|
755
|
+
case C.ALIGN_SPACE_BETWEEN:
|
|
756
|
+
// First line at start, last at end, evenly distributed
|
|
757
|
+
if (numLines > 1) {
|
|
758
|
+
const gap = freeSpace / (numLines - 1);
|
|
759
|
+
for (let i = 1; i < numLines; i++) {
|
|
760
|
+
_lineCrossOffsets[i] += gap * i;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
break;
|
|
764
|
+
case C.ALIGN_SPACE_AROUND:
|
|
765
|
+
// Even spacing with half-space at edges
|
|
766
|
+
{
|
|
767
|
+
const halfGap = freeSpace / (numLines * 2);
|
|
768
|
+
for (let i = 0; i < numLines; i++) {
|
|
769
|
+
_lineCrossOffsets[i] += halfGap + halfGap * 2 * i;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
break;
|
|
773
|
+
case C.ALIGN_STRETCH:
|
|
774
|
+
// Distribute extra space evenly among lines
|
|
775
|
+
if (freeSpace > 0 && numLines > 0) {
|
|
776
|
+
const extraPerLine = freeSpace / numLines;
|
|
777
|
+
for (let i = 0; i < numLines; i++) {
|
|
778
|
+
_lineCrossSizes[i] += extraPerLine;
|
|
779
|
+
// Recalculate offset for subsequent lines
|
|
780
|
+
if (i > 0) {
|
|
781
|
+
_lineCrossOffsets[i] = _lineCrossOffsets[i - 1] + _lineCrossSizes[i - 1] + crossGap;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
break;
|
|
786
|
+
// ALIGN_FLEX_START is the default - lines already at start
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
// For wrap-reverse, lines should be positioned from the end of the cross axis
|
|
790
|
+
// The lines are already in reversed order from breakIntoLines().
|
|
791
|
+
// We just need to shift them so they align to the end instead of the start.
|
|
792
|
+
if (isWrapReverse) {
|
|
793
|
+
let totalLineCrossSize = 0;
|
|
794
|
+
for (let i = 0; i < numLines; i++) {
|
|
795
|
+
totalLineCrossSize += _lineCrossSizes[i];
|
|
796
|
+
}
|
|
797
|
+
totalLineCrossSize += crossGap * (numLines - 1);
|
|
798
|
+
const crossStartOffset = crossAxisSize - totalLineCrossSize;
|
|
799
|
+
for (let i = 0; i < numLines; i++) {
|
|
800
|
+
_lineCrossOffsets[i] += crossStartOffset;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
// -----------------------------------------------------------------------
|
|
805
|
+
// PHASE 8: Position and Layout Children
|
|
806
|
+
// -----------------------------------------------------------------------
|
|
807
|
+
// Calculate each child's position in the container.
|
|
808
|
+
// Apply cross-axis alignment (align-items, align-self).
|
|
809
|
+
// Recursively layout grandchildren.
|
|
810
|
+
// Position and layout children
|
|
811
|
+
// For reverse directions (including RTL for row), start from the END of the container
|
|
812
|
+
// RTL + reverse cancels out (XOR behavior)
|
|
813
|
+
// For shrink-wrap containers, compute effective main size first
|
|
814
|
+
let effectiveMainAxisSize = mainAxisSize;
|
|
815
|
+
const mainIsAuto = isRow
|
|
816
|
+
? style.width.unit !== C.UNIT_POINT && style.width.unit !== C.UNIT_PERCENT
|
|
817
|
+
: style.height.unit !== C.UNIT_POINT && style.height.unit !== C.UNIT_PERCENT;
|
|
818
|
+
// Calculate total gaps for all children (used for shrink-wrap sizing)
|
|
819
|
+
const totalGaps = relativeCount > 1 ? mainGap * (relativeCount - 1) : 0;
|
|
820
|
+
if (effectiveReverse && mainIsAuto) {
|
|
821
|
+
// For reverse with auto size, compute total content size for positioning
|
|
822
|
+
let totalContent = 0;
|
|
823
|
+
for (const child of node.children) {
|
|
824
|
+
if (child.flex.relativeIndex < 0)
|
|
825
|
+
continue;
|
|
826
|
+
totalContent += child.flex.mainSize + child.flex.mainStartMarginValue + child.flex.mainEndMarginValue;
|
|
827
|
+
}
|
|
828
|
+
totalContent += totalGaps;
|
|
829
|
+
effectiveMainAxisSize = totalContent;
|
|
830
|
+
}
|
|
831
|
+
// Use fractional mainPos for edge-based rounding
|
|
832
|
+
// Initialize with first line's startOffset (may be overridden when processing lines)
|
|
833
|
+
let mainPos = effectiveReverse ? effectiveMainAxisSize - startOffset : startOffset;
|
|
834
|
+
let currentLineIdx = -1;
|
|
835
|
+
let relIdx = 0; // Track relative child index globally
|
|
836
|
+
let lineChildIdx = 0; // Track position within current line (for gap handling)
|
|
837
|
+
let currentLineLength = 0; // Length of current line
|
|
838
|
+
let currentItemSpacing = itemSpacing; // Track current line's item spacing
|
|
839
|
+
log.debug?.("positioning children: isRow=%s, startOffset=%d, relativeCount=%d, effectiveReverse=%s, numLines=%d", isRow, startOffset, relativeCount, effectiveReverse, numLines);
|
|
840
|
+
for (const child of node.children) {
|
|
841
|
+
if (child.flex.relativeIndex < 0)
|
|
842
|
+
continue;
|
|
843
|
+
const cflex = child.flex;
|
|
844
|
+
const childStyle = child.style;
|
|
845
|
+
// Check if we've moved to a new line (for flex-wrap)
|
|
846
|
+
const childLineIdx = cflex.lineIndex;
|
|
847
|
+
if (childLineIdx !== currentLineIdx) {
|
|
848
|
+
currentLineIdx = childLineIdx;
|
|
849
|
+
lineChildIdx = 0; // Reset position within line
|
|
850
|
+
currentLineLength = _lineChildren[childLineIdx].length;
|
|
851
|
+
// Reset mainPos for new line using line-specific justify offset
|
|
852
|
+
const lineOffset = _lineJustifyStarts[childLineIdx];
|
|
853
|
+
currentItemSpacing = _lineItemSpacings[childLineIdx];
|
|
854
|
+
mainPos = effectiveReverse ? effectiveMainAxisSize - lineOffset : lineOffset;
|
|
855
|
+
}
|
|
856
|
+
// Get cross-axis offset for this child's line (from pre-allocated array)
|
|
857
|
+
const lineCrossOffset = childLineIdx < MAX_FLEX_LINES ? _lineCrossOffsets[childLineIdx] : 0;
|
|
858
|
+
// For main-axis margins, use computed auto margin values
|
|
859
|
+
// For cross-axis margins, use cached values (auto margins on cross axis handled separately)
|
|
860
|
+
let childMarginLeft;
|
|
861
|
+
let childMarginTop;
|
|
862
|
+
let childMarginRight;
|
|
863
|
+
let childMarginBottom;
|
|
864
|
+
// Use cached margins, with auto margin override for main axis
|
|
865
|
+
// For row layouts, use effectiveReverse (accounts for RTL)
|
|
866
|
+
if (isRow) {
|
|
867
|
+
// Row: main axis is horizontal
|
|
868
|
+
// effectiveReverse handles both row-reverse AND RTL
|
|
869
|
+
childMarginLeft =
|
|
870
|
+
cflex.mainStartMarginAuto && !effectiveReverse
|
|
871
|
+
? cflex.mainStartMarginValue
|
|
872
|
+
: cflex.mainEndMarginAuto && effectiveReverse
|
|
873
|
+
? cflex.mainEndMarginValue
|
|
874
|
+
: cflex.marginL;
|
|
875
|
+
childMarginRight =
|
|
876
|
+
cflex.mainEndMarginAuto && !effectiveReverse
|
|
877
|
+
? cflex.mainEndMarginValue
|
|
878
|
+
: cflex.mainStartMarginAuto && effectiveReverse
|
|
879
|
+
? cflex.mainStartMarginValue
|
|
880
|
+
: cflex.marginR;
|
|
881
|
+
childMarginTop = cflex.marginT;
|
|
882
|
+
childMarginBottom = cflex.marginB;
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
// Column: main axis is vertical (RTL doesn't affect column)
|
|
886
|
+
// In column-reverse, mainStart=bottom(3), mainEnd=top(1)
|
|
887
|
+
childMarginTop =
|
|
888
|
+
cflex.mainStartMarginAuto && !isReverse
|
|
889
|
+
? cflex.mainStartMarginValue
|
|
890
|
+
: cflex.mainEndMarginAuto && isReverse
|
|
891
|
+
? cflex.mainEndMarginValue
|
|
892
|
+
: cflex.marginT;
|
|
893
|
+
childMarginBottom =
|
|
894
|
+
cflex.mainEndMarginAuto && !isReverse
|
|
895
|
+
? cflex.mainEndMarginValue
|
|
896
|
+
: cflex.mainStartMarginAuto && isReverse
|
|
897
|
+
? cflex.mainStartMarginValue
|
|
898
|
+
: cflex.marginB;
|
|
899
|
+
childMarginLeft = cflex.marginL;
|
|
900
|
+
childMarginRight = cflex.marginR;
|
|
901
|
+
}
|
|
902
|
+
// Main axis size comes from flex algorithm (already rounded)
|
|
903
|
+
const childMainSize = cflex.mainSize;
|
|
904
|
+
// Cross axis: determine alignment mode
|
|
905
|
+
let alignment = style.alignItems;
|
|
906
|
+
if (childStyle.alignSelf !== C.ALIGN_AUTO) {
|
|
907
|
+
alignment = childStyle.alignSelf;
|
|
908
|
+
}
|
|
909
|
+
// Cross axis size depends on alignment and child's explicit dimensions
|
|
910
|
+
// IMPORTANT: Resolve percent against parent's cross axis, not child's available
|
|
911
|
+
let childCrossSize;
|
|
912
|
+
const crossDim = isRow ? childStyle.height : childStyle.width;
|
|
913
|
+
const crossMargin = isRow ? childMarginTop + childMarginBottom : childMarginLeft + childMarginRight;
|
|
914
|
+
// Check if parent has definite cross-axis size
|
|
915
|
+
// Parent can have definite cross from either:
|
|
916
|
+
// 1. Explicit style (width/height in points or percent)
|
|
917
|
+
// 2. Definite available space (crossAxisSize is not NaN)
|
|
918
|
+
const parentCrossDim = isRow ? style.height : style.width;
|
|
919
|
+
const parentHasDefiniteCrossStyle = parentCrossDim.unit === C.UNIT_POINT || parentCrossDim.unit === C.UNIT_PERCENT;
|
|
920
|
+
// crossAxisSize comes from available space - if it's a real number, we have a constraint
|
|
921
|
+
const parentHasDefiniteCross = parentHasDefiniteCrossStyle || !Number.isNaN(crossAxisSize);
|
|
922
|
+
if (crossDim.unit === C.UNIT_POINT) {
|
|
923
|
+
// Explicit cross size
|
|
924
|
+
childCrossSize = crossDim.value;
|
|
925
|
+
}
|
|
926
|
+
else if (crossDim.unit === C.UNIT_PERCENT) {
|
|
927
|
+
// Percent of PARENT's cross axis (resolveValue handles NaN -> 0)
|
|
928
|
+
childCrossSize = resolveValue(crossDim, crossAxisSize);
|
|
929
|
+
}
|
|
930
|
+
else if (parentHasDefiniteCross && alignment === C.ALIGN_STRETCH) {
|
|
931
|
+
// Stretch alignment with definite parent cross size - fill the cross axis
|
|
932
|
+
childCrossSize = crossAxisSize - crossMargin;
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
// Non-stretch alignment or no definite cross size - shrink-wrap to content
|
|
936
|
+
childCrossSize = NaN;
|
|
937
|
+
}
|
|
938
|
+
// Apply cross-axis min/max constraints
|
|
939
|
+
const crossMinVal = isRow ? childStyle.minHeight : childStyle.minWidth;
|
|
940
|
+
const crossMaxVal = isRow ? childStyle.maxHeight : childStyle.maxWidth;
|
|
941
|
+
const crossMin = crossMinVal.unit !== C.UNIT_UNDEFINED ? resolveValue(crossMinVal, crossAxisSize) : 0;
|
|
942
|
+
const crossMax = crossMaxVal.unit !== C.UNIT_UNDEFINED ? resolveValue(crossMaxVal, crossAxisSize) : Infinity;
|
|
943
|
+
// Apply constraints - for NaN (shrink-wrap), use min as floor
|
|
944
|
+
if (Number.isNaN(childCrossSize)) {
|
|
945
|
+
// For shrink-wrap, min sets the floor - child will be at least this size
|
|
946
|
+
if (crossMin > 0) {
|
|
947
|
+
childCrossSize = crossMin;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
childCrossSize = Math.max(crossMin, Math.min(crossMax, childCrossSize));
|
|
952
|
+
}
|
|
953
|
+
// Handle intrinsic sizing for auto-sized children
|
|
954
|
+
// For auto main size children, use flex-computed size if flexGrow > 0,
|
|
955
|
+
// otherwise pass remaining available space for shrink-wrap behavior
|
|
956
|
+
const mainDim = isRow ? childStyle.width : childStyle.height;
|
|
957
|
+
const mainIsAutoChild = mainDim.unit === C.UNIT_AUTO || mainDim.unit === C.UNIT_UNDEFINED;
|
|
958
|
+
const hasFlexGrow = cflex.flexGrow > 0;
|
|
959
|
+
// Use flex-computed mainSize for all cases - it includes padding/border as minimum
|
|
960
|
+
// The flex algorithm already computed the proper size based on content/padding/border
|
|
961
|
+
const effectiveMainSize = childMainSize;
|
|
962
|
+
let childWidth = isRow ? effectiveMainSize : childCrossSize;
|
|
963
|
+
let childHeight = isRow ? childCrossSize : effectiveMainSize;
|
|
964
|
+
// Only use measure function for intrinsic sizing when flexGrow is NOT set
|
|
965
|
+
// When flexGrow > 0, the flex algorithm determines size, not the content
|
|
966
|
+
const shouldMeasure = child.hasMeasureFunc() && child.children.length === 0 && !hasFlexGrow;
|
|
967
|
+
if (shouldMeasure) {
|
|
968
|
+
const widthAuto = childStyle.width.unit === C.UNIT_AUTO || childStyle.width.unit === C.UNIT_UNDEFINED;
|
|
969
|
+
const heightAuto = childStyle.height.unit === C.UNIT_AUTO || childStyle.height.unit === C.UNIT_UNDEFINED;
|
|
970
|
+
if (widthAuto || heightAuto) {
|
|
971
|
+
// Call measure function with available space
|
|
972
|
+
const widthMode = widthAuto ? C.MEASURE_MODE_AT_MOST : C.MEASURE_MODE_EXACTLY;
|
|
973
|
+
const heightMode = heightAuto ? C.MEASURE_MODE_UNDEFINED : C.MEASURE_MODE_EXACTLY;
|
|
974
|
+
// For unconstrained dimensions (NaN), use Infinity for measure func
|
|
975
|
+
const rawAvailW = widthAuto
|
|
976
|
+
? isRow
|
|
977
|
+
? mainAxisSize - mainPos // Remaining space after previous children
|
|
978
|
+
: crossAxisSize - crossMargin
|
|
979
|
+
: childStyle.width.value;
|
|
980
|
+
const rawAvailH = heightAuto
|
|
981
|
+
? isRow
|
|
982
|
+
? crossAxisSize - crossMargin
|
|
983
|
+
: mainAxisSize - mainPos // Remaining space for COLUMN
|
|
984
|
+
: childStyle.height.value;
|
|
985
|
+
const availW = Number.isNaN(rawAvailW) ? Infinity : rawAvailW;
|
|
986
|
+
const availH = Number.isNaN(rawAvailH) ? Infinity : rawAvailH;
|
|
987
|
+
// Use cached measure to avoid redundant calls within a layout pass
|
|
988
|
+
const measured = child.cachedMeasure(availW, widthMode, availH, heightMode);
|
|
989
|
+
// For measure function nodes without flexGrow, intrinsic size takes precedence
|
|
990
|
+
if (widthAuto) {
|
|
991
|
+
childWidth = measured.width;
|
|
992
|
+
}
|
|
993
|
+
if (heightAuto) {
|
|
994
|
+
childHeight = measured.height;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
// Child position within content area (fractional for edge-based rounding)
|
|
999
|
+
// For reverse directions (including RTL for row), position from mainPos - childSize
|
|
1000
|
+
// IMPORTANT: In reverse, swap which margin is applied to which side
|
|
1001
|
+
// For RTL row: items flow right-to-left, so right margin becomes trailing
|
|
1002
|
+
// For flex-wrap, add lineCrossOffset to cross-axis position
|
|
1003
|
+
let childX;
|
|
1004
|
+
let childY;
|
|
1005
|
+
if (effectiveReverse) {
|
|
1006
|
+
if (isRow) {
|
|
1007
|
+
// Row-reverse or RTL: items positioned from right
|
|
1008
|
+
// In RTL/reverse, use right margin as trailing margin
|
|
1009
|
+
childX = mainPos - childMainSize - childMarginRight;
|
|
1010
|
+
childY = lineCrossOffset + childMarginTop;
|
|
1011
|
+
}
|
|
1012
|
+
else {
|
|
1013
|
+
// Column-reverse: items positioned from bottom
|
|
1014
|
+
childX = lineCrossOffset + childMarginLeft;
|
|
1015
|
+
childY = mainPos - childMainSize - childMarginTop;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
else {
|
|
1019
|
+
childX = isRow ? mainPos + childMarginLeft : lineCrossOffset + childMarginLeft;
|
|
1020
|
+
childY = isRow ? lineCrossOffset + childMarginTop : mainPos + childMarginTop;
|
|
1021
|
+
}
|
|
1022
|
+
// Edge-based rounding using ABSOLUTE coordinates (Yoga-compatible)
|
|
1023
|
+
// This ensures adjacent elements share exact boundaries without gaps
|
|
1024
|
+
// Key insight: round absolute edges, derive sizes from differences
|
|
1025
|
+
const fractionalLeft = innerLeft + childX;
|
|
1026
|
+
const fractionalTop = innerTop + childY;
|
|
1027
|
+
// Compute position offsets for RELATIVE/STATIC positioned children
|
|
1028
|
+
// These must be included in the absolute position BEFORE rounding (Yoga-compatible)
|
|
1029
|
+
let posOffsetX = 0;
|
|
1030
|
+
let posOffsetY = 0;
|
|
1031
|
+
if (childStyle.positionType === C.POSITION_TYPE_RELATIVE || childStyle.positionType === C.POSITION_TYPE_STATIC) {
|
|
1032
|
+
const relLeftPos = childStyle.position[0];
|
|
1033
|
+
const relTopPos = childStyle.position[1];
|
|
1034
|
+
const relRightPos = childStyle.position[2];
|
|
1035
|
+
const relBottomPos = childStyle.position[3];
|
|
1036
|
+
// Left offset (takes precedence over right)
|
|
1037
|
+
if (relLeftPos.unit !== C.UNIT_UNDEFINED) {
|
|
1038
|
+
posOffsetX = resolveValue(relLeftPos, contentWidth);
|
|
1039
|
+
}
|
|
1040
|
+
else if (relRightPos.unit !== C.UNIT_UNDEFINED) {
|
|
1041
|
+
posOffsetX = -resolveValue(relRightPos, contentWidth);
|
|
1042
|
+
}
|
|
1043
|
+
// Top offset (takes precedence over bottom)
|
|
1044
|
+
if (relTopPos.unit !== C.UNIT_UNDEFINED) {
|
|
1045
|
+
posOffsetY = resolveValue(relTopPos, contentHeight);
|
|
1046
|
+
}
|
|
1047
|
+
else if (relBottomPos.unit !== C.UNIT_UNDEFINED) {
|
|
1048
|
+
posOffsetY = -resolveValue(relBottomPos, contentHeight);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
// Compute ABSOLUTE float positions for edge rounding (including position offsets)
|
|
1052
|
+
// absX/absY are the parent's absolute position from document root
|
|
1053
|
+
// Include BOTH parent's position offset and child's position offset
|
|
1054
|
+
const absChildLeft = absX + marginLeft + parentPosOffsetX + fractionalLeft + posOffsetX;
|
|
1055
|
+
const absChildTop = absY + marginTop + parentPosOffsetY + fractionalTop + posOffsetY;
|
|
1056
|
+
// For main axis: round ABSOLUTE edges and derive size
|
|
1057
|
+
// Only use edge-based rounding when childMainSize is valid (positive)
|
|
1058
|
+
let roundedAbsMainStart;
|
|
1059
|
+
let roundedAbsMainEnd;
|
|
1060
|
+
let edgeBasedMainSize;
|
|
1061
|
+
const useEdgeBasedRounding = childMainSize > 0;
|
|
1062
|
+
// Compute child's box model minimum early (needed for edge-based rounding)
|
|
1063
|
+
// Use resolveEdgeValue to respect logical EDGE_START/END for padding
|
|
1064
|
+
const childPaddingL = resolveEdgeValue(childStyle.padding, 0, childStyle.flexDirection, contentWidth, direction);
|
|
1065
|
+
const childPaddingT = resolveEdgeValue(childStyle.padding, 1, childStyle.flexDirection, contentWidth, direction);
|
|
1066
|
+
const childPaddingR = resolveEdgeValue(childStyle.padding, 2, childStyle.flexDirection, contentWidth, direction);
|
|
1067
|
+
const childPaddingB = resolveEdgeValue(childStyle.padding, 3, childStyle.flexDirection, contentWidth, direction);
|
|
1068
|
+
const childBorderL = resolveEdgeBorderValue(childStyle.border, 0, childStyle.flexDirection, direction);
|
|
1069
|
+
const childBorderT = resolveEdgeBorderValue(childStyle.border, 1, childStyle.flexDirection, direction);
|
|
1070
|
+
const childBorderR = resolveEdgeBorderValue(childStyle.border, 2, childStyle.flexDirection, direction);
|
|
1071
|
+
const childBorderB = resolveEdgeBorderValue(childStyle.border, 3, childStyle.flexDirection, direction);
|
|
1072
|
+
const childMinW = childPaddingL + childPaddingR + childBorderL + childBorderR;
|
|
1073
|
+
const childMinH = childPaddingT + childPaddingB + childBorderT + childBorderB;
|
|
1074
|
+
const childMinMain = isRow ? childMinW : childMinH;
|
|
1075
|
+
// Apply box model constraint to childMainSize before edge rounding
|
|
1076
|
+
const constrainedMainSize = Math.max(childMainSize, childMinMain);
|
|
1077
|
+
if (useEdgeBasedRounding) {
|
|
1078
|
+
if (isRow) {
|
|
1079
|
+
roundedAbsMainStart = Math.round(absChildLeft);
|
|
1080
|
+
roundedAbsMainEnd = Math.round(absChildLeft + constrainedMainSize);
|
|
1081
|
+
edgeBasedMainSize = roundedAbsMainEnd - roundedAbsMainStart;
|
|
1082
|
+
}
|
|
1083
|
+
else {
|
|
1084
|
+
roundedAbsMainStart = Math.round(absChildTop);
|
|
1085
|
+
roundedAbsMainEnd = Math.round(absChildTop + constrainedMainSize);
|
|
1086
|
+
edgeBasedMainSize = roundedAbsMainEnd - roundedAbsMainStart;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
else {
|
|
1090
|
+
// For children without valid main size, use simple rounding
|
|
1091
|
+
roundedAbsMainStart = isRow ? Math.round(absChildLeft) : Math.round(absChildTop);
|
|
1092
|
+
edgeBasedMainSize = childMinMain; // Use minimum size instead of 0
|
|
1093
|
+
}
|
|
1094
|
+
// Calculate child's RELATIVE position (stored in layout)
|
|
1095
|
+
// Yoga behavior: position is rounded locally, size uses absolute edge rounding
|
|
1096
|
+
// This ensures sizes are pixel-perfect at document level while positions remain intuitive
|
|
1097
|
+
const childLeft = Math.round(fractionalLeft + posOffsetX);
|
|
1098
|
+
const childTop = Math.round(fractionalTop + posOffsetY);
|
|
1099
|
+
// Check if cross axis is auto-sized (needed for deciding what to pass to layoutNode)
|
|
1100
|
+
const crossDimForLayoutCall = isRow ? childStyle.height : childStyle.width;
|
|
1101
|
+
const crossIsAutoForLayoutCall = crossDimForLayoutCall.unit === C.UNIT_AUTO || crossDimForLayoutCall.unit === C.UNIT_UNDEFINED;
|
|
1102
|
+
const mainDimForLayoutCall = isRow ? childStyle.width : childStyle.height;
|
|
1103
|
+
const mainIsPercentForLayoutCall = mainDimForLayoutCall.unit === C.UNIT_PERCENT;
|
|
1104
|
+
const crossIsPercentForLayoutCall = crossDimForLayoutCall.unit === C.UNIT_PERCENT;
|
|
1105
|
+
// For auto-sized children (no flexGrow, no measureFunc), pass NaN to let them compute intrinsic size
|
|
1106
|
+
// Otherwise layoutNode would subtract margins from the available size
|
|
1107
|
+
// IMPORTANT: For percent-sized children, pass parent's content size (not child's computed size)
|
|
1108
|
+
// so that grandchildren's percents resolve correctly against the child's actual dimensions.
|
|
1109
|
+
// The child will resolve its own percent against this value, getting the same result the parent computed.
|
|
1110
|
+
//
|
|
1111
|
+
// CRITICAL: When flex distribution changed the child's size (shrinkage/growth applied),
|
|
1112
|
+
// pass the actual childWidth instead of NaN. This ensures layoutNode's fingerprint check
|
|
1113
|
+
// detects the change — otherwise NaN===NaN matches across passes with different flex
|
|
1114
|
+
// distributions, preserving stale overridden dimensions from the previous pass.
|
|
1115
|
+
//
|
|
1116
|
+
// CRITICAL: Measure-func leaf nodes (text) must receive the actual constraint, not NaN.
|
|
1117
|
+
// Their cross-axis size (e.g. height in a row) depends on the main-axis constraint
|
|
1118
|
+
// (e.g. text wrapping width). Passing NaN causes them to measure unconstrained,
|
|
1119
|
+
// producing height=1 instead of the correct wrapped height. The parent's Phase 8
|
|
1120
|
+
// shouldMeasure path computes the correct childWidth/childHeight, but layoutNode
|
|
1121
|
+
// would recompute with NaN and get a different result.
|
|
1122
|
+
const flexDistChanged = child.flex.mainSize !== child.flex.baseSize;
|
|
1123
|
+
const hasMeasureLeaf = child.hasMeasureFunc() && child.children.length === 0;
|
|
1124
|
+
const passWidthToChild = isRow && mainIsAutoChild && !hasFlexGrow && !flexDistChanged && !hasMeasureLeaf
|
|
1125
|
+
? NaN
|
|
1126
|
+
: !isRow && crossIsAutoForLayoutCall && !parentHasDefiniteCross
|
|
1127
|
+
? NaN
|
|
1128
|
+
: isRow && mainIsPercentForLayoutCall
|
|
1129
|
+
? mainAxisSize
|
|
1130
|
+
: !isRow && crossIsPercentForLayoutCall
|
|
1131
|
+
? crossAxisSize
|
|
1132
|
+
: childWidth;
|
|
1133
|
+
const passHeightToChild = !isRow && mainIsAutoChild && !hasFlexGrow && !flexDistChanged && !hasMeasureLeaf
|
|
1134
|
+
? NaN
|
|
1135
|
+
: isRow && crossIsAutoForLayoutCall && !parentHasDefiniteCross
|
|
1136
|
+
? NaN
|
|
1137
|
+
: !isRow && mainIsPercentForLayoutCall
|
|
1138
|
+
? mainAxisSize
|
|
1139
|
+
: isRow && crossIsPercentForLayoutCall
|
|
1140
|
+
? crossAxisSize
|
|
1141
|
+
: childHeight;
|
|
1142
|
+
// Recurse to layout any grandchildren
|
|
1143
|
+
// Pass the child's FLOAT absolute position (margin box start, before child's own margin)
|
|
1144
|
+
// absChildLeft/Top include the child's margins, so subtract them to get margin box start
|
|
1145
|
+
const childAbsX = absChildLeft - childMarginLeft;
|
|
1146
|
+
const childAbsY = absChildTop - childMarginTop;
|
|
1147
|
+
layoutNode(child, passWidthToChild, passHeightToChild, childLeft, childTop, childAbsX, childAbsY, direction);
|
|
1148
|
+
// Enforce box model constraint: child can't be smaller than its padding + border
|
|
1149
|
+
// (using childMinW/childMinH computed earlier for edge-based rounding)
|
|
1150
|
+
if (childWidth < childMinW)
|
|
1151
|
+
childWidth = childMinW;
|
|
1152
|
+
if (childHeight < childMinH)
|
|
1153
|
+
childHeight = childMinH;
|
|
1154
|
+
// Set this child's layout - override what layoutNode computed
|
|
1155
|
+
// Override if any of:
|
|
1156
|
+
// - Child has explicit main dimension AND parent has explicit main dimension (edge-based rounding)
|
|
1157
|
+
// - Child has flexGrow > 0 (flex distribution applied)
|
|
1158
|
+
// - Child has measureFunc (leaf text node)
|
|
1159
|
+
// - Flex distribution actually changed the size (grow or shrink)
|
|
1160
|
+
//
|
|
1161
|
+
// IMPORTANT: Don't override auto-sized containers when flex distribution
|
|
1162
|
+
// didn't change their size. The pre-measurement (Phase 5) computes intrinsic
|
|
1163
|
+
// size at unconstrained main axis, but layoutNode recomputes with actual
|
|
1164
|
+
// cross-axis constraints. For containers with children that wrap text,
|
|
1165
|
+
// layoutNode's result is correct because it accounts for the actual width
|
|
1166
|
+
// after flex distribution of grandchildren. The Phase 5 measureNode pass
|
|
1167
|
+
// measures row children with NaN main width, so text doesn't wrap —
|
|
1168
|
+
// producing height=1 instead of the correct wrapped height.
|
|
1169
|
+
const hasMeasure = child.hasMeasureFunc() && child.children.length === 0;
|
|
1170
|
+
const flexDistributionChangedSize = child.flex.mainSize !== child.flex.baseSize;
|
|
1171
|
+
if ((!mainIsAuto && !mainIsAutoChild) || hasFlexGrow || hasMeasure || flexDistributionChangedSize) {
|
|
1172
|
+
// Use edge-based rounding: size = round(end_edge) - round(start_edge)
|
|
1173
|
+
if (isRow) {
|
|
1174
|
+
_t?.parentOverride(_tn, "main", child.layout.width, edgeBasedMainSize);
|
|
1175
|
+
child.layout.width = edgeBasedMainSize;
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
_t?.parentOverride(_tn, "main", child.layout.height, edgeBasedMainSize);
|
|
1179
|
+
child.layout.height = edgeBasedMainSize;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
// Cross axis: only override for explicit sizing or when we have a real constraint
|
|
1183
|
+
// For auto-sized children, let layoutNode determine the size
|
|
1184
|
+
const crossDimForCheck = isRow ? childStyle.height : childStyle.width;
|
|
1185
|
+
const crossIsAuto = crossDimForCheck.unit === C.UNIT_AUTO || crossDimForCheck.unit === C.UNIT_UNDEFINED;
|
|
1186
|
+
// Only override if child has explicit sizing OR parent has explicit cross size
|
|
1187
|
+
// When parent has auto cross size, let children shrink-wrap first
|
|
1188
|
+
// Note: parentCrossDim and parentHasDefiniteCross already computed above
|
|
1189
|
+
const parentCrossIsAuto = !parentHasDefiniteCross;
|
|
1190
|
+
// Also check if childCrossSize was constrained by min/max - if so, we should override
|
|
1191
|
+
const hasCrossMinMax = crossMinVal.unit !== C.UNIT_UNDEFINED || crossMaxVal.unit !== C.UNIT_UNDEFINED;
|
|
1192
|
+
const shouldOverrideCross = !crossIsAuto ||
|
|
1193
|
+
(!parentCrossIsAuto && alignment === C.ALIGN_STRETCH) ||
|
|
1194
|
+
(hasCrossMinMax && !Number.isNaN(childCrossSize));
|
|
1195
|
+
if (shouldOverrideCross) {
|
|
1196
|
+
if (isRow) {
|
|
1197
|
+
child.layout.height = Math.round(childHeight);
|
|
1198
|
+
}
|
|
1199
|
+
else {
|
|
1200
|
+
child.layout.width = Math.round(childWidth);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
// Store RELATIVE position (within parent's content area), not absolute
|
|
1204
|
+
// This matches Yoga's behavior where getComputedLeft/Top return relative positions
|
|
1205
|
+
// Position offsets are already included in childLeft/childTop via edge-based rounding
|
|
1206
|
+
child.layout.left = childLeft;
|
|
1207
|
+
child.layout.top = childTop;
|
|
1208
|
+
// Update childWidth/childHeight to match actual computed layout for mainPos calculation
|
|
1209
|
+
childWidth = child.layout.width;
|
|
1210
|
+
childHeight = child.layout.height;
|
|
1211
|
+
// Apply cross-axis alignment offset
|
|
1212
|
+
const finalCrossSize = isRow ? child.layout.height : child.layout.width;
|
|
1213
|
+
let crossOffset = 0;
|
|
1214
|
+
// Check for auto margins on cross axis - they override alignment
|
|
1215
|
+
// Use isEdgeAuto to correctly respect logical EDGE_START/END margins
|
|
1216
|
+
const crossStartIndex = isRow ? 1 : 0; // top for row, left for column
|
|
1217
|
+
const crossEndIndex = isRow ? 3 : 2; // bottom for row, right for column
|
|
1218
|
+
const hasAutoStartMargin = isEdgeAuto(childStyle.margin, crossStartIndex, style.flexDirection, direction);
|
|
1219
|
+
const hasAutoEndMargin = isEdgeAuto(childStyle.margin, crossEndIndex, style.flexDirection, direction);
|
|
1220
|
+
const availableCrossSpace = crossAxisSize - finalCrossSize - crossMargin;
|
|
1221
|
+
if (hasAutoStartMargin && hasAutoEndMargin) {
|
|
1222
|
+
// Both auto: center the item
|
|
1223
|
+
// CSS spec: auto margins don't absorb negative free space (clamp to 0)
|
|
1224
|
+
crossOffset = Math.max(0, availableCrossSpace) / 2;
|
|
1225
|
+
}
|
|
1226
|
+
else if (hasAutoStartMargin) {
|
|
1227
|
+
// Auto start margin: push to end
|
|
1228
|
+
// CSS spec: auto margins don't absorb negative free space (clamp to 0)
|
|
1229
|
+
crossOffset = Math.max(0, availableCrossSpace);
|
|
1230
|
+
}
|
|
1231
|
+
else if (hasAutoEndMargin) {
|
|
1232
|
+
// Auto end margin: stay at start (crossOffset = 0)
|
|
1233
|
+
crossOffset = 0;
|
|
1234
|
+
}
|
|
1235
|
+
else {
|
|
1236
|
+
// No auto margins: use alignment
|
|
1237
|
+
switch (alignment) {
|
|
1238
|
+
case C.ALIGN_FLEX_END:
|
|
1239
|
+
crossOffset = availableCrossSpace;
|
|
1240
|
+
break;
|
|
1241
|
+
case C.ALIGN_CENTER:
|
|
1242
|
+
crossOffset = availableCrossSpace / 2;
|
|
1243
|
+
break;
|
|
1244
|
+
case C.ALIGN_BASELINE:
|
|
1245
|
+
// Baseline alignment only applies to row direction
|
|
1246
|
+
// For column direction, it falls through to flex-start (default)
|
|
1247
|
+
if (isRow && hasBaselineAlignment) {
|
|
1248
|
+
// Use pre-computed baseline from Phase 6c (stored in child.flex.baseline)
|
|
1249
|
+
crossOffset = maxBaseline - child.flex.baseline;
|
|
1250
|
+
}
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
if (crossOffset > 0) {
|
|
1255
|
+
if (isRow) {
|
|
1256
|
+
child.layout.top += Math.round(crossOffset);
|
|
1257
|
+
}
|
|
1258
|
+
else {
|
|
1259
|
+
child.layout.left += Math.round(crossOffset);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
// Position advancement: use the right size depending on Phase 8 behavior.
|
|
1263
|
+
// - Phase 8 overrode (explicit size, flexGrow, measure, or flex distribution changed):
|
|
1264
|
+
// Use constrainedMainSize (float) for precise gap/position calculations.
|
|
1265
|
+
// child.layout is edge-rounded (integer), which causes rounding drift in gaps.
|
|
1266
|
+
// - Phase 8 did NOT override (auto-sized container, no grow, no measure):
|
|
1267
|
+
// Use child.layout (from layoutNode), which reflects actual content size.
|
|
1268
|
+
// constrainedMainSize is a stale pre-layout estimate from unconstrained measurement.
|
|
1269
|
+
const phaseEightOverrode = (!mainIsAuto && !mainIsAutoChild) || hasFlexGrow || hasMeasure || flexDistributionChangedSize;
|
|
1270
|
+
const fractionalMainSize = phaseEightOverrode
|
|
1271
|
+
? constrainedMainSize
|
|
1272
|
+
: isRow
|
|
1273
|
+
? child.layout.width
|
|
1274
|
+
: child.layout.height;
|
|
1275
|
+
// Use computed margin values (including auto margins)
|
|
1276
|
+
const totalMainMargin = cflex.mainStartMarginValue + cflex.mainEndMarginValue;
|
|
1277
|
+
log.debug?.(" child %d: mainPos=%d -> top=%d (fractionalMainSize=%d, totalMainMargin=%d)", relIdx, mainPos, child.layout.top, fractionalMainSize, totalMainMargin);
|
|
1278
|
+
if (effectiveReverse) {
|
|
1279
|
+
mainPos -= fractionalMainSize + totalMainMargin;
|
|
1280
|
+
// Add spacing only between items on the SAME LINE (not across line breaks)
|
|
1281
|
+
if (lineChildIdx < currentLineLength - 1) {
|
|
1282
|
+
mainPos -= currentItemSpacing;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
else {
|
|
1286
|
+
mainPos += fractionalMainSize + totalMainMargin;
|
|
1287
|
+
// Add spacing only between items on the SAME LINE (not across line breaks)
|
|
1288
|
+
if (lineChildIdx < currentLineLength - 1) {
|
|
1289
|
+
mainPos += currentItemSpacing;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
relIdx++;
|
|
1293
|
+
lineChildIdx++;
|
|
1294
|
+
}
|
|
1295
|
+
// -----------------------------------------------------------------------
|
|
1296
|
+
// PHASE 9: Shrink-Wrap Auto-Sized Containers
|
|
1297
|
+
// -----------------------------------------------------------------------
|
|
1298
|
+
// For containers without explicit size, compute intrinsic size from children.
|
|
1299
|
+
// For auto-sized containers (including root), shrink-wrap to content
|
|
1300
|
+
// Compute actual used main space from child layouts (not pre-computed flex.mainSize which may be 0)
|
|
1301
|
+
let actualUsedMain = 0;
|
|
1302
|
+
for (const child of node.children) {
|
|
1303
|
+
if (child.flex.relativeIndex < 0)
|
|
1304
|
+
continue;
|
|
1305
|
+
const childMainSize = isRow ? child.layout.width : child.layout.height;
|
|
1306
|
+
const totalMainMargin = child.flex.mainStartMarginValue + child.flex.mainEndMarginValue;
|
|
1307
|
+
actualUsedMain += childMainSize + totalMainMargin;
|
|
1308
|
+
}
|
|
1309
|
+
actualUsedMain += totalGaps;
|
|
1310
|
+
if (isRow && style.width.unit !== C.UNIT_POINT && style.width.unit !== C.UNIT_PERCENT) {
|
|
1311
|
+
// Auto-width row: shrink-wrap to content
|
|
1312
|
+
nodeWidth = actualUsedMain + innerLeft + innerRight;
|
|
1313
|
+
}
|
|
1314
|
+
if (!isRow && style.height.unit !== C.UNIT_POINT && style.height.unit !== C.UNIT_PERCENT) {
|
|
1315
|
+
// Auto-height column: shrink-wrap to content
|
|
1316
|
+
nodeHeight = actualUsedMain + innerTop + innerBottom;
|
|
1317
|
+
}
|
|
1318
|
+
// For cross axis, find the max child size
|
|
1319
|
+
// CSS spec: percentage margins resolve against containing block's WIDTH only
|
|
1320
|
+
// Use resolveEdgeValue to respect logical EDGE_START/END
|
|
1321
|
+
let maxCrossSize = 0;
|
|
1322
|
+
for (const child of node.children) {
|
|
1323
|
+
if (child.flex.relativeIndex < 0)
|
|
1324
|
+
continue;
|
|
1325
|
+
const childCross = isRow ? child.layout.height : child.layout.width;
|
|
1326
|
+
const childMargin = isRow
|
|
1327
|
+
? resolveEdgeValue(child.style.margin, 1, style.flexDirection, contentWidth, direction) +
|
|
1328
|
+
resolveEdgeValue(child.style.margin, 3, style.flexDirection, contentWidth, direction)
|
|
1329
|
+
: resolveEdgeValue(child.style.margin, 0, style.flexDirection, contentWidth, direction) +
|
|
1330
|
+
resolveEdgeValue(child.style.margin, 2, style.flexDirection, contentWidth, direction);
|
|
1331
|
+
maxCrossSize = Math.max(maxCrossSize, childCross + childMargin);
|
|
1332
|
+
}
|
|
1333
|
+
// Cross-axis shrink-wrap for auto-sized dimension
|
|
1334
|
+
// Only shrink-wrap when the available dimension is NaN (unconstrained)
|
|
1335
|
+
// When availableHeight/Width is defined, Yoga uses it for AUTO-sized root nodes
|
|
1336
|
+
if (isRow &&
|
|
1337
|
+
style.height.unit !== C.UNIT_POINT &&
|
|
1338
|
+
style.height.unit !== C.UNIT_PERCENT &&
|
|
1339
|
+
Number.isNaN(availableHeight)) {
|
|
1340
|
+
// Auto-height row: shrink-wrap to max child height
|
|
1341
|
+
nodeHeight = maxCrossSize + innerTop + innerBottom;
|
|
1342
|
+
}
|
|
1343
|
+
if (!isRow &&
|
|
1344
|
+
style.width.unit !== C.UNIT_POINT &&
|
|
1345
|
+
style.width.unit !== C.UNIT_PERCENT &&
|
|
1346
|
+
Number.isNaN(availableWidth)) {
|
|
1347
|
+
// Auto-width column: shrink-wrap to max child width
|
|
1348
|
+
nodeWidth = maxCrossSize + innerLeft + innerRight;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
// Re-apply min/max constraints after any shrink-wrap adjustments
|
|
1352
|
+
// This ensures containers don't violate their constraints after auto-sizing
|
|
1353
|
+
nodeWidth = applyMinMax(nodeWidth, style.minWidth, style.maxWidth, availableWidth);
|
|
1354
|
+
nodeHeight = applyMinMax(nodeHeight, style.minHeight, style.maxHeight, availableHeight);
|
|
1355
|
+
// Re-enforce box model constraint: minimum size = padding + border
|
|
1356
|
+
// This must be applied AFTER applyMinMax since min/max can't reduce below padding+border
|
|
1357
|
+
if (!Number.isNaN(nodeWidth) && nodeWidth < minInnerWidth) {
|
|
1358
|
+
nodeWidth = minInnerWidth;
|
|
1359
|
+
}
|
|
1360
|
+
if (!Number.isNaN(nodeHeight) && nodeHeight < minInnerHeight) {
|
|
1361
|
+
nodeHeight = minInnerHeight;
|
|
1362
|
+
}
|
|
1363
|
+
// =========================================================================
|
|
1364
|
+
// PHASE 10: Final Output - Set Node Layout
|
|
1365
|
+
// =========================================================================
|
|
1366
|
+
// Use edge-based rounding (Yoga-compatible): round absolute edges and derive sizes.
|
|
1367
|
+
// This ensures adjacent elements share exact boundaries without pixel gaps.
|
|
1368
|
+
// Set this node's layout using edge-based rounding (Yoga-compatible)
|
|
1369
|
+
// Use parentPosOffsetX/Y computed earlier (includes position offsets)
|
|
1370
|
+
// Compute absolute positions for edge-based rounding
|
|
1371
|
+
const absNodeLeft = absX + marginLeft + parentPosOffsetX;
|
|
1372
|
+
const absNodeTop = absY + marginTop + parentPosOffsetY;
|
|
1373
|
+
const absNodeRight = absNodeLeft + nodeWidth;
|
|
1374
|
+
const absNodeBottom = absNodeTop + nodeHeight;
|
|
1375
|
+
// Round edges and derive sizes (Yoga algorithm)
|
|
1376
|
+
const roundedAbsLeft = Math.round(absNodeLeft);
|
|
1377
|
+
const roundedAbsTop = Math.round(absNodeTop);
|
|
1378
|
+
const roundedAbsRight = Math.round(absNodeRight);
|
|
1379
|
+
const roundedAbsBottom = Math.round(absNodeBottom);
|
|
1380
|
+
layout.width = roundedAbsRight - roundedAbsLeft;
|
|
1381
|
+
layout.height = roundedAbsBottom - roundedAbsTop;
|
|
1382
|
+
// Position is relative to parent, derived from absolute rounding
|
|
1383
|
+
const roundedAbsParentLeft = Math.round(absX);
|
|
1384
|
+
const roundedAbsParentTop = Math.round(absY);
|
|
1385
|
+
layout.left = roundedAbsLeft - roundedAbsParentLeft;
|
|
1386
|
+
layout.top = roundedAbsTop - roundedAbsParentTop;
|
|
1387
|
+
// =========================================================================
|
|
1388
|
+
// PHASE 11: Layout Absolute Children
|
|
1389
|
+
// =========================================================================
|
|
1390
|
+
// Absolute children are positioned relative to the padding box, not content box.
|
|
1391
|
+
// They don't participate in flex layout - they're laid out independently.
|
|
1392
|
+
// Layout absolute children - handle left/right/top/bottom offsets
|
|
1393
|
+
// Absolute positioning uses the PADDING BOX as the containing block
|
|
1394
|
+
// (inside border but INCLUDING padding, not the content box)
|
|
1395
|
+
const absInnerLeft = borderLeft;
|
|
1396
|
+
const absInnerTop = borderTop;
|
|
1397
|
+
const absInnerRight = borderRight;
|
|
1398
|
+
const absInnerBottom = borderBottom;
|
|
1399
|
+
const absPaddingBoxW = nodeWidth - absInnerLeft - absInnerRight;
|
|
1400
|
+
const absPaddingBoxH = nodeHeight - absInnerTop - absInnerBottom;
|
|
1401
|
+
// Content box dimensions for percentage resolution of absolute children
|
|
1402
|
+
const absContentBoxW = absPaddingBoxW - paddingLeft - paddingRight;
|
|
1403
|
+
const absContentBoxH = absPaddingBoxH - paddingTop - paddingBottom;
|
|
1404
|
+
// Layout absolute positioned children (relativeIndex === -1 but not display:none)
|
|
1405
|
+
for (const child of node.children) {
|
|
1406
|
+
if (child.style.display === C.DISPLAY_NONE)
|
|
1407
|
+
continue;
|
|
1408
|
+
if (child.style.positionType !== C.POSITION_TYPE_ABSOLUTE)
|
|
1409
|
+
continue;
|
|
1410
|
+
const childStyle = child.style;
|
|
1411
|
+
// CSS spec: percentage margins resolve against containing block's WIDTH only
|
|
1412
|
+
// Use resolveEdgeValue to respect logical EDGE_START/END
|
|
1413
|
+
// Note: Auto margins will resolve to 0 here, we handle them separately below
|
|
1414
|
+
const childMarginLeft = resolveEdgeValue(childStyle.margin, 0, style.flexDirection, nodeWidth, direction);
|
|
1415
|
+
const childMarginTop = resolveEdgeValue(childStyle.margin, 1, style.flexDirection, nodeWidth, direction);
|
|
1416
|
+
const childMarginRight = resolveEdgeValue(childStyle.margin, 2, style.flexDirection, nodeWidth, direction);
|
|
1417
|
+
const childMarginBottom = resolveEdgeValue(childStyle.margin, 3, style.flexDirection, nodeWidth, direction);
|
|
1418
|
+
// Check for auto margins (used for centering absolute children)
|
|
1419
|
+
const hasAutoMarginLeft = isEdgeAuto(childStyle.margin, 0, style.flexDirection, direction);
|
|
1420
|
+
const hasAutoMarginRight = isEdgeAuto(childStyle.margin, 2, style.flexDirection, direction);
|
|
1421
|
+
const hasAutoMarginTop = isEdgeAuto(childStyle.margin, 1, style.flexDirection, direction);
|
|
1422
|
+
const hasAutoMarginBottom = isEdgeAuto(childStyle.margin, 3, style.flexDirection, direction);
|
|
1423
|
+
// Position offsets from setPosition(edge, value)
|
|
1424
|
+
const leftPos = childStyle.position[0];
|
|
1425
|
+
const topPos = childStyle.position[1];
|
|
1426
|
+
const rightPos = childStyle.position[2];
|
|
1427
|
+
const bottomPos = childStyle.position[3];
|
|
1428
|
+
const hasLeft = leftPos.unit !== C.UNIT_UNDEFINED;
|
|
1429
|
+
const hasRight = rightPos.unit !== C.UNIT_UNDEFINED;
|
|
1430
|
+
const hasTop = topPos.unit !== C.UNIT_UNDEFINED;
|
|
1431
|
+
const hasBottom = bottomPos.unit !== C.UNIT_UNDEFINED;
|
|
1432
|
+
const leftOffset = resolveValue(leftPos, nodeWidth);
|
|
1433
|
+
const topOffset = resolveValue(topPos, nodeHeight);
|
|
1434
|
+
const rightOffset = resolveValue(rightPos, nodeWidth);
|
|
1435
|
+
const bottomOffset = resolveValue(bottomPos, nodeHeight);
|
|
1436
|
+
// Calculate available size for absolute child using padding box
|
|
1437
|
+
const contentW = absPaddingBoxW;
|
|
1438
|
+
const contentH = absPaddingBoxH;
|
|
1439
|
+
// Determine child width
|
|
1440
|
+
// - If both left and right set with auto width: stretch to fill
|
|
1441
|
+
// - If auto width but NOT both left and right: shrink to intrinsic (NaN)
|
|
1442
|
+
// - For percentage width: resolve against content box
|
|
1443
|
+
// - Otherwise (explicit width): use available width as constraint
|
|
1444
|
+
let childAvailWidth;
|
|
1445
|
+
const widthIsAuto = childStyle.width.unit === C.UNIT_AUTO || childStyle.width.unit === C.UNIT_UNDEFINED;
|
|
1446
|
+
const widthIsPercent = childStyle.width.unit === C.UNIT_PERCENT;
|
|
1447
|
+
if (widthIsAuto && hasLeft && hasRight) {
|
|
1448
|
+
childAvailWidth = contentW - leftOffset - rightOffset - childMarginLeft - childMarginRight;
|
|
1449
|
+
}
|
|
1450
|
+
else if (widthIsAuto) {
|
|
1451
|
+
childAvailWidth = NaN; // Shrink to intrinsic size
|
|
1452
|
+
}
|
|
1453
|
+
else if (widthIsPercent) {
|
|
1454
|
+
// Percentage widths resolve against content box (inside padding)
|
|
1455
|
+
childAvailWidth = absContentBoxW;
|
|
1456
|
+
}
|
|
1457
|
+
else {
|
|
1458
|
+
childAvailWidth = contentW;
|
|
1459
|
+
}
|
|
1460
|
+
// Determine child height
|
|
1461
|
+
// - If both top and bottom set with auto height: stretch to fill
|
|
1462
|
+
// - If auto height but NOT both top and bottom: shrink to intrinsic (NaN)
|
|
1463
|
+
// - For percentage height: resolve against content box
|
|
1464
|
+
// - Otherwise (explicit height): use available height as constraint
|
|
1465
|
+
let childAvailHeight;
|
|
1466
|
+
const heightIsAuto = childStyle.height.unit === C.UNIT_AUTO || childStyle.height.unit === C.UNIT_UNDEFINED;
|
|
1467
|
+
const heightIsPercent = childStyle.height.unit === C.UNIT_PERCENT;
|
|
1468
|
+
if (heightIsAuto && hasTop && hasBottom) {
|
|
1469
|
+
childAvailHeight = contentH - topOffset - bottomOffset - childMarginTop - childMarginBottom;
|
|
1470
|
+
}
|
|
1471
|
+
else if (heightIsAuto) {
|
|
1472
|
+
childAvailHeight = NaN; // Shrink to intrinsic size
|
|
1473
|
+
}
|
|
1474
|
+
else if (heightIsPercent) {
|
|
1475
|
+
// Percentage heights resolve against content box (inside padding)
|
|
1476
|
+
childAvailHeight = absContentBoxH;
|
|
1477
|
+
}
|
|
1478
|
+
else {
|
|
1479
|
+
childAvailHeight = contentH;
|
|
1480
|
+
}
|
|
1481
|
+
// Compute child position
|
|
1482
|
+
let childX = childMarginLeft + leftOffset;
|
|
1483
|
+
let childY = childMarginTop + topOffset;
|
|
1484
|
+
// First, layout the child to get its dimensions
|
|
1485
|
+
// Use padding box origin (absInnerLeft/Top = border only)
|
|
1486
|
+
// Compute child's absolute position (margin box start, before child's own margin)
|
|
1487
|
+
// Parent's padding box = absX + marginLeft + borderLeft = absX + marginLeft + absInnerLeft
|
|
1488
|
+
// Child's margin box = parent's padding box + leftOffset
|
|
1489
|
+
const childAbsX = absX + marginLeft + absInnerLeft + leftOffset;
|
|
1490
|
+
const childAbsY = absY + marginTop + absInnerTop + topOffset;
|
|
1491
|
+
// Preserve NaN for shrink-wrap mode - only clamp real numbers to 0
|
|
1492
|
+
const clampIfNumber = (v) => (Number.isNaN(v) ? NaN : Math.max(0, v));
|
|
1493
|
+
layoutNode(child, clampIfNumber(childAvailWidth), clampIfNumber(childAvailHeight), layout.left + absInnerLeft + childX, layout.top + absInnerTop + childY, childAbsX, childAbsY, direction);
|
|
1494
|
+
// Now compute final position based on right/bottom if left/top not set
|
|
1495
|
+
const childWidth = child.layout.width;
|
|
1496
|
+
const childHeight = child.layout.height;
|
|
1497
|
+
// Apply alignment when no explicit position set
|
|
1498
|
+
// For absolute children, align-items/justify-content apply when no position offsets
|
|
1499
|
+
if (!hasLeft && !hasRight) {
|
|
1500
|
+
// No horizontal position - use align-items (for row) or justify-content (for column)
|
|
1501
|
+
// Default column direction: cross-axis is horizontal, use alignItems
|
|
1502
|
+
let alignment = style.alignItems;
|
|
1503
|
+
if (childStyle.alignSelf !== C.ALIGN_AUTO) {
|
|
1504
|
+
alignment = childStyle.alignSelf;
|
|
1505
|
+
}
|
|
1506
|
+
const freeSpaceX = contentW - childWidth - childMarginLeft - childMarginRight;
|
|
1507
|
+
switch (alignment) {
|
|
1508
|
+
case C.ALIGN_CENTER:
|
|
1509
|
+
childX = childMarginLeft + freeSpaceX / 2;
|
|
1510
|
+
break;
|
|
1511
|
+
case C.ALIGN_FLEX_END:
|
|
1512
|
+
childX = childMarginLeft + freeSpaceX;
|
|
1513
|
+
break;
|
|
1514
|
+
case C.ALIGN_STRETCH:
|
|
1515
|
+
// Stretch: already handled by setting width to fill
|
|
1516
|
+
break;
|
|
1517
|
+
default: // FLEX_START
|
|
1518
|
+
childX = childMarginLeft;
|
|
1519
|
+
break;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
else if (!hasLeft && hasRight) {
|
|
1523
|
+
// Position from right edge
|
|
1524
|
+
childX = contentW - rightOffset - childMarginRight - childWidth;
|
|
1525
|
+
}
|
|
1526
|
+
else if (hasLeft && hasRight) {
|
|
1527
|
+
// Both left and right are set
|
|
1528
|
+
if (widthIsAuto) {
|
|
1529
|
+
// Stretch width already handled above
|
|
1530
|
+
child.layout.width = Math.round(childAvailWidth);
|
|
1531
|
+
}
|
|
1532
|
+
else if (hasAutoMarginLeft || hasAutoMarginRight) {
|
|
1533
|
+
// Auto margins absorb remaining space for centering
|
|
1534
|
+
// CSS spec: auto margins don't absorb negative free space (clamp to 0)
|
|
1535
|
+
const freeSpace = Math.max(0, contentW - leftOffset - rightOffset - childWidth);
|
|
1536
|
+
if (hasAutoMarginLeft && hasAutoMarginRight) {
|
|
1537
|
+
// Both auto: center
|
|
1538
|
+
childX = leftOffset + freeSpace / 2;
|
|
1539
|
+
}
|
|
1540
|
+
else if (hasAutoMarginLeft) {
|
|
1541
|
+
// Only left auto: push to right
|
|
1542
|
+
childX = leftOffset + freeSpace;
|
|
1543
|
+
}
|
|
1544
|
+
// Only right auto: childX already set to leftOffset + childMarginLeft
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
if (!hasTop && !hasBottom) {
|
|
1548
|
+
// No vertical position - use justify-content (for row) or align-items (for column)
|
|
1549
|
+
// Default column direction: main-axis is vertical, use justifyContent
|
|
1550
|
+
const freeSpaceY = contentH - childHeight - childMarginTop - childMarginBottom;
|
|
1551
|
+
switch (style.justifyContent) {
|
|
1552
|
+
case C.JUSTIFY_CENTER:
|
|
1553
|
+
childY = childMarginTop + freeSpaceY / 2;
|
|
1554
|
+
break;
|
|
1555
|
+
case C.JUSTIFY_FLEX_END:
|
|
1556
|
+
childY = childMarginTop + freeSpaceY;
|
|
1557
|
+
break;
|
|
1558
|
+
default: // FLEX_START
|
|
1559
|
+
childY = childMarginTop;
|
|
1560
|
+
break;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
else if (!hasTop && hasBottom) {
|
|
1564
|
+
// Position from bottom edge
|
|
1565
|
+
childY = contentH - bottomOffset - childMarginBottom - childHeight;
|
|
1566
|
+
}
|
|
1567
|
+
else if (hasTop && hasBottom) {
|
|
1568
|
+
// Both top and bottom are set
|
|
1569
|
+
if (heightIsAuto) {
|
|
1570
|
+
// Stretch height already handled above
|
|
1571
|
+
child.layout.height = Math.round(childAvailHeight);
|
|
1572
|
+
}
|
|
1573
|
+
else if (hasAutoMarginTop || hasAutoMarginBottom) {
|
|
1574
|
+
// Auto margins absorb remaining space for centering
|
|
1575
|
+
// CSS spec: auto margins don't absorb negative free space (clamp to 0)
|
|
1576
|
+
const freeSpace = Math.max(0, contentH - topOffset - bottomOffset - childHeight);
|
|
1577
|
+
if (hasAutoMarginTop && hasAutoMarginBottom) {
|
|
1578
|
+
// Both auto: center
|
|
1579
|
+
childY = topOffset + freeSpace / 2;
|
|
1580
|
+
}
|
|
1581
|
+
else if (hasAutoMarginTop) {
|
|
1582
|
+
// Only top auto: push to bottom
|
|
1583
|
+
childY = topOffset + freeSpace;
|
|
1584
|
+
}
|
|
1585
|
+
// Only bottom auto: childY already set to topOffset + childMarginTop
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
// Set final position (relative to container padding box)
|
|
1589
|
+
child.layout.left = Math.round(absInnerLeft + childX);
|
|
1590
|
+
child.layout.top = Math.round(absInnerTop + childY);
|
|
1591
|
+
}
|
|
1592
|
+
// Update constraint fingerprint - layout is now valid for these constraints
|
|
1593
|
+
flex.lastAvailW = availableWidth;
|
|
1594
|
+
flex.lastAvailH = availableHeight;
|
|
1595
|
+
flex.lastOffsetX = offsetX;
|
|
1596
|
+
flex.lastOffsetY = offsetY;
|
|
1597
|
+
flex.lastDir = direction;
|
|
1598
|
+
flex.layoutValid = true;
|
|
1599
|
+
_t?.layoutExit(_tn, layout.width, layout.height);
|
|
1600
|
+
}
|
|
1601
|
+
//# sourceMappingURL=layout-zero.js.map
|