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.
Files changed (73) hide show
  1. package/README.md +2 -0
  2. package/package.json +14 -22
  3. package/src/classic/layout.ts +2 -2
  4. package/src/layout-helpers.ts +2 -2
  5. package/dist/classic/layout.d.ts +0 -57
  6. package/dist/classic/layout.d.ts.map +0 -1
  7. package/dist/classic/layout.js +0 -1567
  8. package/dist/classic/layout.js.map +0 -1
  9. package/dist/classic/node.d.ts +0 -648
  10. package/dist/classic/node.d.ts.map +0 -1
  11. package/dist/classic/node.js +0 -1002
  12. package/dist/classic/node.js.map +0 -1
  13. package/dist/constants.d.ts +0 -59
  14. package/dist/constants.d.ts.map +0 -1
  15. package/dist/constants.js +0 -71
  16. package/dist/constants.js.map +0 -1
  17. package/dist/index-classic.d.ts +0 -30
  18. package/dist/index-classic.d.ts.map +0 -1
  19. package/dist/index-classic.js +0 -57
  20. package/dist/index-classic.js.map +0 -1
  21. package/dist/index.d.ts +0 -30
  22. package/dist/index.d.ts.map +0 -1
  23. package/dist/index.js +0 -57
  24. package/dist/index.js.map +0 -1
  25. package/dist/layout-flex-lines.d.ts +0 -77
  26. package/dist/layout-flex-lines.d.ts.map +0 -1
  27. package/dist/layout-flex-lines.js +0 -317
  28. package/dist/layout-flex-lines.js.map +0 -1
  29. package/dist/layout-helpers.d.ts +0 -45
  30. package/dist/layout-helpers.d.ts.map +0 -1
  31. package/dist/layout-helpers.js +0 -103
  32. package/dist/layout-helpers.js.map +0 -1
  33. package/dist/layout-measure.d.ts +0 -25
  34. package/dist/layout-measure.d.ts.map +0 -1
  35. package/dist/layout-measure.js +0 -231
  36. package/dist/layout-measure.js.map +0 -1
  37. package/dist/layout-stats.d.ts +0 -19
  38. package/dist/layout-stats.d.ts.map +0 -1
  39. package/dist/layout-stats.js +0 -37
  40. package/dist/layout-stats.js.map +0 -1
  41. package/dist/layout-traversal.d.ts +0 -28
  42. package/dist/layout-traversal.d.ts.map +0 -1
  43. package/dist/layout-traversal.js +0 -65
  44. package/dist/layout-traversal.js.map +0 -1
  45. package/dist/layout-zero.d.ts +0 -26
  46. package/dist/layout-zero.d.ts.map +0 -1
  47. package/dist/layout-zero.js +0 -1757
  48. package/dist/layout-zero.js.map +0 -1
  49. package/dist/logger.d.ts +0 -14
  50. package/dist/logger.d.ts.map +0 -1
  51. package/dist/logger.js +0 -61
  52. package/dist/logger.js.map +0 -1
  53. package/dist/node-zero.d.ts +0 -702
  54. package/dist/node-zero.d.ts.map +0 -1
  55. package/dist/node-zero.js +0 -1268
  56. package/dist/node-zero.js.map +0 -1
  57. package/dist/testing.d.ts +0 -69
  58. package/dist/testing.d.ts.map +0 -1
  59. package/dist/testing.js +0 -179
  60. package/dist/testing.js.map +0 -1
  61. package/dist/trace.d.ts +0 -74
  62. package/dist/trace.d.ts.map +0 -1
  63. package/dist/trace.js +0 -191
  64. package/dist/trace.js.map +0 -1
  65. package/dist/types.d.ts +0 -170
  66. package/dist/types.d.ts.map +0 -1
  67. package/dist/types.js +0 -43
  68. package/dist/types.js.map +0 -1
  69. package/dist/utils.d.ts +0 -49
  70. package/dist/utils.d.ts.map +0 -1
  71. package/dist/utils.js +0 -222
  72. package/dist/utils.js.map +0 -1
  73. package/src/beorn-logger.d.ts +0 -10
