flexily 0.3.0 → 0.3.2

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