flexily 0.2.0 → 0.3.1

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