@@ -1,1757 +0,0 @@
1
- /**
2
- * Flexily 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
- // baselineZoneHeight: the effective cross-axis size that non-baseline children
610
- // align within when baseline alignment is present. This is max(maxBaseline, maxChildHeight).
611
- // Only meaningful when alignItems != ALIGN_BASELINE but some children have alignSelf=baseline.
612
- let baselineZoneHeight = 0;
613
- const alignItemsIsBaseline = style.alignItems === C.ALIGN_BASELINE;
614
- if (hasBaselineAlignment && isRow) {
615
- // First pass: compute each child's baseline and find the maximum
616
- let maxChildHeight = 0;
617
- for (const child of node.children) {
618
- if (child.flex.relativeIndex < 0)
619
- continue;
620
- const childStyle = child.style;
621
- // Get cross-axis (top) margin for this child - use cached value
622
- const topMargin = child.flex.marginT;
623
- // Compute child's dimensions - need to do a mini-layout or use the cached size
624
- // For children with explicit dimensions, use those
625
- // For auto-sized children, we need to layout them first
626
- let childWidth;
627
- let childHeight;
628
- const widthDim = childStyle.width;
629
- const heightDim = childStyle.height;
630
- // Get width for baseline function
631
- if (widthDim.unit === C.UNIT_POINT) {
632
- childWidth = widthDim.value;
633
- }
634
- else if (widthDim.unit === C.UNIT_PERCENT && !Number.isNaN(mainAxisSize)) {
635
- childWidth = mainAxisSize * (widthDim.value / 100);
636
- }
637
- else {
638
- childWidth = child.flex.mainSize;
639
- }
640
- // Get height for baseline
641
- if (heightDim.unit === C.UNIT_POINT) {
642
- childHeight = heightDim.value;
643
- }
644
- else if (heightDim.unit === C.UNIT_PERCENT && !Number.isNaN(crossAxisSize)) {
645
- childHeight = crossAxisSize * (heightDim.value / 100);
646
- }
647
- else {
648
- // Auto height - need to layout to get intrinsic size
649
- // Check cache first to avoid redundant recursive calls
650
- const cached = child.getCachedLayout(child.flex.mainSize, NaN);
651
- if (cached) {
652
- incLayoutCacheHits();
653
- childWidth = cached.width;
654
- childHeight = cached.height;
655
- }
656
- else {
657
- // Use measureNode for sizing-only pass (faster than full layoutNode)
658
- // Save layout before measureNode — it overwrites node.layout.width/height
659
- // with intrinsic measurements. Without save/restore, layoutNode's fingerprint
660
- // check in Phase 9 would skip re-computation and preserve corrupted values.
661
- const savedW = child.layout.width;
662
- const savedH = child.layout.height;
663
- measureNode(child, child.flex.mainSize, NaN);
664
- childWidth = child.layout.width;
665
- childHeight = child.layout.height;
666
- child.layout.width = savedW;
667
- child.layout.height = savedH;
668
- child.setCachedLayout(child.flex.mainSize, NaN, childWidth, childHeight);
669
- }
670
- }
671
- // Compute baseline: use baselineFunc if available, otherwise use bottom of content box
672
- // Store directly in child.flex.baseline (zero-alloc)
673
- if (child.baselineFunc !== null) {
674
- // Custom baseline function provided (e.g., for text nodes)
675
- child.flex.baseline = topMargin + child.baselineFunc(childWidth, childHeight);
676
- }
677
- else {
678
- // Fallback: bottom of content box (default for non-text elements)
679
- // Note: We don't recursively propagate first-child baselines to avoid O(n^depth) cost
680
- // This is a simplification from CSS spec but acceptable for TUI use cases
681
- child.flex.baseline = topMargin + childHeight;
682
- }
683
- // Track max child height (including margin) for baseline zone calculation
684
- maxChildHeight = Math.max(maxChildHeight, topMargin + childHeight + child.flex.marginB);
685
- // When alignItems is baseline, ALL children participate in baseline computation.
686
- // When alignItems is NOT baseline, only children with alignSelf=baseline participate.
687
- // This matches Yoga's behavior: non-baseline children are positioned within the
688
- // "baseline zone" (the effective height determined by baseline-aligned children),
689
- // not the full container cross-axis.
690
- if (alignItemsIsBaseline || childStyle.alignSelf === C.ALIGN_BASELINE) {
691
- maxBaseline = Math.max(maxBaseline, child.flex.baseline);
692
- }
693
- }
694
- // Baseline zone height: the max of maxBaseline and the tallest child.
695
- // Non-baseline children are aligned within this zone, not the full container.
696
- baselineZoneHeight = Math.max(maxBaseline, maxChildHeight);
697
- }
698
- // -----------------------------------------------------------------------
699
- // PHASE 7a: Estimate Flex Line Cross-Axis Sizes (Tentative)
700
- // -----------------------------------------------------------------------
701
- // Estimate cross-axis size of each flex line from definite child dimensions.
702
- // Auto-sized children use 0 here; actual sizes computed during Phase 8.
703
- // These are tentative values used for alignContent distribution.
704
- // Compute line cross-axis sizes and offsets for flex-wrap
705
- // Each child already has lineIndex set by breakIntoLines
706
- // Use pre-allocated _lineCrossOffsets and _lineCrossSizes arrays
707
- let cumulativeCrossOffset = 0;
708
- const isWrapReverse = style.flexWrap === C.WRAP_WRAP_REVERSE;
709
- for (let lineIdx = 0; lineIdx < numLines; lineIdx++) {
710
- _lineCrossOffsets[lineIdx] = cumulativeCrossOffset;
711
- // Calculate max cross size for this line using pre-collected _lineChildren
712
- const lineChildren = _lineChildren[lineIdx];
713
- const lineLength = lineChildren.length;
714
- let maxLineCross = 0;
715
- for (let i = 0; i < lineLength; i++) {
716
- const child = lineChildren[i];
717
- // Estimate child cross size (will be computed more precisely during layout)
718
- const childStyle = child.style;
719
- const crossDim = isRow ? childStyle.height : childStyle.width;
720
- // Use cached margins
721
- const crossMarginStart = isRow ? child.flex.marginT : child.flex.marginL;
722
- const crossMarginEnd = isRow ? child.flex.marginB : child.flex.marginR;
723
- let childCross = 0;
724
- if (crossDim.unit === C.UNIT_POINT) {
725
- childCross = crossDim.value;
726
- }
727
- else if (crossDim.unit === C.UNIT_PERCENT && !Number.isNaN(crossAxisSize)) {
728
- childCross = crossAxisSize * (crossDim.value / 100);
729
- }
730
- else if (child.hasMeasureFunc()) {
731
- // Auto-sized with measureFunc: get tentative cross size from cached measure.
732
- // Phase 5 already called cachedMeasure, so this is typically a cache hit (no alloc).
733
- const crossMargin = crossMarginStart + crossMarginEnd;
734
- const availCross = Number.isNaN(crossAxisSize) ? Infinity : crossAxisSize - crossMargin;
735
- const mW = isRow ? mainAxisSize : availCross;
736
- const mH = isRow ? availCross : mainAxisSize;
737
- const mWMode = Number.isNaN(mW) ? C.MEASURE_MODE_UNDEFINED : C.MEASURE_MODE_AT_MOST;
738
- const mHMode = Number.isNaN(mH) ? C.MEASURE_MODE_UNDEFINED : C.MEASURE_MODE_AT_MOST;
739
- const measured = child.cachedMeasure(Number.isNaN(mW) ? Infinity : mW, mWMode, Number.isNaN(mH) ? Infinity : mH, mHMode);
740
- if (measured) {
741
- childCross = isRow ? measured.height : measured.width;
742
- }
743
- }
744
- maxLineCross = Math.max(maxLineCross, childCross + crossMarginStart + crossMarginEnd);
745
- }
746
- // Use measured max cross size. If all children are auto-sized (maxLineCross === 0),
747
- // use 0 — NOT crossAxisSize/numLines, which would consume all free space and
748
- // prevent alignContent from distributing it. Actual sizes are computed in Phase 8.
749
- const lineCrossSize = maxLineCross;
750
- _lineCrossSizes[lineIdx] = lineCrossSize;
751
- cumulativeCrossOffset += lineCrossSize + crossGap;
752
- }
753
- // -----------------------------------------------------------------------
754
- // PHASE 7b: Apply alignContent
755
- // -----------------------------------------------------------------------
756
- // Distribute flex lines within the container's cross-axis.
757
- // Only applies when flex-wrap creates multiple lines.
758
- // Apply alignContent to distribute lines in the cross axis
759
- // Note: While CSS spec says alignContent only applies to multi-line containers,
760
- // Yoga applies ALIGN_STRETCH to single-line layouts as well. We match Yoga behavior.
761
- if (!Number.isNaN(crossAxisSize) && numLines > 0) {
762
- const totalLineCrossSize = cumulativeCrossOffset - crossGap; // Remove trailing gap
763
- const freeSpace = crossAxisSize - totalLineCrossSize;
764
- const alignContent = style.alignContent;
765
- // Reset offsets based on alignContent
766
- if (freeSpace > 0 || alignContent === C.ALIGN_STRETCH) {
767
- switch (alignContent) {
768
- case C.ALIGN_FLEX_END:
769
- // Lines packed at end
770
- for (let i = 0; i < numLines; i++) {
771
- _lineCrossOffsets[i] += freeSpace;
772
- }
773
- break;
774
- case C.ALIGN_CENTER:
775
- // Lines centered
776
- {
777
- const centerOffset = freeSpace / 2;
778
- for (let i = 0; i < numLines; i++) {
779
- _lineCrossOffsets[i] += centerOffset;
780
- }
781
- }
782
- break;
783
- case C.ALIGN_SPACE_BETWEEN:
784
- // First line at start, last at end, evenly distributed
785
- if (numLines > 1) {
786
- const gap = freeSpace / (numLines - 1);
787
- for (let i = 1; i < numLines; i++) {
788
- _lineCrossOffsets[i] += gap * i;
789
- }
790
- }
791
- break;
792
- case C.ALIGN_SPACE_AROUND:
793
- // Even spacing with half-space at edges
794
- {
795
- const halfGap = freeSpace / (numLines * 2);
796
- for (let i = 0; i < numLines; i++) {
797
- _lineCrossOffsets[i] += halfGap + halfGap * 2 * i;
798
- }
799
- }
800
- break;
801
- case C.ALIGN_SPACE_EVENLY:
802
- // Equal spacing between lines and at edges
803
- if (numLines > 0) {
804
- const gap = freeSpace / (numLines + 1);
805
- for (let i = 0; i < numLines; i++) {
806
- _lineCrossOffsets[i] += gap * (i + 1);
807
- }
808
- }
809
- break;
810
- case C.ALIGN_STRETCH:
811
- // Distribute extra space evenly among lines
812
- if (freeSpace > 0 && numLines > 0) {
813
- const extraPerLine = freeSpace / numLines;
814
- for (let i = 0; i < numLines; i++) {
815
- _lineCrossSizes[i] += extraPerLine;
816
- // Recalculate offset for subsequent lines
817
- if (i > 0) {
818
- _lineCrossOffsets[i] = _lineCrossOffsets[i - 1] + _lineCrossSizes[i - 1] + crossGap;
819
- }
820
- }
821
- }
822
- break;
823
- // ALIGN_FLEX_START is the default - lines already at start
824
- }
825
- }
826
- // For wrap-reverse, lines should be positioned from the end of the cross axis
827
- // The lines are already in reversed order from breakIntoLines().
828
- // We just need to shift them so they align to the end instead of the start.
829
- if (isWrapReverse) {
830
- let totalLineCrossSize = 0;
831
- for (let i = 0; i < numLines; i++) {
832
- totalLineCrossSize += _lineCrossSizes[i];
833
- }
834
- totalLineCrossSize += crossGap * (numLines - 1);
835
- const crossStartOffset = crossAxisSize - totalLineCrossSize;
836
- for (let i = 0; i < numLines; i++) {
837
- _lineCrossOffsets[i] += crossStartOffset;
838
- }
839
- }
840
- }
841
- // -----------------------------------------------------------------------
842
- // PHASE 8: Position and Layout Children
843
- // -----------------------------------------------------------------------
844
- // Calculate each child's position in the container.
845
- // Apply cross-axis alignment (align-items, align-self).
846
- // Recursively layout grandchildren.
847
- // Position and layout children
848
- // For reverse directions (including RTL for row), start from the END of the container
849
- // RTL + reverse cancels out (XOR behavior)
850
- // For shrink-wrap containers, compute effective main size first
851
- let effectiveMainAxisSize = mainAxisSize;
852
- const mainIsAuto = isRow
853
- ? style.width.unit !== C.UNIT_POINT && style.width.unit !== C.UNIT_PERCENT
854
- : style.height.unit !== C.UNIT_POINT && style.height.unit !== C.UNIT_PERCENT;
855
- // Calculate total gaps for all children (used for shrink-wrap sizing)
856
- const totalGaps = relativeCount > 1 ? mainGap * (relativeCount - 1) : 0;
857
- if (effectiveReverse && mainIsAuto) {
858
- // For reverse with auto size, compute total content size for positioning
859
- let totalContent = 0;
860
- for (const child of node.children) {
861
- if (child.flex.relativeIndex < 0)
862
- continue;
863
- totalContent += child.flex.mainSize + child.flex.mainStartMarginValue + child.flex.mainEndMarginValue;
864
- }
865
- totalContent += totalGaps;
866
- effectiveMainAxisSize = totalContent;
867
- }
868
- // Use fractional mainPos for edge-based rounding
869
- // Initialize with first line's startOffset (may be overridden when processing lines)
870
- let mainPos = effectiveReverse ? effectiveMainAxisSize - startOffset : startOffset;
871
- let currentLineIdx = -1;
872
- let relIdx = 0; // Track relative child index globally
873
- let lineChildIdx = 0; // Track position within current line (for gap handling)
874
- let currentLineLength = 0; // Length of current line
875
- let currentItemSpacing = itemSpacing; // Track current line's item spacing
876
- log.debug?.("positioning children: isRow=%s, startOffset=%d, relativeCount=%d, effectiveReverse=%s, numLines=%d", isRow, startOffset, relativeCount, effectiveReverse, numLines);
877
- for (const child of node.children) {
878
- if (child.flex.relativeIndex < 0)
879
- continue;
880
- const cflex = child.flex;
881
- const childStyle = child.style;
882
- // Check if we've moved to a new line (for flex-wrap)
883
- const childLineIdx = cflex.lineIndex;
884
- if (childLineIdx !== currentLineIdx) {
885
- currentLineIdx = childLineIdx;
886
- lineChildIdx = 0; // Reset position within line
887
- currentLineLength = _lineChildren[childLineIdx].length;
888
- // Reset mainPos for new line using line-specific justify offset
889
- const lineOffset = _lineJustifyStarts[childLineIdx];
890
- currentItemSpacing = _lineItemSpacings[childLineIdx];
891
- mainPos = effectiveReverse ? effectiveMainAxisSize - lineOffset : lineOffset;
892
- }
893
- // Get cross-axis offset for this child's line (from pre-allocated array)
894
- const lineCrossOffset = childLineIdx < MAX_FLEX_LINES ? _lineCrossOffsets[childLineIdx] : 0;
895
- // For main-axis margins, use computed auto margin values
896
- // For cross-axis margins, use cached values (auto margins on cross axis handled separately)
897
- let childMarginLeft;
898
- let childMarginTop;
899
- let childMarginRight;
900
- let childMarginBottom;
901
- // Use cached margins, with auto margin override for main axis
902
- // For row layouts, use effectiveReverse (accounts for RTL)
903
- if (isRow) {
904
- // Row: main axis is horizontal
905
- // effectiveReverse handles both row-reverse AND RTL
906
- childMarginLeft =
907
- cflex.mainStartMarginAuto && !effectiveReverse
908
- ? cflex.mainStartMarginValue
909
- : cflex.mainEndMarginAuto && effectiveReverse
910
- ? cflex.mainEndMarginValue
911
- : cflex.marginL;
912
- childMarginRight =
913
- cflex.mainEndMarginAuto && !effectiveReverse
914
- ? cflex.mainEndMarginValue
915
- : cflex.mainStartMarginAuto && effectiveReverse
916
- ? cflex.mainStartMarginValue
917
- : cflex.marginR;
918
- childMarginTop = cflex.marginT;
919
- childMarginBottom = cflex.marginB;
920
- }
921
- else {
922
- // Column: main axis is vertical (RTL doesn't affect column)
923
- // In column-reverse, mainStart=bottom(3), mainEnd=top(1)
924
- childMarginTop =
925
- cflex.mainStartMarginAuto && !isReverse
926
- ? cflex.mainStartMarginValue
927
- : cflex.mainEndMarginAuto && isReverse
928
- ? cflex.mainEndMarginValue
929
- : cflex.marginT;
930
- childMarginBottom =
931
- cflex.mainEndMarginAuto && !isReverse
932
- ? cflex.mainEndMarginValue
933
- : cflex.mainStartMarginAuto && isReverse
934
- ? cflex.mainStartMarginValue
935
- : cflex.marginB;
936
- childMarginLeft = cflex.marginL;
937
- childMarginRight = cflex.marginR;
938
- }
939
- // Main axis size comes from flex algorithm (already rounded)
940
- const childMainSize = cflex.mainSize;
941
- // Cross axis: determine alignment mode
942
- let alignment = style.alignItems;
943
- if (childStyle.alignSelf !== C.ALIGN_AUTO) {
944
- alignment = childStyle.alignSelf;
945
- }
946
- // CSS Alignment spec: aspect-ratio fallback alignment
947
- // When a flex item has aspect-ratio and auto cross-axis dimension,
948
- // the fallback alignment is flex-start (not stretch). This prevents
949
- // stretch from overriding the AR-derived dimension.
950
- // Only applies when stretch is inherited (align-self: auto), not explicit.
951
- const childCrossDimForAR = isRow ? childStyle.height : childStyle.width;
952
- const childCrossIsAutoForAR = childCrossDimForAR.unit === C.UNIT_AUTO || childCrossDimForAR.unit === C.UNIT_UNDEFINED;
953
- if (alignment === C.ALIGN_STRETCH &&
954
- childStyle.alignSelf === C.ALIGN_AUTO &&
955
- !Number.isNaN(childStyle.aspectRatio) &&
956
- childStyle.aspectRatio > 0 &&
957
- childCrossIsAutoForAR) {
958
- alignment = C.ALIGN_FLEX_START;
959
- }
960
- // Cross axis size depends on alignment and child's explicit dimensions
961
- // IMPORTANT: Resolve percent against parent's cross axis, not child's available
962
- let childCrossSize;
963
- const crossDim = isRow ? childStyle.height : childStyle.width;
964
- const crossMargin = isRow ? childMarginTop + childMarginBottom : childMarginLeft + childMarginRight;
965
- // Check if parent has definite cross-axis size
966
- // Parent can have definite cross from either:
967
- // 1. Explicit style (width/height in points or percent)
968
- // 2. Definite available space (crossAxisSize is not NaN)
969
- const parentCrossDim = isRow ? style.height : style.width;
970
- const parentHasDefiniteCrossStyle = parentCrossDim.unit === C.UNIT_POINT || parentCrossDim.unit === C.UNIT_PERCENT;
971
- // crossAxisSize comes from available space - if it's a real number, we have a constraint
972
- const parentHasDefiniteCross = parentHasDefiniteCrossStyle || !Number.isNaN(crossAxisSize);
973
- if (crossDim.unit === C.UNIT_POINT) {
974
- // Explicit cross size
975
- childCrossSize = crossDim.value;
976
- }
977
- else if (crossDim.unit === C.UNIT_PERCENT) {
978
- // Percent of PARENT's cross axis (resolveValue handles NaN -> 0)
979
- childCrossSize = resolveValue(crossDim, crossAxisSize);
980
- }
981
- else if (parentHasDefiniteCross && alignment === C.ALIGN_STRETCH) {
982
- // Stretch alignment with definite parent cross size - fill the line's cross axis
983
- // For wrapping layouts, stretch to line cross size, not full container cross size
984
- const lineCross = numLines > 1 && childLineIdx < MAX_FLEX_LINES ? _lineCrossSizes[childLineIdx] : crossAxisSize;
985
- childCrossSize = lineCross - crossMargin;
986
- }
987
- else {
988
- // Non-stretch alignment or no definite cross size - shrink-wrap to content
989
- childCrossSize = NaN;
990
- }
991
- // Apply cross-axis min/max constraints
992
- const crossMinVal = isRow ? childStyle.minHeight : childStyle.minWidth;
993
- const crossMaxVal = isRow ? childStyle.maxHeight : childStyle.maxWidth;
994
- const crossMin = crossMinVal.unit !== C.UNIT_UNDEFINED ? resolveValue(crossMinVal, crossAxisSize) : 0;
995
- const crossMax = crossMaxVal.unit !== C.UNIT_UNDEFINED ? resolveValue(crossMaxVal, crossAxisSize) : Infinity;
996
- // Apply constraints - for NaN (shrink-wrap), use min as floor
997
- if (Number.isNaN(childCrossSize)) {
998
- // For shrink-wrap, min sets the floor - child will be at least this size
999
- if (crossMin > 0) {
1000
- childCrossSize = crossMin;
1001
- }
1002
- }
1003
- else {
1004
- childCrossSize = Math.max(crossMin, Math.min(crossMax, childCrossSize));
1005
- }
1006
- // Handle intrinsic sizing for auto-sized children
1007
- // For auto main size children, use flex-computed size if flexGrow > 0,
1008
- // otherwise pass remaining available space for shrink-wrap behavior
1009
- const mainDim = isRow ? childStyle.width : childStyle.height;
1010
- // A child has definite main size if it has explicit width/height OR non-auto flexBasis
1011
- const hasDefiniteFlexBasis = childStyle.flexBasis.unit === C.UNIT_POINT || childStyle.flexBasis.unit === C.UNIT_PERCENT;
1012
- const mainIsAutoChild = (mainDim.unit === C.UNIT_AUTO || mainDim.unit === C.UNIT_UNDEFINED) && !hasDefiniteFlexBasis;
1013
- const hasFlexGrow = cflex.flexGrow > 0;
1014
- // Use flex-computed mainSize for all cases - it includes padding/border as minimum
1015
- // The flex algorithm already computed the proper size based on content/padding/border
1016
- const effectiveMainSize = childMainSize;
1017
- let childWidth = isRow ? effectiveMainSize : childCrossSize;
1018
- let childHeight = isRow ? childCrossSize : effectiveMainSize;
1019
- // Only use measure function for intrinsic sizing when flexGrow is NOT set
1020
- // When flexGrow > 0, the flex algorithm determines size, not the content
1021
- const shouldMeasure = child.hasMeasureFunc() && child.children.length === 0 && !hasFlexGrow;
1022
- if (shouldMeasure) {
1023
- const widthAuto = childStyle.width.unit === C.UNIT_AUTO || childStyle.width.unit === C.UNIT_UNDEFINED;
1024
- const heightAuto = childStyle.height.unit === C.UNIT_AUTO || childStyle.height.unit === C.UNIT_UNDEFINED;
1025
- if (widthAuto || heightAuto) {
1026
- // Call measure function with available space
1027
- const widthMode = widthAuto ? C.MEASURE_MODE_AT_MOST : C.MEASURE_MODE_EXACTLY;
1028
- const heightMode = heightAuto ? C.MEASURE_MODE_UNDEFINED : C.MEASURE_MODE_EXACTLY;
1029
- // For unconstrained dimensions (NaN), use Infinity for measure func
1030
- const rawAvailW = widthAuto
1031
- ? isRow
1032
- ? mainAxisSize - mainPos // Remaining space after previous children
1033
- : crossAxisSize - crossMargin
1034
- : childStyle.width.value;
1035
- const rawAvailH = heightAuto
1036
- ? isRow
1037
- ? crossAxisSize - crossMargin
1038
- : mainAxisSize - mainPos // Remaining space for COLUMN
1039
- : childStyle.height.value;
1040
- const availW = Number.isNaN(rawAvailW) ? Infinity : rawAvailW;
1041
- const availH = Number.isNaN(rawAvailH) ? Infinity : rawAvailH;
1042
- // Use cached measure to avoid redundant calls within a layout pass
1043
- const measured = child.cachedMeasure(availW, widthMode, availH, heightMode);
1044
- // For measure function nodes without flexGrow, intrinsic size takes precedence
1045
- if (widthAuto) {
1046
- childWidth = measured.width;
1047
- }
1048
- if (heightAuto) {
1049
- childHeight = measured.height;
1050
- }
1051
- }
1052
- }
1053
- // Child position within content area (fractional for edge-based rounding)
1054
- // For reverse directions (including RTL for row), position from mainPos - childSize
1055
- // IMPORTANT: In reverse, swap which margin is applied to which side
1056
- // For RTL row: items flow right-to-left, so right margin becomes trailing
1057
- // For flex-wrap, add lineCrossOffset to cross-axis position
1058
- let childX;
1059
- let childY;
1060
- if (effectiveReverse) {
1061
- if (isRow) {
1062
- // Row-reverse or RTL: items positioned from right
1063
- // In RTL/reverse, use right margin as trailing margin
1064
- childX = mainPos - childMainSize - childMarginRight;
1065
- childY = lineCrossOffset + childMarginTop;
1066
- }
1067
- else {
1068
- // Column-reverse: items positioned from bottom
1069
- childX = lineCrossOffset + childMarginLeft;
1070
- childY = mainPos - childMainSize - childMarginTop;
1071
- }
1072
- }
1073
- else {
1074
- childX = isRow ? mainPos + childMarginLeft : lineCrossOffset + childMarginLeft;
1075
- childY = isRow ? lineCrossOffset + childMarginTop : mainPos + childMarginTop;
1076
- }
1077
- // Edge-based rounding using ABSOLUTE coordinates (Yoga-compatible)
1078
- // This ensures adjacent elements share exact boundaries without gaps
1079
- // Key insight: round absolute edges, derive sizes from differences
1080
- const fractionalLeft = innerLeft + childX;
1081
- const fractionalTop = innerTop + childY;
1082
- // Compute position offsets for RELATIVE/STATIC positioned children
1083
- // These must be included in the absolute position BEFORE rounding (Yoga-compatible)
1084
- let posOffsetX = 0;
1085
- let posOffsetY = 0;
1086
- if (childStyle.positionType === C.POSITION_TYPE_RELATIVE || childStyle.positionType === C.POSITION_TYPE_STATIC) {
1087
- const relLeftPos = childStyle.position[0];
1088
- const relTopPos = childStyle.position[1];
1089
- const relRightPos = childStyle.position[2];
1090
- const relBottomPos = childStyle.position[3];
1091
- // Left offset (takes precedence over right)
1092
- if (relLeftPos.unit !== C.UNIT_UNDEFINED) {
1093
- posOffsetX = resolveValue(relLeftPos, contentWidth);
1094
- }
1095
- else if (relRightPos.unit !== C.UNIT_UNDEFINED) {
1096
- posOffsetX = -resolveValue(relRightPos, contentWidth);
1097
- }
1098
- // Top offset (takes precedence over bottom)
1099
- if (relTopPos.unit !== C.UNIT_UNDEFINED) {
1100
- posOffsetY = resolveValue(relTopPos, contentHeight);
1101
- }
1102
- else if (relBottomPos.unit !== C.UNIT_UNDEFINED) {
1103
- posOffsetY = -resolveValue(relBottomPos, contentHeight);
1104
- }
1105
- }
1106
- // Compute ABSOLUTE float positions for edge rounding (including position offsets)
1107
- // absX/absY are the parent's absolute position from document root
1108
- // Include BOTH parent's position offset and child's position offset
1109
- const absChildLeft = absX + marginLeft + parentPosOffsetX + fractionalLeft + posOffsetX;
1110
- const absChildTop = absY + marginTop + parentPosOffsetY + fractionalTop + posOffsetY;
1111
- // For main axis: round ABSOLUTE edges and derive size
1112
- // Only use edge-based rounding when childMainSize is valid (positive)
1113
- let roundedAbsMainStart;
1114
- let roundedAbsMainEnd;
1115
- let edgeBasedMainSize;
1116
- const useEdgeBasedRounding = childMainSize > 0;
1117
- // Compute child's box model minimum early (needed for edge-based rounding)
1118
- // Use resolveEdgeValue to respect logical EDGE_START/END for padding
1119
- const childPaddingL = resolveEdgeValue(childStyle.padding, 0, childStyle.flexDirection, contentWidth, direction);
1120
- const childPaddingT = resolveEdgeValue(childStyle.padding, 1, childStyle.flexDirection, contentWidth, direction);
1121
- const childPaddingR = resolveEdgeValue(childStyle.padding, 2, childStyle.flexDirection, contentWidth, direction);
1122
- const childPaddingB = resolveEdgeValue(childStyle.padding, 3, childStyle.flexDirection, contentWidth, direction);
1123
- const childBorderL = resolveEdgeBorderValue(childStyle.border, 0, childStyle.flexDirection, direction);
1124
- const childBorderT = resolveEdgeBorderValue(childStyle.border, 1, childStyle.flexDirection, direction);
1125
- const childBorderR = resolveEdgeBorderValue(childStyle.border, 2, childStyle.flexDirection, direction);
1126
- const childBorderB = resolveEdgeBorderValue(childStyle.border, 3, childStyle.flexDirection, direction);
1127
- const childMinW = childPaddingL + childPaddingR + childBorderL + childBorderR;
1128
- const childMinH = childPaddingT + childPaddingB + childBorderT + childBorderB;
1129
- const childMinMain = isRow ? childMinW : childMinH;
1130
- // Apply box model constraint to childMainSize before edge rounding
1131
- const constrainedMainSize = Math.max(childMainSize, childMinMain);
1132
- if (useEdgeBasedRounding) {
1133
- if (isRow) {
1134
- roundedAbsMainStart = Math.round(absChildLeft);
1135
- roundedAbsMainEnd = Math.round(absChildLeft + constrainedMainSize);
1136
- edgeBasedMainSize = roundedAbsMainEnd - roundedAbsMainStart;
1137
- }
1138
- else {
1139
- roundedAbsMainStart = Math.round(absChildTop);
1140
- roundedAbsMainEnd = Math.round(absChildTop + constrainedMainSize);
1141
- edgeBasedMainSize = roundedAbsMainEnd - roundedAbsMainStart;
1142
- }
1143
- }
1144
- else {
1145
- // For children without valid main size, use simple rounding
1146
- roundedAbsMainStart = isRow ? Math.round(absChildLeft) : Math.round(absChildTop);
1147
- edgeBasedMainSize = childMinMain; // Use minimum size instead of 0
1148
- }
1149
- // Calculate child's RELATIVE position (stored in layout)
1150
- // Yoga behavior: position is rounded locally, size uses absolute edge rounding
1151
- // This ensures sizes are pixel-perfect at document level while positions remain intuitive
1152
- // Yoga 3.x quirk: measureFunc leaf nodes use Math.floor for position rounding,
1153
- // while explicit-sized children use Math.round. This affects any justify/align mode
1154
- // that produces fractional offsets (center, space-around, space-evenly).
1155
- const posRound = shouldMeasure ? Math.floor : Math.round;
1156
- const childLeft = posRound(fractionalLeft + posOffsetX);
1157
- const childTop = posRound(fractionalTop + posOffsetY);
1158
- // Check if cross axis is auto-sized (needed for deciding what to pass to layoutNode)
1159
- const crossDimForLayoutCall = isRow ? childStyle.height : childStyle.width;
1160
- const crossIsAutoForLayoutCall = crossDimForLayoutCall.unit === C.UNIT_AUTO || crossDimForLayoutCall.unit === C.UNIT_UNDEFINED;
1161
- const mainDimForLayoutCall = isRow ? childStyle.width : childStyle.height;
1162
- const mainIsPercentForLayoutCall = mainDimForLayoutCall.unit === C.UNIT_PERCENT;
1163
- const crossIsPercentForLayoutCall = crossDimForLayoutCall.unit === C.UNIT_PERCENT;
1164
- // For auto-sized children (no flexGrow, no measureFunc), pass NaN to let them compute intrinsic size
1165
- // Otherwise layoutNode would subtract margins from the available size
1166
- // IMPORTANT: For percent-sized children, pass parent's content size (not child's computed size)
1167
- // so that grandchildren's percents resolve correctly against the child's actual dimensions.
1168
- // The child will resolve its own percent against this value, getting the same result the parent computed.
1169
- //
1170
- // CRITICAL: When flex distribution changed the child's size (shrinkage/growth applied),
1171
- // pass the actual childWidth instead of NaN. This ensures layoutNode's fingerprint check
1172
- // detects the change — otherwise NaN===NaN matches across passes with different flex
1173
- // distributions, preserving stale overridden dimensions from the previous pass.
1174
- //
1175
- // CRITICAL: Measure-func leaf nodes (text) must receive the actual constraint, not NaN.
1176
- // Their cross-axis size (e.g. height in a row) depends on the main-axis constraint
1177
- // (e.g. text wrapping width). Passing NaN causes them to measure unconstrained,
1178
- // producing height=1 instead of the correct wrapped height. The parent's Phase 8
1179
- // shouldMeasure path computes the correct childWidth/childHeight, but layoutNode
1180
- // would recompute with NaN and get a different result.
1181
- const flexDistChanged = child.flex.mainSize !== child.flex.baseSize;
1182
- const hasMeasureLeaf = child.hasMeasureFunc() && child.children.length === 0;
1183
- const passWidthToChild = isRow && mainIsAutoChild && !hasFlexGrow && !flexDistChanged && !hasMeasureLeaf
1184
- ? NaN
1185
- : !isRow && crossIsAutoForLayoutCall && !parentHasDefiniteCross
1186
- ? NaN
1187
- : isRow && mainIsPercentForLayoutCall
1188
- ? mainAxisSize
1189
- : !isRow && crossIsPercentForLayoutCall
1190
- ? crossAxisSize
1191
- : childWidth;
1192
- const passHeightToChild = !isRow && mainIsAutoChild && !hasFlexGrow && !flexDistChanged && !hasMeasureLeaf
1193
- ? NaN
1194
- : isRow && crossIsAutoForLayoutCall && !parentHasDefiniteCross
1195
- ? NaN
1196
- : !isRow && mainIsPercentForLayoutCall
1197
- ? mainAxisSize
1198
- : isRow && crossIsPercentForLayoutCall
1199
- ? crossAxisSize
1200
- : childHeight;
1201
- // Recurse to layout any grandchildren
1202
- // Pass the child's FLOAT absolute position (margin box start, before child's own margin)
1203
- // absChildLeft/Top include the child's margins, so subtract them to get margin box start
1204
- const childAbsX = absChildLeft - childMarginLeft;
1205
- const childAbsY = absChildTop - childMarginTop;
1206
- layoutNode(child, passWidthToChild, passHeightToChild, childLeft, childTop, childAbsX, childAbsY, direction);
1207
- // Enforce box model constraint: child can't be smaller than its padding + border
1208
- // (using childMinW/childMinH computed earlier for edge-based rounding)
1209
- if (childWidth < childMinW)
1210
- childWidth = childMinW;
1211
- if (childHeight < childMinH)
1212
- childHeight = childMinH;
1213
- // Set this child's layout - override what layoutNode computed
1214
- // Override if any of:
1215
- // - Child has explicit main dimension AND parent has explicit main dimension (edge-based rounding)
1216
- // - Child has flexGrow > 0 (flex distribution applied)
1217
- // - Child has measureFunc (leaf text node)
1218
- // - Flex distribution actually changed the size (grow or shrink)
1219
- //
1220
- // IMPORTANT: Don't override auto-sized containers when flex distribution
1221
- // didn't change their size. The pre-measurement (Phase 5) computes intrinsic
1222
- // size at unconstrained main axis, but layoutNode recomputes with actual
1223
- // cross-axis constraints. For containers with children that wrap text,
1224
- // layoutNode's result is correct because it accounts for the actual width
1225
- // after flex distribution of grandchildren. The Phase 5 measureNode pass
1226
- // measures row children with NaN main width, so text doesn't wrap —
1227
- // producing height=1 instead of the correct wrapped height.
1228
- const hasMeasure = child.hasMeasureFunc() && child.children.length === 0;
1229
- const flexDistributionChangedSize = child.flex.mainSize !== child.flex.baseSize;
1230
- if ((!mainIsAuto && !mainIsAutoChild) || hasFlexGrow || hasMeasure || flexDistributionChangedSize) {
1231
- // Use edge-based rounding: size = round(end_edge) - round(start_edge)
1232
- if (isRow) {
1233
- _t?.parentOverride(_tn, "main", child.layout.width, edgeBasedMainSize);
1234
- child.layout.width = edgeBasedMainSize;
1235
- }
1236
- else {
1237
- _t?.parentOverride(_tn, "main", child.layout.height, edgeBasedMainSize);
1238
- child.layout.height = edgeBasedMainSize;
1239
- }
1240
- }
1241
- // Cross axis: only override for explicit sizing or when we have a real constraint
1242
- // For auto-sized children, let layoutNode determine the size
1243
- const crossDimForCheck = isRow ? childStyle.height : childStyle.width;
1244
- const crossIsAuto = crossDimForCheck.unit === C.UNIT_AUTO || crossDimForCheck.unit === C.UNIT_UNDEFINED;
1245
- // Only override if child has explicit sizing OR parent has explicit cross size
1246
- // When parent has auto cross size, let children shrink-wrap first
1247
- // Note: parentCrossDim and parentHasDefiniteCross already computed above
1248
- const parentCrossIsAuto = !parentHasDefiniteCross;
1249
- // Also check if childCrossSize was constrained by min/max - if so, we should override
1250
- const hasCrossMinMax = crossMinVal.unit !== C.UNIT_UNDEFINED || crossMaxVal.unit !== C.UNIT_UNDEFINED;
1251
- const shouldOverrideCross = !crossIsAuto ||
1252
- (!parentCrossIsAuto && alignment === C.ALIGN_STRETCH) ||
1253
- (hasCrossMinMax && !Number.isNaN(childCrossSize));
1254
- if (shouldOverrideCross) {
1255
- if (isRow) {
1256
- child.layout.height = Math.round(childHeight);
1257
- }
1258
- else {
1259
- child.layout.width = Math.round(childWidth);
1260
- }
1261
- }
1262
- // Store RELATIVE position (within parent's content area), not absolute
1263
- // This matches Yoga's behavior where getComputedLeft/Top return relative positions
1264
- // Position offsets are already included in childLeft/childTop via edge-based rounding
1265
- child.layout.left = childLeft;
1266
- child.layout.top = childTop;
1267
- // Update childWidth/childHeight to match actual computed layout for mainPos calculation
1268
- childWidth = child.layout.width;
1269
- childHeight = child.layout.height;
1270
- // Apply cross-axis alignment offset
1271
- const finalCrossSize = isRow ? child.layout.height : child.layout.width;
1272
- let crossOffset = 0;
1273
- // Check for auto margins on cross axis - they override alignment
1274
- // Use isEdgeAuto to correctly respect logical EDGE_START/END margins
1275
- const crossStartIndex = isRow ? 1 : 0; // top for row, left for column
1276
- const crossEndIndex = isRow ? 3 : 2; // bottom for row, right for column
1277
- const hasAutoStartMargin = isEdgeAuto(childStyle.margin, crossStartIndex, style.flexDirection, direction);
1278
- const hasAutoEndMargin = isEdgeAuto(childStyle.margin, crossEndIndex, style.flexDirection, direction);
1279
- // When baseline alignment is present (hasBaselineAlignment) and this child is NOT using
1280
- // baseline alignment, align within the baseline zone instead of the full container.
1281
- // Yoga behavior: non-baseline children are positioned relative to the effective height
1282
- // of the baseline group (max of maxBaseline and tallest child), not the container.
1283
- const useBaselineZone = hasBaselineAlignment &&
1284
- isRow &&
1285
- !alignItemsIsBaseline &&
1286
- alignment !== C.ALIGN_BASELINE &&
1287
- baselineZoneHeight > 0;
1288
- const effectiveCrossSize = useBaselineZone ? baselineZoneHeight : crossAxisSize;
1289
- const availableCrossSpace = effectiveCrossSize - finalCrossSize - crossMargin;
1290
- if (hasAutoStartMargin && hasAutoEndMargin) {
1291
- // Both auto: center the item
1292
- // CSS spec: auto margins don't absorb negative free space (clamp to 0)
1293
- crossOffset = Math.max(0, availableCrossSpace) / 2;
1294
- }
1295
- else if (hasAutoStartMargin) {
1296
- // Auto start margin: push to end
1297
- // CSS spec: auto margins don't absorb negative free space (clamp to 0)
1298
- crossOffset = Math.max(0, availableCrossSpace);
1299
- }
1300
- else if (hasAutoEndMargin) {
1301
- // Auto end margin: stay at start (crossOffset = 0)
1302
- crossOffset = 0;
1303
- }
1304
- else {
1305
- // No auto margins: use alignment
1306
- switch (alignment) {
1307
- case C.ALIGN_FLEX_END:
1308
- crossOffset = availableCrossSpace;
1309
- break;
1310
- case C.ALIGN_CENTER:
1311
- crossOffset = availableCrossSpace / 2;
1312
- break;
1313
- case C.ALIGN_BASELINE:
1314
- // Baseline alignment only applies to row direction
1315
- // For column direction, it falls through to flex-start (default)
1316
- if (isRow && hasBaselineAlignment) {
1317
- // Use pre-computed baseline from Phase 6c (stored in child.flex.baseline)
1318
- crossOffset = maxBaseline - child.flex.baseline;
1319
- }
1320
- break;
1321
- }
1322
- }
1323
- if (crossOffset > 0) {
1324
- // Yoga 3.x quirk: measureFunc leaf nodes use Math.floor for cross-axis alignment
1325
- // offset, matching the main-axis floor rounding behavior
1326
- const crossRound = shouldMeasure ? Math.floor : Math.round;
1327
- if (isRow) {
1328
- child.layout.top += crossRound(crossOffset);
1329
- }
1330
- else {
1331
- child.layout.left += crossRound(crossOffset);
1332
- }
1333
- }
1334
- // Position advancement: use the right size depending on Phase 8 behavior.
1335
- // - Phase 8 overrode (explicit size, flexGrow, measure, or flex distribution changed):
1336
- // Use constrainedMainSize (float) for precise gap/position calculations.
1337
- // child.layout is edge-rounded (integer), which causes rounding drift in gaps.
1338
- // - Phase 8 did NOT override (auto-sized container, no grow, no measure):
1339
- // Use child.layout (from layoutNode), which reflects actual content size.
1340
- // constrainedMainSize is a stale pre-layout estimate from unconstrained measurement.
1341
- const phaseEightOverrode = (!mainIsAuto && !mainIsAutoChild) || hasFlexGrow || hasMeasure || flexDistributionChangedSize;
1342
- const fractionalMainSize = phaseEightOverrode
1343
- ? constrainedMainSize
1344
- : isRow
1345
- ? child.layout.width
1346
- : child.layout.height;
1347
- // Use computed margin values (including auto margins)
1348
- const totalMainMargin = cflex.mainStartMarginValue + cflex.mainEndMarginValue;
1349
- log.debug?.(" child %d: mainPos=%d -> top=%d (fractionalMainSize=%d, totalMainMargin=%d)", relIdx, mainPos, child.layout.top, fractionalMainSize, totalMainMargin);
1350
- if (effectiveReverse) {
1351
- mainPos -= fractionalMainSize + totalMainMargin;
1352
- // Add spacing only between items on the SAME LINE (not across line breaks)
1353
- if (lineChildIdx < currentLineLength - 1) {
1354
- mainPos -= currentItemSpacing;
1355
- }
1356
- }
1357
- else {
1358
- mainPos += fractionalMainSize + totalMainMargin;
1359
- // Add spacing only between items on the SAME LINE (not across line breaks)
1360
- if (lineChildIdx < currentLineLength - 1) {
1361
- mainPos += currentItemSpacing;
1362
- }
1363
- }
1364
- relIdx++;
1365
- lineChildIdx++;
1366
- }
1367
- // -----------------------------------------------------------------------
1368
- // PHASE 9: Shrink-Wrap Auto-Sized Containers
1369
- // -----------------------------------------------------------------------
1370
- // For containers without explicit size, compute intrinsic size from children.
1371
- // For auto-sized containers (including root), shrink-wrap to content
1372
- // Compute actual used main space from child layouts (not pre-computed flex.mainSize which may be 0)
1373
- let actualUsedMain = 0;
1374
- for (const child of node.children) {
1375
- if (child.flex.relativeIndex < 0)
1376
- continue;
1377
- const childMainSize = isRow ? child.layout.width : child.layout.height;
1378
- const totalMainMargin = child.flex.mainStartMarginValue + child.flex.mainEndMarginValue;
1379
- actualUsedMain += childMainSize + totalMainMargin;
1380
- }
1381
- actualUsedMain += totalGaps;
1382
- // Skip main-axis shrink-wrap when aspect ratio determined this dimension
1383
- const hasAR = !Number.isNaN(aspectRatio) && aspectRatio > 0;
1384
- if (isRow && style.width.unit !== C.UNIT_POINT && style.width.unit !== C.UNIT_PERCENT && !hasAR) {
1385
- // Auto-width row: shrink-wrap to content
1386
- nodeWidth = actualUsedMain + innerLeft + innerRight;
1387
- }
1388
- if (!isRow && style.height.unit !== C.UNIT_POINT && style.height.unit !== C.UNIT_PERCENT && !hasAR) {
1389
- // Auto-height column: shrink-wrap to content
1390
- nodeHeight = actualUsedMain + innerTop + innerBottom;
1391
- }
1392
- // For cross axis, compute shrink-wrap size
1393
- // For multi-line (flex-wrap), sum line cross sizes + cross gaps
1394
- // For single line, use max child cross size (existing behavior)
1395
- let totalCrossSize = 0;
1396
- if (numLines > 1) {
1397
- // Multi-line: sum line cross sizes + cross gaps between lines
1398
- for (let i = 0; i < numLines; i++) {
1399
- totalCrossSize += _lineCrossSizes[i];
1400
- }
1401
- totalCrossSize += crossGap * (numLines - 1);
1402
- }
1403
- else {
1404
- // Single line: max child cross size
1405
- // CSS spec: percentage margins resolve against containing block's WIDTH only
1406
- // Use resolveEdgeValue to respect logical EDGE_START/END
1407
- for (const child of node.children) {
1408
- if (child.flex.relativeIndex < 0)
1409
- continue;
1410
- const childCross = isRow ? child.layout.height : child.layout.width;
1411
- const childMargin = isRow
1412
- ? resolveEdgeValue(child.style.margin, 1, style.flexDirection, contentWidth, direction) +
1413
- resolveEdgeValue(child.style.margin, 3, style.flexDirection, contentWidth, direction)
1414
- : resolveEdgeValue(child.style.margin, 0, style.flexDirection, contentWidth, direction) +
1415
- resolveEdgeValue(child.style.margin, 2, style.flexDirection, contentWidth, direction);
1416
- totalCrossSize = Math.max(totalCrossSize, childCross + childMargin);
1417
- }
1418
- }
1419
- // Cross-axis shrink-wrap for auto-sized dimension
1420
- // Only shrink-wrap when the available dimension is NaN (unconstrained)
1421
- // When availableHeight/Width is defined, Yoga uses it for AUTO-sized root nodes
1422
- // Skip if aspect ratio already determined this dimension (aspect ratio > shrink-wrap)
1423
- if (isRow &&
1424
- style.height.unit !== C.UNIT_POINT &&
1425
- style.height.unit !== C.UNIT_PERCENT &&
1426
- Number.isNaN(availableHeight) &&
1427
- !hasAR) {
1428
- // Auto-height row: shrink-wrap to total cross size (accounts for multi-line)
1429
- nodeHeight = totalCrossSize + innerTop + innerBottom;
1430
- }
1431
- if (!isRow &&
1432
- style.width.unit !== C.UNIT_POINT &&
1433
- style.width.unit !== C.UNIT_PERCENT &&
1434
- Number.isNaN(availableWidth) &&
1435
- !hasAR) {
1436
- // Auto-width column: shrink-wrap to total cross size (accounts for multi-line)
1437
- nodeWidth = totalCrossSize + innerLeft + innerRight;
1438
- }
1439
- }
1440
- // Re-apply min/max constraints after any shrink-wrap adjustments
1441
- // This ensures containers don't violate their constraints after auto-sizing
1442
- nodeWidth = applyMinMax(nodeWidth, style.minWidth, style.maxWidth, availableWidth);
1443
- nodeHeight = applyMinMax(nodeHeight, style.minHeight, style.maxHeight, availableHeight);
1444
- // Re-enforce box model constraint: minimum size = padding + border
1445
- // This must be applied AFTER applyMinMax since min/max can't reduce below padding+border
1446
- if (!Number.isNaN(nodeWidth) && nodeWidth < minInnerWidth) {
1447
- nodeWidth = minInnerWidth;
1448
- }
1449
- if (!Number.isNaN(nodeHeight) && nodeHeight < minInnerHeight) {
1450
- nodeHeight = minInnerHeight;
1451
- }
1452
- // -----------------------------------------------------------------------
1453
- // PHASE 9b: Re-stretch children after shrink-wrap (Yoga compat)
1454
- // -----------------------------------------------------------------------
1455
- // When the parent's cross axis was auto (NaN during Phase 8), children with
1456
- // stretch alignment were shrink-wrapped to content. Now that the cross size
1457
- // is known from shrink-wrap, re-layout those children with the definite size.
1458
- // This matches Yoga's two-pass approach for auto-sized containers.
1459
- if (Number.isNaN(crossAxisSize) && relativeCount > 0) {
1460
- const finalCross = isRow ? nodeHeight - innerTop - innerBottom : nodeWidth - innerLeft - innerRight;
1461
- if (!Number.isNaN(finalCross) && finalCross > 0) {
1462
- for (const child of node.children) {
1463
- if (child.flex.relativeIndex < 0)
1464
- continue;
1465
- const cstyle = child.style;
1466
- // Determine alignment for this child
1467
- let childAlign = style.alignItems;
1468
- if (cstyle.alignSelf !== C.ALIGN_AUTO) {
1469
- childAlign = cstyle.alignSelf;
1470
- }
1471
- // AR fallback: aspect-ratio prevents implicit stretch
1472
- const cCrossDim = isRow ? cstyle.height : cstyle.width;
1473
- const cCrossIsAuto = cCrossDim.unit === C.UNIT_AUTO || cCrossDim.unit === C.UNIT_UNDEFINED;
1474
- if (childAlign === C.ALIGN_STRETCH &&
1475
- cstyle.alignSelf === C.ALIGN_AUTO &&
1476
- !Number.isNaN(cstyle.aspectRatio) &&
1477
- cstyle.aspectRatio > 0 &&
1478
- cCrossIsAuto) {
1479
- childAlign = C.ALIGN_FLEX_START;
1480
- }
1481
- if (childAlign !== C.ALIGN_STRETCH)
1482
- continue;
1483
- if (!cCrossIsAuto)
1484
- continue;
1485
- // Compute child's cross margin
1486
- const cCrossMargin = isRow
1487
- ? resolveEdgeValue(cstyle.margin, 1, style.flexDirection, contentWidth, direction) +
1488
- resolveEdgeValue(cstyle.margin, 3, style.flexDirection, contentWidth, direction)
1489
- : resolveEdgeValue(cstyle.margin, 0, style.flexDirection, contentWidth, direction) +
1490
- resolveEdgeValue(cstyle.margin, 2, style.flexDirection, contentWidth, direction);
1491
- const stretchedCross = finalCross - cCrossMargin;
1492
- // Only re-layout if the cross size actually changed
1493
- const currentCross = isRow ? child.layout.height : child.layout.width;
1494
- if (Math.round(stretchedCross) <= currentCross)
1495
- continue;
1496
- // Re-layout child with the definite cross size
1497
- // Save position — layoutNode overwrites layout.left/top
1498
- const savedLeft = child.layout.left;
1499
- const savedTop = child.layout.top;
1500
- const cMarginL = resolveEdgeValue(cstyle.margin, 0, style.flexDirection, contentWidth, direction);
1501
- const cMarginT = resolveEdgeValue(cstyle.margin, 1, style.flexDirection, contentWidth, direction);
1502
- const cAbsX = absX + innerLeft + savedLeft - cMarginL;
1503
- const cAbsY = absY + innerTop + savedTop - cMarginT;
1504
- const passW = isRow ? child.layout.width : stretchedCross;
1505
- const passH = isRow ? stretchedCross : child.layout.height;
1506
- layoutNode(child, passW, passH, savedLeft, savedTop, cAbsX, cAbsY, direction);
1507
- // Restore position and override cross dimension to stretched size
1508
- child.layout.left = savedLeft;
1509
- child.layout.top = savedTop;
1510
- if (isRow) {
1511
- child.layout.height = Math.round(stretchedCross);
1512
- }
1513
- else {
1514
- child.layout.width = Math.round(stretchedCross);
1515
- }
1516
- }
1517
- }
1518
- }
1519
- // =========================================================================
1520
- // PHASE 10: Final Output - Set Node Layout
1521
- // =========================================================================
1522
- // Use edge-based rounding (Yoga-compatible): round absolute edges and derive sizes.
1523
- // This ensures adjacent elements share exact boundaries without pixel gaps.
1524
- // Set this node's layout using edge-based rounding (Yoga-compatible)
1525
- // Use parentPosOffsetX/Y computed earlier (includes position offsets)
1526
- // Compute absolute positions for edge-based rounding
1527
- const absNodeLeft = absX + marginLeft + parentPosOffsetX;
1528
- const absNodeTop = absY + marginTop + parentPosOffsetY;
1529
- const absNodeRight = absNodeLeft + nodeWidth;
1530
- const absNodeBottom = absNodeTop + nodeHeight;
1531
- // Round edges and derive sizes (Yoga algorithm)
1532
- const roundedAbsLeft = Math.round(absNodeLeft);
1533
- const roundedAbsTop = Math.round(absNodeTop);
1534
- const roundedAbsRight = Math.round(absNodeRight);
1535
- const roundedAbsBottom = Math.round(absNodeBottom);
1536
- layout.width = roundedAbsRight - roundedAbsLeft;
1537
- layout.height = roundedAbsBottom - roundedAbsTop;
1538
- // Position is relative to parent, derived from absolute rounding
1539
- const roundedAbsParentLeft = Math.round(absX);
1540
- const roundedAbsParentTop = Math.round(absY);
1541
- layout.left = roundedAbsLeft - roundedAbsParentLeft;
1542
- layout.top = roundedAbsTop - roundedAbsParentTop;
1543
- // =========================================================================
1544
- // PHASE 11: Layout Absolute Children
1545
- // =========================================================================
1546
- // Absolute children are positioned relative to the padding box, not content box.
1547
- // They don't participate in flex layout - they're laid out independently.
1548
- // Layout absolute children - handle left/right/top/bottom offsets
1549
- // Absolute positioning uses the PADDING BOX as the containing block
1550
- // (inside border but INCLUDING padding, not the content box)
1551
- const absInnerLeft = borderLeft;
1552
- const absInnerTop = borderTop;
1553
- const absInnerRight = borderRight;
1554
- const absInnerBottom = borderBottom;
1555
- const absPaddingBoxW = nodeWidth - absInnerLeft - absInnerRight;
1556
- const absPaddingBoxH = nodeHeight - absInnerTop - absInnerBottom;
1557
- // Content box dimensions for percentage resolution of absolute children
1558
- const absContentBoxW = absPaddingBoxW - paddingLeft - paddingRight;
1559
- const absContentBoxH = absPaddingBoxH - paddingTop - paddingBottom;
1560
- // Layout absolute positioned children (relativeIndex === -1 but not display:none)
1561
- for (const child of node.children) {
1562
- if (child.style.display === C.DISPLAY_NONE)
1563
- continue;
1564
- if (child.style.positionType !== C.POSITION_TYPE_ABSOLUTE)
1565
- continue;
1566
- const childStyle = child.style;
1567
- // CSS spec: percentage margins resolve against containing block's WIDTH only
1568
- // Use resolveEdgeValue to respect logical EDGE_START/END
1569
- // Note: Auto margins will resolve to 0 here, we handle them separately below
1570
- const childMarginLeft = resolveEdgeValue(childStyle.margin, 0, style.flexDirection, nodeWidth, direction);
1571
- const childMarginTop = resolveEdgeValue(childStyle.margin, 1, style.flexDirection, nodeWidth, direction);
1572
- const childMarginRight = resolveEdgeValue(childStyle.margin, 2, style.flexDirection, nodeWidth, direction);
1573
- const childMarginBottom = resolveEdgeValue(childStyle.margin, 3, style.flexDirection, nodeWidth, direction);
1574
- // Check for auto margins (used for centering absolute children)
1575
- const hasAutoMarginLeft = isEdgeAuto(childStyle.margin, 0, style.flexDirection, direction);
1576
- const hasAutoMarginRight = isEdgeAuto(childStyle.margin, 2, style.flexDirection, direction);
1577
- const hasAutoMarginTop = isEdgeAuto(childStyle.margin, 1, style.flexDirection, direction);
1578
- const hasAutoMarginBottom = isEdgeAuto(childStyle.margin, 3, style.flexDirection, direction);
1579
- // Position offsets from setPosition(edge, value)
1580
- const leftPos = childStyle.position[0];
1581
- const topPos = childStyle.position[1];
1582
- const rightPos = childStyle.position[2];
1583
- const bottomPos = childStyle.position[3];
1584
- const hasLeft = leftPos.unit !== C.UNIT_UNDEFINED;
1585
- const hasRight = rightPos.unit !== C.UNIT_UNDEFINED;
1586
- const hasTop = topPos.unit !== C.UNIT_UNDEFINED;
1587
- const hasBottom = bottomPos.unit !== C.UNIT_UNDEFINED;
1588
- const leftOffset = resolveValue(leftPos, nodeWidth);
1589
- const topOffset = resolveValue(topPos, nodeHeight);
1590
- const rightOffset = resolveValue(rightPos, nodeWidth);
1591
- const bottomOffset = resolveValue(bottomPos, nodeHeight);
1592
- // Calculate available size for absolute child using padding box
1593
- const contentW = absPaddingBoxW;
1594
- const contentH = absPaddingBoxH;
1595
- // Determine child width
1596
- // - If both left and right set with auto width: stretch to fill
1597
- // - If auto width but NOT both left and right: shrink to intrinsic (NaN)
1598
- // - For percentage width: resolve against content box
1599
- // - Otherwise (explicit width): use available width as constraint
1600
- let childAvailWidth;
1601
- const widthIsAuto = childStyle.width.unit === C.UNIT_AUTO || childStyle.width.unit === C.UNIT_UNDEFINED;
1602
- const widthIsPercent = childStyle.width.unit === C.UNIT_PERCENT;
1603
- if (widthIsAuto && hasLeft && hasRight) {
1604
- childAvailWidth = contentW - leftOffset - rightOffset - childMarginLeft - childMarginRight;
1605
- }
1606
- else if (widthIsAuto) {
1607
- childAvailWidth = NaN; // Shrink to intrinsic size
1608
- }
1609
- else if (widthIsPercent) {
1610
- // Percentage widths resolve against content box (inside padding)
1611
- childAvailWidth = absContentBoxW;
1612
- }
1613
- else {
1614
- childAvailWidth = contentW;
1615
- }
1616
- // Determine child height
1617
- // - If both top and bottom set with auto height: stretch to fill
1618
- // - If auto height but NOT both top and bottom: shrink to intrinsic (NaN)
1619
- // - For percentage height: resolve against content box
1620
- // - Otherwise (explicit height): use available height as constraint
1621
- let childAvailHeight;
1622
- const heightIsAuto = childStyle.height.unit === C.UNIT_AUTO || childStyle.height.unit === C.UNIT_UNDEFINED;
1623
- const heightIsPercent = childStyle.height.unit === C.UNIT_PERCENT;
1624
- if (heightIsAuto && hasTop && hasBottom) {
1625
- childAvailHeight = contentH - topOffset - bottomOffset - childMarginTop - childMarginBottom;
1626
- }
1627
- else if (heightIsAuto) {
1628
- childAvailHeight = NaN; // Shrink to intrinsic size
1629
- }
1630
- else if (heightIsPercent) {
1631
- // Percentage heights resolve against content box (inside padding)
1632
- childAvailHeight = absContentBoxH;
1633
- }
1634
- else {
1635
- childAvailHeight = contentH;
1636
- }
1637
- // Compute child position
1638
- let childX = childMarginLeft + leftOffset;
1639
- let childY = childMarginTop + topOffset;
1640
- // First, layout the child to get its dimensions
1641
- // Use padding box origin (absInnerLeft/Top = border only)
1642
- // Compute child's absolute position (margin box start, before child's own margin)
1643
- // Parent's padding box = absX + marginLeft + borderLeft = absX + marginLeft + absInnerLeft
1644
- // Child's margin box = parent's padding box + leftOffset
1645
- const childAbsX = absX + marginLeft + absInnerLeft + leftOffset;
1646
- const childAbsY = absY + marginTop + absInnerTop + topOffset;
1647
- // Preserve NaN for shrink-wrap mode - only clamp real numbers to 0
1648
- const clampIfNumber = (v) => (Number.isNaN(v) ? NaN : Math.max(0, v));
1649
- layoutNode(child, clampIfNumber(childAvailWidth), clampIfNumber(childAvailHeight), layout.left + absInnerLeft + childX, layout.top + absInnerTop + childY, childAbsX, childAbsY, direction);
1650
- // Now compute final position based on right/bottom if left/top not set
1651
- const childWidth = child.layout.width;
1652
- const childHeight = child.layout.height;
1653
- // Apply alignment when no explicit position set
1654
- // For absolute children, align-items/justify-content apply when no position offsets
1655
- if (!hasLeft && !hasRight) {
1656
- // No horizontal position - use align-items (for row) or justify-content (for column)
1657
- // Default column direction: cross-axis is horizontal, use alignItems
1658
- let alignment = style.alignItems;
1659
- if (childStyle.alignSelf !== C.ALIGN_AUTO) {
1660
- alignment = childStyle.alignSelf;
1661
- }
1662
- const freeSpaceX = contentW - childWidth - childMarginLeft - childMarginRight;
1663
- switch (alignment) {
1664
- case C.ALIGN_CENTER:
1665
- childX = childMarginLeft + freeSpaceX / 2;
1666
- break;
1667
- case C.ALIGN_FLEX_END:
1668
- childX = childMarginLeft + freeSpaceX;
1669
- break;
1670
- case C.ALIGN_STRETCH:
1671
- // Stretch: already handled by setting width to fill
1672
- break;
1673
- default: // FLEX_START
1674
- childX = childMarginLeft;
1675
- break;
1676
- }
1677
- }
1678
- else if (!hasLeft && hasRight) {
1679
- // Position from right edge
1680
- childX = contentW - rightOffset - childMarginRight - childWidth;
1681
- }
1682
- else if (hasLeft && hasRight) {
1683
- // Both left and right are set
1684
- if (widthIsAuto) {
1685
- // Stretch width already handled above
1686
- child.layout.width = Math.round(childAvailWidth);
1687
- }
1688
- else if (hasAutoMarginLeft || hasAutoMarginRight) {
1689
- // Auto margins absorb remaining space for centering
1690
- // CSS spec: auto margins don't absorb negative free space (clamp to 0)
1691
- const freeSpace = Math.max(0, contentW - leftOffset - rightOffset - childWidth);
1692
- if (hasAutoMarginLeft && hasAutoMarginRight) {
1693
- // Both auto: center
1694
- childX = leftOffset + freeSpace / 2;
1695
- }
1696
- else if (hasAutoMarginLeft) {
1697
- // Only left auto: push to right
1698
- childX = leftOffset + freeSpace;
1699
- }
1700
- // Only right auto: childX already set to leftOffset + childMarginLeft
1701
- }
1702
- }
1703
- if (!hasTop && !hasBottom) {
1704
- // No vertical position - use justify-content (for row) or align-items (for column)
1705
- // Default column direction: main-axis is vertical, use justifyContent
1706
- const freeSpaceY = contentH - childHeight - childMarginTop - childMarginBottom;
1707
- switch (style.justifyContent) {
1708
- case C.JUSTIFY_CENTER:
1709
- childY = childMarginTop + freeSpaceY / 2;
1710
- break;
1711
- case C.JUSTIFY_FLEX_END:
1712
- childY = childMarginTop + freeSpaceY;
1713
- break;
1714
- default: // FLEX_START
1715
- childY = childMarginTop;
1716
- break;
1717
- }
1718
- }
1719
- else if (!hasTop && hasBottom) {
1720
- // Position from bottom edge
1721
- childY = contentH - bottomOffset - childMarginBottom - childHeight;
1722
- }
1723
- else if (hasTop && hasBottom) {
1724
- // Both top and bottom are set
1725
- if (heightIsAuto) {
1726
- // Stretch height already handled above
1727
- child.layout.height = Math.round(childAvailHeight);
1728
- }
1729
- else if (hasAutoMarginTop || hasAutoMarginBottom) {
1730
- // Auto margins absorb remaining space for centering
1731
- // CSS spec: auto margins don't absorb negative free space (clamp to 0)
1732
- const freeSpace = Math.max(0, contentH - topOffset - bottomOffset - childHeight);
1733
- if (hasAutoMarginTop && hasAutoMarginBottom) {
1734
- // Both auto: center
1735
- childY = topOffset + freeSpace / 2;
1736
- }
1737
- else if (hasAutoMarginTop) {
1738
- // Only top auto: push to bottom
1739
- childY = topOffset + freeSpace;
1740
- }
1741
- // Only bottom auto: childY already set to topOffset + childMarginTop
1742
- }
1743
- }
1744
- // Set final position (relative to container padding box)
1745
- child.layout.left = Math.round(absInnerLeft + childX);
1746
- child.layout.top = Math.round(absInnerTop + childY);
1747
- }
1748
- // Update constraint fingerprint - layout is now valid for these constraints
1749
- flex.lastAvailW = availableWidth;
1750
- flex.lastAvailH = availableHeight;
1751
- flex.lastOffsetX = offsetX;
1752
- flex.lastOffsetY = offsetY;
1753
- flex.lastDir = direction;
1754
- flex.layoutValid = true;
1755
- _t?.layoutExit(_tn, layout.width, layout.height);
1756
- }
1757
- //# sourceMappingURL=layout-zero.js.map