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