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