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.
- package/README.md +16 -16
- package/package.json +24 -24
- package/src/CLAUDE.md +36 -21
- package/src/classic/layout.ts +107 -47
- package/src/classic/node.ts +60 -0
- package/src/constants.ts +2 -1
- package/src/index-classic.ts +1 -1
- package/src/index.ts +1 -1
- package/src/layout-flex-lines.ts +70 -3
- package/src/layout-helpers.ts +29 -9
- package/src/layout-stats.ts +0 -2
- package/src/layout-zero.ts +587 -160
- package/src/node-zero.ts +98 -2
- package/src/testing.ts +20 -14
- package/src/types.ts +22 -15
- package/src/utils.ts +47 -21
- package/dist/classic/layout.d.ts +0 -57
- package/dist/classic/layout.d.ts.map +0 -1
- package/dist/classic/layout.js +0 -1558
- package/dist/classic/layout.js.map +0 -1
- package/dist/classic/node.d.ts +0 -648
- package/dist/classic/node.d.ts.map +0 -1
- package/dist/classic/node.js +0 -1002
- package/dist/classic/node.js.map +0 -1
- package/dist/constants.d.ts +0 -58
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -70
- package/dist/constants.js.map +0 -1
- package/dist/index-classic.d.ts +0 -30
- package/dist/index-classic.d.ts.map +0 -1
- package/dist/index-classic.js +0 -57
- package/dist/index-classic.js.map +0 -1
- package/dist/index.d.ts +0 -30
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -57
- package/dist/index.js.map +0 -1
- package/dist/layout-flex-lines.d.ts +0 -77
- package/dist/layout-flex-lines.d.ts.map +0 -1
- package/dist/layout-flex-lines.js +0 -317
- package/dist/layout-flex-lines.js.map +0 -1
- package/dist/layout-helpers.d.ts +0 -48
- package/dist/layout-helpers.d.ts.map +0 -1
- package/dist/layout-helpers.js +0 -108
- package/dist/layout-helpers.js.map +0 -1
- package/dist/layout-measure.d.ts +0 -25
- package/dist/layout-measure.d.ts.map +0 -1
- package/dist/layout-measure.js +0 -231
- package/dist/layout-measure.js.map +0 -1
- package/dist/layout-stats.d.ts +0 -19
- package/dist/layout-stats.d.ts.map +0 -1
- package/dist/layout-stats.js +0 -37
- package/dist/layout-stats.js.map +0 -1
- package/dist/layout-traversal.d.ts +0 -28
- package/dist/layout-traversal.d.ts.map +0 -1
- package/dist/layout-traversal.js +0 -65
- package/dist/layout-traversal.js.map +0 -1
- package/dist/layout-zero.d.ts +0 -26
- package/dist/layout-zero.d.ts.map +0 -1
- package/dist/layout-zero.js +0 -1601
- package/dist/layout-zero.js.map +0 -1
- package/dist/logger.d.ts +0 -14
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -61
- package/dist/logger.js.map +0 -1
- package/dist/node-zero.d.ts +0 -702
- package/dist/node-zero.d.ts.map +0 -1
- package/dist/node-zero.js +0 -1268
- package/dist/node-zero.js.map +0 -1
- package/dist/testing.d.ts +0 -69
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -179
- package/dist/testing.js.map +0 -1
- package/dist/trace.d.ts +0 -74
- package/dist/trace.d.ts.map +0 -1
- package/dist/trace.js +0 -191
- package/dist/trace.js.map +0 -1
- package/dist/types.d.ts +0 -170
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -43
- package/dist/types.js.map +0 -1
- package/dist/utils.d.ts +0 -41
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -197
- package/dist/utils.js.map +0 -1
- package/src/beorn-logger.d.ts +0 -10
package/src/layout-zero.ts
CHANGED
|
@@ -37,7 +37,6 @@ export { markSubtreeLayoutSeen, countNodes } from "./layout-traversal.js"
|
|
|
37
37
|
export {
|
|
38
38
|
layoutNodeCalls,
|
|
39
39
|
measureNodeCalls,
|
|
40
|
-
resolveEdgeCalls,
|
|
41
40
|
layoutSizingCalls,
|
|
42
41
|
layoutPositioningCalls,
|
|
43
42
|
layoutCacheHits,
|
|
@@ -52,6 +51,7 @@ import {
|
|
|
52
51
|
isRowDirection,
|
|
53
52
|
isReverseDirection,
|
|
54
53
|
resolveEdgeValue,
|
|
54
|
+
resolvePositionEdge,
|
|
55
55
|
isEdgeAuto,
|
|
56
56
|
resolveEdgeBorderValue,
|
|
57
57
|
} from "./layout-helpers.js"
|
|
@@ -73,6 +73,8 @@ import {
|
|
|
73
73
|
_lineItemSpacings,
|
|
74
74
|
breakIntoLines,
|
|
75
75
|
distributeFlexSpaceForLine,
|
|
76
|
+
enterLayout,
|
|
77
|
+
exitLayout,
|
|
76
78
|
} from "./layout-flex-lines.js"
|
|
77
79
|
|
|
78
80
|
/**
|
|
@@ -84,12 +86,19 @@ export function computeLayout(
|
|
|
84
86
|
availableHeight: number,
|
|
85
87
|
direction: number = C.DIRECTION_LTR,
|
|
86
88
|
): void {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
// Save line state if re-entrant (nested calculateLayout from measureFunc)
|
|
90
|
+
const saved = enterLayout()
|
|
91
|
+
try {
|
|
92
|
+
resetLayoutStats()
|
|
93
|
+
getTrace()?.resetCounter()
|
|
94
|
+
// Clear layout cache from previous pass (important for correct layout after tree changes)
|
|
95
|
+
root.resetLayoutCache()
|
|
96
|
+
// Pass absolute position (0,0) for root node - used for Yoga-compatible edge rounding
|
|
97
|
+
layoutNode(root, availableWidth, availableHeight, 0, 0, 0, 0, direction)
|
|
98
|
+
} finally {
|
|
99
|
+
// Restore line state for outer pass (no-op at depth 0)
|
|
100
|
+
exitLayout(saved)
|
|
101
|
+
}
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
/**
|
|
@@ -153,7 +162,9 @@ function layoutNode(
|
|
|
153
162
|
!node.isDirty() &&
|
|
154
163
|
Object.is(flex.lastAvailW, availableWidth) &&
|
|
155
164
|
Object.is(flex.lastAvailH, availableHeight) &&
|
|
156
|
-
flex.lastDir === direction
|
|
165
|
+
flex.lastDir === direction &&
|
|
166
|
+
flex.lastAbsX === absX &&
|
|
167
|
+
flex.lastAbsY === absY
|
|
157
168
|
) {
|
|
158
169
|
// Constraints unchanged - just update position based on offset delta
|
|
159
170
|
_t?.fingerprintHit(_tn, availableWidth, availableHeight)
|
|
@@ -175,6 +186,8 @@ function layoutNode(
|
|
|
175
186
|
sameW: Object.is(flex.lastAvailW, availableWidth),
|
|
176
187
|
sameH: Object.is(flex.lastAvailH, availableHeight),
|
|
177
188
|
sameDir: flex.lastDir === direction,
|
|
189
|
+
sameAbsX: flex.lastAbsX === absX,
|
|
190
|
+
sameAbsY: flex.lastAbsY === absY,
|
|
178
191
|
})
|
|
179
192
|
|
|
180
193
|
// ============================================================================
|
|
@@ -278,10 +291,13 @@ function layoutNode(
|
|
|
278
291
|
// This ensures children's absolute positions include parent's position offset
|
|
279
292
|
let parentPosOffsetX = 0
|
|
280
293
|
let parentPosOffsetY = 0
|
|
281
|
-
|
|
282
|
-
|
|
294
|
+
// CSS spec: position:static ignores insets (top/left/right/bottom).
|
|
295
|
+
// Only position:relative applies insets as offsets from normal flow position.
|
|
296
|
+
if (style.positionType === C.POSITION_TYPE_RELATIVE) {
|
|
297
|
+
// Resolve logical EDGE_START/EDGE_END to physical left/right based on direction
|
|
298
|
+
const leftPos = resolvePositionEdge(style.position, 0, direction)
|
|
283
299
|
const topPos = style.position[1]
|
|
284
|
-
const rightPos = style.position
|
|
300
|
+
const rightPos = resolvePositionEdge(style.position, 2, direction)
|
|
285
301
|
const bottomPos = style.position[3]
|
|
286
302
|
|
|
287
303
|
if (leftPos.unit !== C.UNIT_UNDEFINED) {
|
|
@@ -445,29 +461,52 @@ function layoutNode(
|
|
|
445
461
|
baseSize = sizeVal.value
|
|
446
462
|
} else if (sizeVal.unit === C.UNIT_PERCENT) {
|
|
447
463
|
baseSize = Number.isNaN(mainAxisSize) ? 0 : mainAxisSize * (sizeVal.value / 100)
|
|
448
|
-
} else if (child.hasMeasureFunc()
|
|
449
|
-
// For auto-sized children with measureFunc
|
|
450
|
-
// pre-measure to get intrinsic size
|
|
451
|
-
//
|
|
464
|
+
} else if (child.hasMeasureFunc()) {
|
|
465
|
+
// For auto-sized children with measureFunc,
|
|
466
|
+
// pre-measure to get intrinsic content size as flex base size.
|
|
467
|
+
// CSS spec section 9.2: flex base size is content-based regardless of flexGrow.
|
|
468
|
+
//
|
|
469
|
+
// When flexGrow > 0, measure UNCONSTRAINED on main axis to get max-content
|
|
470
|
+
// size. This ensures proportional distribution based on content (e.g., two
|
|
471
|
+
// text nodes with widths 10 and 20 get proportional shares of free space).
|
|
472
|
+
// When flexGrow === 0, measure AT_MOST container (original behavior for
|
|
473
|
+
// justify-content calculation).
|
|
452
474
|
const crossMargin = isRow ? cflex.marginT + cflex.marginB : cflex.marginL + cflex.marginR
|
|
453
475
|
const availCross = crossAxisSize - crossMargin
|
|
454
|
-
// Use cached measure to avoid redundant calls within a layout pass
|
|
455
476
|
// Measure function takes PHYSICAL (width, height), not logical (main, cross).
|
|
456
477
|
// For row: main=width, cross=height. For column: main=height, cross=width.
|
|
457
|
-
const
|
|
458
|
-
const
|
|
478
|
+
const wantMaxContent = childStyle.flexGrow > 0
|
|
479
|
+
const mW = isRow
|
|
480
|
+
? wantMaxContent
|
|
481
|
+
? Infinity
|
|
482
|
+
: Number.isNaN(mainAxisSize)
|
|
483
|
+
? Infinity
|
|
484
|
+
: mainAxisSize
|
|
485
|
+
: Number.isNaN(availCross)
|
|
486
|
+
? Infinity
|
|
487
|
+
: availCross
|
|
488
|
+
const mH = isRow
|
|
489
|
+
? Number.isNaN(availCross)
|
|
490
|
+
? Infinity
|
|
491
|
+
: availCross
|
|
492
|
+
: wantMaxContent
|
|
493
|
+
? Infinity
|
|
494
|
+
: Number.isNaN(mainAxisSize)
|
|
495
|
+
? Infinity
|
|
496
|
+
: mainAxisSize
|
|
459
497
|
const mWMode = isRow
|
|
460
|
-
?
|
|
498
|
+
? wantMaxContent
|
|
499
|
+
? C.MEASURE_MODE_UNDEFINED
|
|
500
|
+
: C.MEASURE_MODE_AT_MOST
|
|
461
501
|
: Number.isNaN(availCross)
|
|
462
502
|
? C.MEASURE_MODE_UNDEFINED
|
|
463
503
|
: C.MEASURE_MODE_AT_MOST
|
|
464
|
-
const mHMode = isRow
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
)!
|
|
504
|
+
const mHMode = isRow
|
|
505
|
+
? C.MEASURE_MODE_UNDEFINED
|
|
506
|
+
: wantMaxContent
|
|
507
|
+
? C.MEASURE_MODE_UNDEFINED
|
|
508
|
+
: C.MEASURE_MODE_AT_MOST
|
|
509
|
+
const measured = child.cachedMeasure(mW, mWMode, mH, mHMode)!
|
|
471
510
|
baseSize = isRow ? measured.width : measured.height
|
|
472
511
|
} else if (child.children.length > 0) {
|
|
473
512
|
// For auto-sized children WITH children but no measureFunc,
|
|
@@ -478,8 +517,10 @@ function layoutNode(
|
|
|
478
517
|
const cached = child.getCachedLayout(sizingW, sizingH)
|
|
479
518
|
if (cached) {
|
|
480
519
|
incLayoutCacheHits()
|
|
520
|
+
_t?.cacheHit(_tn, sizingW, sizingH, cached.width, cached.height)
|
|
481
521
|
baseSize = isRow ? cached.width : cached.height
|
|
482
522
|
} else {
|
|
523
|
+
_t?.cacheMiss(_tn, sizingW, sizingH)
|
|
483
524
|
// Use measureNode for sizing-only pass (faster than full layoutNode)
|
|
484
525
|
// Save layout before measureNode — it overwrites node.layout.width/height
|
|
485
526
|
// with intrinsic measurements (unconstrained widths -> text doesn't wrap ->
|
|
@@ -487,11 +528,12 @@ function layoutNode(
|
|
|
487
528
|
// in Phase 9 would skip re-computation and preserve the corrupted values.
|
|
488
529
|
const savedW = child.layout.width
|
|
489
530
|
const savedH = child.layout.height
|
|
490
|
-
measureNode(child, sizingW, sizingH)
|
|
531
|
+
measureNode(child, sizingW, sizingH, direction)
|
|
491
532
|
const measuredW = child.layout.width
|
|
492
533
|
const measuredH = child.layout.height
|
|
493
534
|
child.layout.width = savedW
|
|
494
535
|
child.layout.height = savedH
|
|
536
|
+
_t?.measureSaveRestore(_tn, savedW, savedH, measuredW, measuredH)
|
|
495
537
|
baseSize = isRow ? measuredW : measuredH
|
|
496
538
|
// Cache the result for potential reuse
|
|
497
539
|
child.setCachedLayout(sizingW, sizingH, measuredW, measuredH)
|
|
@@ -529,8 +571,16 @@ function layoutNode(
|
|
|
529
571
|
// below content size. Yoga defaults flexShrink to 0, preventing this. For
|
|
530
572
|
// overflow:hidden/scroll children, ensure flexShrink >= 1 so they participate
|
|
531
573
|
// in negative free space distribution (matching CSS behavior).
|
|
532
|
-
|
|
533
|
-
|
|
574
|
+
//
|
|
575
|
+
// Measured items with flexGrow > 0 use max-content as base size (CSS section 9.2).
|
|
576
|
+
// When their total base sizes exceed the container, they must be shrinkable so
|
|
577
|
+
// the flex algorithm can distribute negative free space. Without this, a single
|
|
578
|
+
// flexGrow text node whose content exceeds the container would overflow instead
|
|
579
|
+
// of filling the remaining space.
|
|
580
|
+
let shrink = childStyle.flexShrink
|
|
581
|
+
if (childStyle.overflow !== C.OVERFLOW_VISIBLE) shrink = Math.max(shrink, 1)
|
|
582
|
+
if (child.hasMeasureFunc() && childStyle.flexGrow > 0) shrink = Math.max(shrink, 1)
|
|
583
|
+
cflex.flexShrink = shrink
|
|
534
584
|
|
|
535
585
|
// Store base and main size (start from base size - distribution happens from here)
|
|
536
586
|
cflex.baseSize = baseSize
|
|
@@ -722,9 +772,15 @@ function layoutNode(
|
|
|
722
772
|
// For ALIGN_BASELINE in row direction, we need to know the max baseline first
|
|
723
773
|
// Zero-alloc: store baseline in child.flex.baseline, not a temporary array
|
|
724
774
|
let maxBaseline = 0
|
|
775
|
+
// baselineZoneHeight: the effective cross-axis size that non-baseline children
|
|
776
|
+
// align within when baseline alignment is present. This is max(maxBaseline, maxChildHeight).
|
|
777
|
+
// Only meaningful when alignItems != ALIGN_BASELINE but some children have alignSelf=baseline.
|
|
778
|
+
let baselineZoneHeight = 0
|
|
779
|
+
const alignItemsIsBaseline = style.alignItems === C.ALIGN_BASELINE
|
|
725
780
|
|
|
726
781
|
if (hasBaselineAlignment && isRow) {
|
|
727
782
|
// First pass: compute each child's baseline and find the maximum
|
|
783
|
+
let maxChildHeight = 0
|
|
728
784
|
for (const child of node.children) {
|
|
729
785
|
if (child.flex.relativeIndex < 0) continue
|
|
730
786
|
const childStyle = child.style
|
|
@@ -760,20 +816,23 @@ function layoutNode(
|
|
|
760
816
|
const cached = child.getCachedLayout(child.flex.mainSize, NaN)
|
|
761
817
|
if (cached) {
|
|
762
818
|
incLayoutCacheHits()
|
|
819
|
+
_t?.cacheHit(_tn, child.flex.mainSize, NaN, cached.width, cached.height)
|
|
763
820
|
childWidth = cached.width
|
|
764
821
|
childHeight = cached.height
|
|
765
822
|
} else {
|
|
823
|
+
_t?.cacheMiss(_tn, child.flex.mainSize, NaN)
|
|
766
824
|
// Use measureNode for sizing-only pass (faster than full layoutNode)
|
|
767
825
|
// Save layout before measureNode — it overwrites node.layout.width/height
|
|
768
826
|
// with intrinsic measurements. Without save/restore, layoutNode's fingerprint
|
|
769
827
|
// check in Phase 9 would skip re-computation and preserve corrupted values.
|
|
770
828
|
const savedW = child.layout.width
|
|
771
829
|
const savedH = child.layout.height
|
|
772
|
-
measureNode(child, child.flex.mainSize, NaN)
|
|
830
|
+
measureNode(child, child.flex.mainSize, NaN, direction)
|
|
773
831
|
childWidth = child.layout.width
|
|
774
832
|
childHeight = child.layout.height
|
|
775
833
|
child.layout.width = savedW
|
|
776
834
|
child.layout.height = savedH
|
|
835
|
+
_t?.measureSaveRestore(_tn, savedW, savedH, childWidth, childHeight)
|
|
777
836
|
child.setCachedLayout(child.flex.mainSize, NaN, childWidth, childHeight)
|
|
778
837
|
}
|
|
779
838
|
}
|
|
@@ -789,8 +848,23 @@ function layoutNode(
|
|
|
789
848
|
// This is a simplification from CSS spec but acceptable for TUI use cases
|
|
790
849
|
child.flex.baseline = topMargin + childHeight
|
|
791
850
|
}
|
|
792
|
-
|
|
851
|
+
|
|
852
|
+
// Track max child height (including margin) for baseline zone calculation
|
|
853
|
+
maxChildHeight = Math.max(maxChildHeight, topMargin + childHeight + child.flex.marginB)
|
|
854
|
+
|
|
855
|
+
// When alignItems is baseline, ALL children participate in baseline computation.
|
|
856
|
+
// When alignItems is NOT baseline, only children with alignSelf=baseline participate.
|
|
857
|
+
// This matches Yoga's behavior: non-baseline children are positioned within the
|
|
858
|
+
// "baseline zone" (the effective height determined by baseline-aligned children),
|
|
859
|
+
// not the full container cross-axis.
|
|
860
|
+
if (alignItemsIsBaseline || childStyle.alignSelf === C.ALIGN_BASELINE) {
|
|
861
|
+
maxBaseline = Math.max(maxBaseline, child.flex.baseline)
|
|
862
|
+
}
|
|
793
863
|
}
|
|
864
|
+
|
|
865
|
+
// Baseline zone height: the max of maxBaseline and the tallest child.
|
|
866
|
+
// Non-baseline children are aligned within this zone, not the full container.
|
|
867
|
+
baselineZoneHeight = Math.max(maxBaseline, maxChildHeight)
|
|
794
868
|
}
|
|
795
869
|
|
|
796
870
|
// -----------------------------------------------------------------------
|
|
@@ -827,16 +901,49 @@ function layoutNode(
|
|
|
827
901
|
childCross = crossDim.value
|
|
828
902
|
} else if (crossDim.unit === C.UNIT_PERCENT && !Number.isNaN(crossAxisSize)) {
|
|
829
903
|
childCross = crossAxisSize * (crossDim.value / 100)
|
|
830
|
-
} else {
|
|
831
|
-
// Auto
|
|
832
|
-
|
|
904
|
+
} else if (child.hasMeasureFunc()) {
|
|
905
|
+
// Auto-sized with measureFunc: get tentative cross size from cached measure.
|
|
906
|
+
// Phase 5 already called cachedMeasure, so this is typically a cache hit (no alloc).
|
|
907
|
+
const crossMargin = crossMarginStart + crossMarginEnd
|
|
908
|
+
const availCross = Number.isNaN(crossAxisSize) ? Infinity : crossAxisSize - crossMargin
|
|
909
|
+
// Use child's resolved mainSize (from flex distribution) instead of parent's
|
|
910
|
+
// mainAxisSize. After Phase 6a, child.flex.mainSize may be smaller than
|
|
911
|
+
// mainAxisSize (e.g., due to wrapping). Measuring at parent width would
|
|
912
|
+
// underestimate text wrapping height (fewer lines → shorter cross size).
|
|
913
|
+
const childMainSize = child.flex.mainSize
|
|
914
|
+
const mW = isRow ? childMainSize : availCross
|
|
915
|
+
const mH = isRow ? availCross : childMainSize
|
|
916
|
+
const mWMode = Number.isNaN(mW) ? C.MEASURE_MODE_UNDEFINED : C.MEASURE_MODE_AT_MOST
|
|
917
|
+
const mHMode = Number.isNaN(mH) ? C.MEASURE_MODE_UNDEFINED : C.MEASURE_MODE_AT_MOST
|
|
918
|
+
const measured = child.cachedMeasure(
|
|
919
|
+
Number.isNaN(mW) ? Infinity : mW,
|
|
920
|
+
mWMode,
|
|
921
|
+
Number.isNaN(mH) ? Infinity : mH,
|
|
922
|
+
mHMode,
|
|
923
|
+
)
|
|
924
|
+
if (measured) {
|
|
925
|
+
childCross = isRow ? measured.height : measured.width
|
|
926
|
+
}
|
|
927
|
+
} else if (child.children.length > 0) {
|
|
928
|
+
// Auto-sized container children (no measureFunc, has children):
|
|
929
|
+
// Compute intrinsic cross-axis size by measuring with unconstrained
|
|
930
|
+
// dimensions. This gives the shrink-wrap size, which is what we need
|
|
931
|
+
// for line cross-size estimation. Passing NaN for both axes ensures
|
|
932
|
+
// measureNode returns the content-determined size rather than
|
|
933
|
+
// filling the available space.
|
|
934
|
+
const savedW = child.layout.width
|
|
935
|
+
const savedH = child.layout.height
|
|
936
|
+
measureNode(child, NaN, NaN, direction)
|
|
937
|
+
childCross = isRow ? child.layout.height : child.layout.width
|
|
938
|
+
child.layout.width = savedW
|
|
939
|
+
child.layout.height = savedH
|
|
833
940
|
}
|
|
834
941
|
maxLineCross = Math.max(maxLineCross, childCross + crossMarginStart + crossMarginEnd)
|
|
835
942
|
}
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
|
|
839
|
-
const lineCrossSize = maxLineCross
|
|
943
|
+
// Use measured max cross size. If all children are auto-sized (maxLineCross === 0),
|
|
944
|
+
// use 0 — NOT crossAxisSize/numLines, which would consume all free space and
|
|
945
|
+
// prevent alignContent from distributing it. Actual sizes are computed in Phase 8.
|
|
946
|
+
const lineCrossSize = maxLineCross
|
|
840
947
|
_lineCrossSizes[lineIdx] = lineCrossSize
|
|
841
948
|
cumulativeCrossOffset += lineCrossSize + crossGap
|
|
842
949
|
}
|
|
@@ -855,62 +962,89 @@ function layoutNode(
|
|
|
855
962
|
const freeSpace = crossAxisSize - totalLineCrossSize
|
|
856
963
|
const alignContent = style.alignContent
|
|
857
964
|
|
|
858
|
-
//
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
965
|
+
// Apply alignContent offset based on free space.
|
|
966
|
+
// flex-end and center apply with both positive and negative free space.
|
|
967
|
+
// space-between/around/evenly only distribute with positive free space;
|
|
968
|
+
// with negative space they collapse to flex-start or center (CSS spec).
|
|
969
|
+
// stretch only expands lines with positive free space.
|
|
970
|
+
switch (alignContent) {
|
|
971
|
+
case C.ALIGN_FLEX_END:
|
|
972
|
+
// Lines packed at end (works with negative free space too — shifts lines up)
|
|
973
|
+
for (let i = 0; i < numLines; i++) {
|
|
974
|
+
_lineCrossOffsets[i]! += freeSpace
|
|
975
|
+
}
|
|
976
|
+
break
|
|
977
|
+
|
|
978
|
+
case C.ALIGN_CENTER:
|
|
979
|
+
// Lines centered (works with negative free space — equal overflow both sides)
|
|
980
|
+
{
|
|
981
|
+
const centerOffset = freeSpace / 2
|
|
863
982
|
for (let i = 0; i < numLines; i++) {
|
|
864
|
-
_lineCrossOffsets[i]! +=
|
|
983
|
+
_lineCrossOffsets[i]! += centerOffset
|
|
865
984
|
}
|
|
866
|
-
|
|
985
|
+
}
|
|
986
|
+
break
|
|
867
987
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
988
|
+
case C.ALIGN_SPACE_BETWEEN:
|
|
989
|
+
// First line at start, last at end, evenly distributed
|
|
990
|
+
// With negative free space: collapses to flex-start (no adjustment)
|
|
991
|
+
if (freeSpace > 0 && numLines > 1) {
|
|
992
|
+
const gap = freeSpace / (numLines - 1)
|
|
993
|
+
for (let i = 1; i < numLines; i++) {
|
|
994
|
+
_lineCrossOffsets[i]! += gap * i
|
|
875
995
|
}
|
|
876
|
-
|
|
996
|
+
}
|
|
997
|
+
break
|
|
877
998
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
999
|
+
case C.ALIGN_SPACE_AROUND:
|
|
1000
|
+
// Even spacing with half-space at edges
|
|
1001
|
+
// With negative free space: collapses to center (CSS spec)
|
|
1002
|
+
if (freeSpace > 0) {
|
|
1003
|
+
const halfGap = freeSpace / (numLines * 2)
|
|
1004
|
+
for (let i = 0; i < numLines; i++) {
|
|
1005
|
+
_lineCrossOffsets[i]! += halfGap + halfGap * 2 * i
|
|
885
1006
|
}
|
|
886
|
-
|
|
1007
|
+
} else {
|
|
1008
|
+
// Negative space: center fallback
|
|
1009
|
+
const centerOffset = freeSpace / 2
|
|
1010
|
+
for (let i = 0; i < numLines; i++) {
|
|
1011
|
+
_lineCrossOffsets[i]! += centerOffset
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
break
|
|
887
1015
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1016
|
+
case C.ALIGN_SPACE_EVENLY:
|
|
1017
|
+
// Equal spacing between lines and at edges
|
|
1018
|
+
// With negative free space: collapses to center (CSS spec)
|
|
1019
|
+
if (freeSpace > 0 && numLines > 0) {
|
|
1020
|
+
const gap = freeSpace / (numLines + 1)
|
|
1021
|
+
for (let i = 0; i < numLines; i++) {
|
|
1022
|
+
_lineCrossOffsets[i]! += gap * (i + 1)
|
|
895
1023
|
}
|
|
896
|
-
|
|
1024
|
+
} else if (freeSpace < 0) {
|
|
1025
|
+
// Negative space: center fallback
|
|
1026
|
+
const centerOffset = freeSpace / 2
|
|
1027
|
+
for (let i = 0; i < numLines; i++) {
|
|
1028
|
+
_lineCrossOffsets[i]! += centerOffset
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
break
|
|
897
1032
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
}
|
|
1033
|
+
case C.ALIGN_STRETCH:
|
|
1034
|
+
// Distribute extra space evenly among lines (only with positive free space)
|
|
1035
|
+
if (freeSpace > 0 && numLines > 0) {
|
|
1036
|
+
const extraPerLine = freeSpace / numLines
|
|
1037
|
+
for (let i = 0; i < numLines; i++) {
|
|
1038
|
+
_lineCrossSizes[i]! += extraPerLine
|
|
1039
|
+
// Recalculate offset for subsequent lines
|
|
1040
|
+
if (i > 0) {
|
|
1041
|
+
_lineCrossOffsets[i] = _lineCrossOffsets[i - 1]! + _lineCrossSizes[i - 1]! + crossGap
|
|
908
1042
|
}
|
|
909
1043
|
}
|
|
910
|
-
|
|
1044
|
+
}
|
|
1045
|
+
break
|
|
911
1046
|
|
|
912
|
-
|
|
913
|
-
}
|
|
1047
|
+
// ALIGN_FLEX_START is the default - lines already at start
|
|
914
1048
|
}
|
|
915
1049
|
|
|
916
1050
|
// For wrap-reverse, lines should be positioned from the end of the cross axis
|
|
@@ -929,6 +1063,29 @@ function layoutNode(
|
|
|
929
1063
|
}
|
|
930
1064
|
}
|
|
931
1065
|
|
|
1066
|
+
// Save line data before Phase 8: recursive layoutNode calls for children
|
|
1067
|
+
// with sub-children overwrite the global pre-allocated _lineCrossSizes,
|
|
1068
|
+
// _lineCrossOffsets, _lineJustifyStarts, and _lineItemSpacings arrays.
|
|
1069
|
+
// For multi-line layouts, we copy the values into small local arrays.
|
|
1070
|
+
// This allocation is rare (only for multi-line wrapping containers) and
|
|
1071
|
+
// tiny (4 arrays x numLines x 8 bytes).
|
|
1072
|
+
let savedLineCrossSizes: Float64Array | null = null
|
|
1073
|
+
let savedLineCrossOffsets: Float64Array | null = null
|
|
1074
|
+
let savedLineJustifyStarts: Float64Array | null = null
|
|
1075
|
+
let savedLineItemSpacings: Float64Array | null = null
|
|
1076
|
+
if (numLines > 1) {
|
|
1077
|
+
savedLineCrossSizes = new Float64Array(numLines)
|
|
1078
|
+
savedLineCrossOffsets = new Float64Array(numLines)
|
|
1079
|
+
savedLineJustifyStarts = new Float64Array(numLines)
|
|
1080
|
+
savedLineItemSpacings = new Float64Array(numLines)
|
|
1081
|
+
for (let i = 0; i < numLines; i++) {
|
|
1082
|
+
savedLineCrossSizes[i] = _lineCrossSizes[i]!
|
|
1083
|
+
savedLineCrossOffsets[i] = _lineCrossOffsets[i]!
|
|
1084
|
+
savedLineJustifyStarts[i] = _lineJustifyStarts[i]!
|
|
1085
|
+
savedLineItemSpacings[i] = _lineItemSpacings[i]!
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
932
1089
|
// -----------------------------------------------------------------------
|
|
933
1090
|
// PHASE 8: Position and Layout Children
|
|
934
1091
|
// -----------------------------------------------------------------------
|
|
@@ -989,13 +1146,23 @@ function layoutNode(
|
|
|
989
1146
|
lineChildIdx = 0 // Reset position within line
|
|
990
1147
|
currentLineLength = _lineChildren[childLineIdx]!.length
|
|
991
1148
|
// Reset mainPos for new line using line-specific justify offset
|
|
992
|
-
|
|
993
|
-
|
|
1149
|
+
// Use saved arrays for multi-line to avoid corruption by recursive layoutNode
|
|
1150
|
+
const lineOffset = savedLineJustifyStarts
|
|
1151
|
+
? savedLineJustifyStarts[childLineIdx]!
|
|
1152
|
+
: _lineJustifyStarts[childLineIdx]!
|
|
1153
|
+
currentItemSpacing = savedLineItemSpacings
|
|
1154
|
+
? savedLineItemSpacings[childLineIdx]!
|
|
1155
|
+
: _lineItemSpacings[childLineIdx]!
|
|
994
1156
|
mainPos = effectiveReverse ? effectiveMainAxisSize - lineOffset : lineOffset
|
|
995
1157
|
}
|
|
996
1158
|
|
|
997
|
-
// Get cross-axis offset for this child's line
|
|
998
|
-
|
|
1159
|
+
// Get cross-axis offset for this child's line
|
|
1160
|
+
// Use saved arrays for multi-line to avoid corruption by recursive layoutNode
|
|
1161
|
+
const lineCrossOffset = savedLineCrossOffsets
|
|
1162
|
+
? savedLineCrossOffsets[childLineIdx]!
|
|
1163
|
+
: childLineIdx < MAX_FLEX_LINES
|
|
1164
|
+
? _lineCrossOffsets[childLineIdx]
|
|
1165
|
+
: 0
|
|
999
1166
|
|
|
1000
1167
|
// For main-axis margins, use computed auto margin values
|
|
1001
1168
|
// For cross-axis margins, use cached values (auto margins on cross axis handled separately)
|
|
@@ -1051,6 +1218,24 @@ function layoutNode(
|
|
|
1051
1218
|
alignment = childStyle.alignSelf
|
|
1052
1219
|
}
|
|
1053
1220
|
|
|
1221
|
+
// CSS Alignment spec: aspect-ratio fallback alignment
|
|
1222
|
+
// When a flex item has aspect-ratio and auto cross-axis dimension,
|
|
1223
|
+
// the fallback alignment is flex-start (not stretch). This prevents
|
|
1224
|
+
// stretch from overriding the AR-derived dimension.
|
|
1225
|
+
// Only applies when stretch is inherited (align-self: auto), not explicit.
|
|
1226
|
+
const childCrossDimForAR = isRow ? childStyle.height : childStyle.width
|
|
1227
|
+
const childCrossIsAutoForAR =
|
|
1228
|
+
childCrossDimForAR.unit === C.UNIT_AUTO || childCrossDimForAR.unit === C.UNIT_UNDEFINED
|
|
1229
|
+
if (
|
|
1230
|
+
alignment === C.ALIGN_STRETCH &&
|
|
1231
|
+
childStyle.alignSelf === C.ALIGN_AUTO &&
|
|
1232
|
+
!Number.isNaN(childStyle.aspectRatio) &&
|
|
1233
|
+
childStyle.aspectRatio > 0 &&
|
|
1234
|
+
childCrossIsAutoForAR
|
|
1235
|
+
) {
|
|
1236
|
+
alignment = C.ALIGN_FLEX_START
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1054
1239
|
// Cross axis size depends on alignment and child's explicit dimensions
|
|
1055
1240
|
// IMPORTANT: Resolve percent against parent's cross axis, not child's available
|
|
1056
1241
|
let childCrossSize: number
|
|
@@ -1073,8 +1258,16 @@ function layoutNode(
|
|
|
1073
1258
|
// Percent of PARENT's cross axis (resolveValue handles NaN -> 0)
|
|
1074
1259
|
childCrossSize = resolveValue(crossDim, crossAxisSize)
|
|
1075
1260
|
} else if (parentHasDefiniteCross && alignment === C.ALIGN_STRETCH) {
|
|
1076
|
-
// Stretch alignment with definite parent cross size - fill the cross axis
|
|
1077
|
-
|
|
1261
|
+
// Stretch alignment with definite parent cross size - fill the line's cross axis
|
|
1262
|
+
// For wrapping layouts, stretch to line cross size, not full container cross size
|
|
1263
|
+
// Use saved arrays for multi-line to avoid corruption by recursive layoutNode
|
|
1264
|
+
const lineCross =
|
|
1265
|
+
numLines > 1
|
|
1266
|
+
? savedLineCrossSizes
|
|
1267
|
+
? savedLineCrossSizes[childLineIdx]!
|
|
1268
|
+
: _lineCrossSizes[childLineIdx]!
|
|
1269
|
+
: crossAxisSize
|
|
1270
|
+
childCrossSize = lineCross - crossMargin
|
|
1078
1271
|
} else {
|
|
1079
1272
|
// Non-stretch alignment or no definite cross size - shrink-wrap to content
|
|
1080
1273
|
childCrossSize = NaN
|
|
@@ -1100,7 +1293,11 @@ function layoutNode(
|
|
|
1100
1293
|
// For auto main size children, use flex-computed size if flexGrow > 0,
|
|
1101
1294
|
// otherwise pass remaining available space for shrink-wrap behavior
|
|
1102
1295
|
const mainDim = isRow ? childStyle.width : childStyle.height
|
|
1103
|
-
|
|
1296
|
+
// A child has definite main size if it has explicit width/height OR non-auto flexBasis
|
|
1297
|
+
const hasDefiniteFlexBasis =
|
|
1298
|
+
childStyle.flexBasis.unit === C.UNIT_POINT || childStyle.flexBasis.unit === C.UNIT_PERCENT
|
|
1299
|
+
const mainIsAutoChild =
|
|
1300
|
+
(mainDim.unit === C.UNIT_AUTO || mainDim.unit === C.UNIT_UNDEFINED) && !hasDefiniteFlexBasis
|
|
1104
1301
|
const hasFlexGrow = cflex.flexGrow > 0
|
|
1105
1302
|
// Use flex-computed mainSize for all cases - it includes padding/border as minimum
|
|
1106
1303
|
// The flex algorithm already computed the proper size based on content/padding/border
|
|
@@ -1177,14 +1374,16 @@ function layoutNode(
|
|
|
1177
1374
|
const fractionalLeft = innerLeft + childX
|
|
1178
1375
|
const fractionalTop = innerTop + childY
|
|
1179
1376
|
|
|
1180
|
-
// Compute position offsets for RELATIVE
|
|
1377
|
+
// Compute position offsets for RELATIVE positioned children
|
|
1378
|
+
// CSS spec: position:static ignores insets; only position:relative applies them.
|
|
1181
1379
|
// These must be included in the absolute position BEFORE rounding (Yoga-compatible)
|
|
1182
1380
|
let posOffsetX = 0
|
|
1183
1381
|
let posOffsetY = 0
|
|
1184
|
-
if (childStyle.positionType === C.POSITION_TYPE_RELATIVE
|
|
1185
|
-
|
|
1382
|
+
if (childStyle.positionType === C.POSITION_TYPE_RELATIVE) {
|
|
1383
|
+
// Resolve logical EDGE_START/EDGE_END to physical left/right based on direction
|
|
1384
|
+
const relLeftPos = resolvePositionEdge(childStyle.position, 0, direction)
|
|
1186
1385
|
const relTopPos = childStyle.position[1]
|
|
1187
|
-
const relRightPos = childStyle.position
|
|
1386
|
+
const relRightPos = resolvePositionEdge(childStyle.position, 2, direction)
|
|
1188
1387
|
const relBottomPos = childStyle.position[3]
|
|
1189
1388
|
|
|
1190
1389
|
// Left offset (takes precedence over right)
|
|
@@ -1251,8 +1450,12 @@ function layoutNode(
|
|
|
1251
1450
|
// Calculate child's RELATIVE position (stored in layout)
|
|
1252
1451
|
// Yoga behavior: position is rounded locally, size uses absolute edge rounding
|
|
1253
1452
|
// This ensures sizes are pixel-perfect at document level while positions remain intuitive
|
|
1254
|
-
|
|
1255
|
-
|
|
1453
|
+
// Yoga 3.x quirk: measureFunc leaf nodes use Math.floor for position rounding,
|
|
1454
|
+
// while explicit-sized children use Math.round. This affects any justify/align mode
|
|
1455
|
+
// that produces fractional offsets (center, space-around, space-evenly).
|
|
1456
|
+
const posRound = shouldMeasure ? Math.floor : Math.round
|
|
1457
|
+
const childLeft = posRound(fractionalLeft + posOffsetX)
|
|
1458
|
+
const childTop = posRound(fractionalTop + posOffsetY)
|
|
1256
1459
|
|
|
1257
1460
|
// Check if cross axis is auto-sized (needed for deciding what to pass to layoutNode)
|
|
1258
1461
|
const crossDimForLayoutCall = isRow ? childStyle.height : childStyle.width
|
|
@@ -1382,7 +1585,18 @@ function layoutNode(
|
|
|
1382
1585
|
const crossEndIndex = isRow ? 3 : 2 // bottom for row, right for column
|
|
1383
1586
|
const hasAutoStartMargin = isEdgeAuto(childStyle.margin, crossStartIndex, style.flexDirection, direction)
|
|
1384
1587
|
const hasAutoEndMargin = isEdgeAuto(childStyle.margin, crossEndIndex, style.flexDirection, direction)
|
|
1385
|
-
|
|
1588
|
+
// When baseline alignment is present (hasBaselineAlignment) and this child is NOT using
|
|
1589
|
+
// baseline alignment, align within the baseline zone instead of the full container.
|
|
1590
|
+
// Yoga behavior: non-baseline children are positioned relative to the effective height
|
|
1591
|
+
// of the baseline group (max of maxBaseline and tallest child), not the container.
|
|
1592
|
+
const useBaselineZone =
|
|
1593
|
+
hasBaselineAlignment &&
|
|
1594
|
+
isRow &&
|
|
1595
|
+
!alignItemsIsBaseline &&
|
|
1596
|
+
alignment !== C.ALIGN_BASELINE &&
|
|
1597
|
+
baselineZoneHeight > 0
|
|
1598
|
+
const effectiveCrossSize = useBaselineZone ? baselineZoneHeight : crossAxisSize
|
|
1599
|
+
const availableCrossSpace = effectiveCrossSize - finalCrossSize - crossMargin
|
|
1386
1600
|
|
|
1387
1601
|
if (hasAutoStartMargin && hasAutoEndMargin) {
|
|
1388
1602
|
// Both auto: center the item
|
|
@@ -1415,11 +1629,14 @@ function layoutNode(
|
|
|
1415
1629
|
}
|
|
1416
1630
|
}
|
|
1417
1631
|
|
|
1418
|
-
if (crossOffset
|
|
1632
|
+
if (crossOffset !== 0) {
|
|
1633
|
+
// Yoga 3.x quirk: measureFunc leaf nodes use Math.floor for cross-axis alignment
|
|
1634
|
+
// offset, matching the main-axis floor rounding behavior
|
|
1635
|
+
const crossRound = shouldMeasure ? Math.floor : Math.round
|
|
1419
1636
|
if (isRow) {
|
|
1420
|
-
child.layout.top +=
|
|
1637
|
+
child.layout.top += crossRound(crossOffset)
|
|
1421
1638
|
} else {
|
|
1422
|
-
child.layout.left +=
|
|
1639
|
+
child.layout.left += crossRound(crossOffset)
|
|
1423
1640
|
}
|
|
1424
1641
|
}
|
|
1425
1642
|
|
|
@@ -1480,48 +1697,65 @@ function layoutNode(
|
|
|
1480
1697
|
}
|
|
1481
1698
|
actualUsedMain += totalGaps
|
|
1482
1699
|
|
|
1483
|
-
|
|
1700
|
+
// Skip main-axis shrink-wrap when aspect ratio determined this dimension
|
|
1701
|
+
const hasAR = !Number.isNaN(aspectRatio) && aspectRatio > 0
|
|
1702
|
+
if (isRow && style.width.unit !== C.UNIT_POINT && style.width.unit !== C.UNIT_PERCENT && !hasAR) {
|
|
1484
1703
|
// Auto-width row: shrink-wrap to content
|
|
1485
1704
|
nodeWidth = actualUsedMain + innerLeft + innerRight
|
|
1486
1705
|
}
|
|
1487
|
-
if (!isRow && style.height.unit !== C.UNIT_POINT && style.height.unit !== C.UNIT_PERCENT) {
|
|
1706
|
+
if (!isRow && style.height.unit !== C.UNIT_POINT && style.height.unit !== C.UNIT_PERCENT && !hasAR) {
|
|
1488
1707
|
// Auto-height column: shrink-wrap to content
|
|
1489
1708
|
nodeHeight = actualUsedMain + innerTop + innerBottom
|
|
1490
1709
|
}
|
|
1491
|
-
// For cross axis,
|
|
1492
|
-
//
|
|
1493
|
-
//
|
|
1494
|
-
let
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1710
|
+
// For cross axis, compute shrink-wrap size
|
|
1711
|
+
// For multi-line (flex-wrap), sum line cross sizes + cross gaps
|
|
1712
|
+
// For single line, use max child cross size (existing behavior)
|
|
1713
|
+
let totalCrossSize = 0
|
|
1714
|
+
if (numLines > 1) {
|
|
1715
|
+
// Multi-line: sum line cross sizes + cross gaps between lines
|
|
1716
|
+
// Use saved arrays to avoid corruption by recursive layoutNode
|
|
1717
|
+
for (let i = 0; i < numLines; i++) {
|
|
1718
|
+
totalCrossSize += savedLineCrossSizes ? savedLineCrossSizes[i]! : _lineCrossSizes[i]!
|
|
1719
|
+
}
|
|
1720
|
+
totalCrossSize += crossGap * (numLines - 1)
|
|
1721
|
+
} else {
|
|
1722
|
+
// Single line: max child cross size
|
|
1723
|
+
// CSS spec: percentage margins resolve against containing block's WIDTH only
|
|
1724
|
+
// Use resolveEdgeValue to respect logical EDGE_START/END
|
|
1725
|
+
for (const child of node.children) {
|
|
1726
|
+
if (child.flex.relativeIndex < 0) continue
|
|
1727
|
+
const childCross = isRow ? child.layout.height : child.layout.width
|
|
1728
|
+
const childMargin = isRow
|
|
1729
|
+
? resolveEdgeValue(child.style.margin, 1, style.flexDirection, contentWidth, direction) +
|
|
1730
|
+
resolveEdgeValue(child.style.margin, 3, style.flexDirection, contentWidth, direction)
|
|
1731
|
+
: resolveEdgeValue(child.style.margin, 0, style.flexDirection, contentWidth, direction) +
|
|
1732
|
+
resolveEdgeValue(child.style.margin, 2, style.flexDirection, contentWidth, direction)
|
|
1733
|
+
totalCrossSize = Math.max(totalCrossSize, childCross + childMargin)
|
|
1734
|
+
}
|
|
1504
1735
|
}
|
|
1505
1736
|
// Cross-axis shrink-wrap for auto-sized dimension
|
|
1506
1737
|
// Only shrink-wrap when the available dimension is NaN (unconstrained)
|
|
1507
1738
|
// When availableHeight/Width is defined, Yoga uses it for AUTO-sized root nodes
|
|
1739
|
+
// Skip if aspect ratio already determined this dimension (aspect ratio > shrink-wrap)
|
|
1508
1740
|
if (
|
|
1509
1741
|
isRow &&
|
|
1510
1742
|
style.height.unit !== C.UNIT_POINT &&
|
|
1511
1743
|
style.height.unit !== C.UNIT_PERCENT &&
|
|
1512
|
-
Number.isNaN(availableHeight)
|
|
1744
|
+
Number.isNaN(availableHeight) &&
|
|
1745
|
+
!hasAR
|
|
1513
1746
|
) {
|
|
1514
|
-
// Auto-height row: shrink-wrap to
|
|
1515
|
-
nodeHeight =
|
|
1747
|
+
// Auto-height row: shrink-wrap to total cross size (accounts for multi-line)
|
|
1748
|
+
nodeHeight = totalCrossSize + innerTop + innerBottom
|
|
1516
1749
|
}
|
|
1517
1750
|
if (
|
|
1518
1751
|
!isRow &&
|
|
1519
1752
|
style.width.unit !== C.UNIT_POINT &&
|
|
1520
1753
|
style.width.unit !== C.UNIT_PERCENT &&
|
|
1521
|
-
Number.isNaN(availableWidth)
|
|
1754
|
+
Number.isNaN(availableWidth) &&
|
|
1755
|
+
!hasAR
|
|
1522
1756
|
) {
|
|
1523
|
-
// Auto-width column: shrink-wrap to
|
|
1524
|
-
nodeWidth =
|
|
1757
|
+
// Auto-width column: shrink-wrap to total cross size (accounts for multi-line)
|
|
1758
|
+
nodeWidth = totalCrossSize + innerLeft + innerRight
|
|
1525
1759
|
}
|
|
1526
1760
|
}
|
|
1527
1761
|
|
|
@@ -1539,6 +1773,156 @@ function layoutNode(
|
|
|
1539
1773
|
nodeHeight = minInnerHeight
|
|
1540
1774
|
}
|
|
1541
1775
|
|
|
1776
|
+
// -----------------------------------------------------------------------
|
|
1777
|
+
// PHASE 9b: Re-stretch children after shrink-wrap (Yoga compat)
|
|
1778
|
+
// -----------------------------------------------------------------------
|
|
1779
|
+
// When the parent's cross axis was auto (NaN during Phase 8), children with
|
|
1780
|
+
// stretch alignment were shrink-wrapped to content. Now that the cross size
|
|
1781
|
+
// is known from shrink-wrap, re-layout those children with the definite size.
|
|
1782
|
+
// This matches Yoga's two-pass approach for auto-sized containers.
|
|
1783
|
+
if (Number.isNaN(crossAxisSize) && relativeCount > 0) {
|
|
1784
|
+
const finalCross = isRow ? nodeHeight - innerTop - innerBottom : nodeWidth - innerLeft - innerRight
|
|
1785
|
+
if (!Number.isNaN(finalCross) && finalCross > 0) {
|
|
1786
|
+
for (const child of node.children) {
|
|
1787
|
+
if (child.flex.relativeIndex < 0) continue
|
|
1788
|
+
const cstyle = child.style
|
|
1789
|
+
// Determine alignment for this child
|
|
1790
|
+
let childAlign = style.alignItems
|
|
1791
|
+
if (cstyle.alignSelf !== C.ALIGN_AUTO) {
|
|
1792
|
+
childAlign = cstyle.alignSelf
|
|
1793
|
+
}
|
|
1794
|
+
// AR fallback: aspect-ratio prevents implicit stretch
|
|
1795
|
+
const cCrossDim = isRow ? cstyle.height : cstyle.width
|
|
1796
|
+
const cCrossIsAuto = cCrossDim.unit === C.UNIT_AUTO || cCrossDim.unit === C.UNIT_UNDEFINED
|
|
1797
|
+
if (
|
|
1798
|
+
childAlign === C.ALIGN_STRETCH &&
|
|
1799
|
+
cstyle.alignSelf === C.ALIGN_AUTO &&
|
|
1800
|
+
!Number.isNaN(cstyle.aspectRatio) &&
|
|
1801
|
+
cstyle.aspectRatio > 0 &&
|
|
1802
|
+
cCrossIsAuto
|
|
1803
|
+
) {
|
|
1804
|
+
childAlign = C.ALIGN_FLEX_START
|
|
1805
|
+
}
|
|
1806
|
+
if (childAlign !== C.ALIGN_STRETCH) continue
|
|
1807
|
+
if (!cCrossIsAuto) continue
|
|
1808
|
+
|
|
1809
|
+
// Compute child's cross margin
|
|
1810
|
+
const cCrossMargin = isRow
|
|
1811
|
+
? resolveEdgeValue(cstyle.margin, 1, style.flexDirection, contentWidth, direction) +
|
|
1812
|
+
resolveEdgeValue(cstyle.margin, 3, style.flexDirection, contentWidth, direction)
|
|
1813
|
+
: resolveEdgeValue(cstyle.margin, 0, style.flexDirection, contentWidth, direction) +
|
|
1814
|
+
resolveEdgeValue(cstyle.margin, 2, style.flexDirection, contentWidth, direction)
|
|
1815
|
+
const stretchedCross = finalCross - cCrossMargin
|
|
1816
|
+
|
|
1817
|
+
// Only re-layout if the cross size actually changed
|
|
1818
|
+
const currentCross = isRow ? child.layout.height : child.layout.width
|
|
1819
|
+
if (Math.round(stretchedCross) <= currentCross) continue
|
|
1820
|
+
|
|
1821
|
+
// Re-layout child with the definite cross size
|
|
1822
|
+
// Save position — layoutNode overwrites layout.left/top
|
|
1823
|
+
const savedLeft = child.layout.left
|
|
1824
|
+
const savedTop = child.layout.top
|
|
1825
|
+
const cMarginL = resolveEdgeValue(cstyle.margin, 0, style.flexDirection, contentWidth, direction)
|
|
1826
|
+
const cMarginT = resolveEdgeValue(cstyle.margin, 1, style.flexDirection, contentWidth, direction)
|
|
1827
|
+
const cAbsX = absX + innerLeft + savedLeft - cMarginL
|
|
1828
|
+
const cAbsY = absY + innerTop + savedTop - cMarginT
|
|
1829
|
+
const passW = isRow ? child.layout.width : stretchedCross
|
|
1830
|
+
const passH = isRow ? stretchedCross : child.layout.height
|
|
1831
|
+
layoutNode(child, passW, passH, savedLeft, savedTop, cAbsX, cAbsY, direction)
|
|
1832
|
+
// Restore position and override cross dimension to stretched size
|
|
1833
|
+
child.layout.left = savedLeft
|
|
1834
|
+
child.layout.top = savedTop
|
|
1835
|
+
if (isRow) {
|
|
1836
|
+
child.layout.height = Math.round(stretchedCross)
|
|
1837
|
+
} else {
|
|
1838
|
+
child.layout.width = Math.round(stretchedCross)
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// -----------------------------------------------------------------------
|
|
1843
|
+
// PHASE 9c: Recompute cross-axis alignment after shrink-wrap
|
|
1844
|
+
// -----------------------------------------------------------------------
|
|
1845
|
+
// When the parent's cross axis was auto (NaN during Phase 8), alignment
|
|
1846
|
+
// offsets for CENTER, FLEX_END, and auto margins computed NaN because
|
|
1847
|
+
// availableCrossSpace = NaN - childSize - margin = NaN.
|
|
1848
|
+
// Now that cross size is known from shrink-wrap, recompute those offsets.
|
|
1849
|
+
if (Number.isNaN(crossAxisSize) && relativeCount > 0) {
|
|
1850
|
+
const finalCross9c = isRow ? nodeHeight - innerTop - innerBottom : nodeWidth - innerLeft - innerRight
|
|
1851
|
+
if (!Number.isNaN(finalCross9c) && finalCross9c > 0) {
|
|
1852
|
+
for (const child of node.children) {
|
|
1853
|
+
if (child.flex.relativeIndex < 0) continue
|
|
1854
|
+
const cstyle = child.style
|
|
1855
|
+
let childAlign = style.alignItems
|
|
1856
|
+
if (cstyle.alignSelf !== C.ALIGN_AUTO) {
|
|
1857
|
+
childAlign = cstyle.alignSelf
|
|
1858
|
+
}
|
|
1859
|
+
const cCrossDim = isRow ? cstyle.height : cstyle.width
|
|
1860
|
+
const cCrossIsAuto = cCrossDim.unit === C.UNIT_AUTO || cCrossDim.unit === C.UNIT_UNDEFINED
|
|
1861
|
+
if (
|
|
1862
|
+
childAlign === C.ALIGN_STRETCH &&
|
|
1863
|
+
cstyle.alignSelf === C.ALIGN_AUTO &&
|
|
1864
|
+
!Number.isNaN(cstyle.aspectRatio) &&
|
|
1865
|
+
cstyle.aspectRatio > 0 &&
|
|
1866
|
+
cCrossIsAuto
|
|
1867
|
+
) {
|
|
1868
|
+
childAlign = C.ALIGN_FLEX_START
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
const crossStartIdx = isRow ? 1 : 0
|
|
1872
|
+
const crossEndIdx = isRow ? 3 : 2
|
|
1873
|
+
const hasAutoStart = isEdgeAuto(cstyle.margin, crossStartIdx, style.flexDirection, direction)
|
|
1874
|
+
const hasAutoEnd = isEdgeAuto(cstyle.margin, crossEndIdx, style.flexDirection, direction)
|
|
1875
|
+
const needsAlignment =
|
|
1876
|
+
hasAutoStart || hasAutoEnd || childAlign === C.ALIGN_CENTER || childAlign === C.ALIGN_FLEX_END
|
|
1877
|
+
if (!needsAlignment) continue
|
|
1878
|
+
|
|
1879
|
+
const childCrossSize = isRow ? child.layout.height : child.layout.width
|
|
1880
|
+
const cCrossMargin = isRow
|
|
1881
|
+
? resolveEdgeValue(cstyle.margin, 1, style.flexDirection, contentWidth, direction) +
|
|
1882
|
+
resolveEdgeValue(cstyle.margin, 3, style.flexDirection, contentWidth, direction)
|
|
1883
|
+
: resolveEdgeValue(cstyle.margin, 0, style.flexDirection, contentWidth, direction) +
|
|
1884
|
+
resolveEdgeValue(cstyle.margin, 2, style.flexDirection, contentWidth, direction)
|
|
1885
|
+
const availSpace = finalCross9c - childCrossSize - cCrossMargin
|
|
1886
|
+
|
|
1887
|
+
let crossOffset = 0
|
|
1888
|
+
if (hasAutoStart && hasAutoEnd) {
|
|
1889
|
+
crossOffset = Math.max(0, availSpace) / 2
|
|
1890
|
+
} else if (hasAutoStart) {
|
|
1891
|
+
crossOffset = Math.max(0, availSpace)
|
|
1892
|
+
} else if (hasAutoEnd) {
|
|
1893
|
+
crossOffset = 0
|
|
1894
|
+
} else {
|
|
1895
|
+
switch (childAlign) {
|
|
1896
|
+
case C.ALIGN_FLEX_END:
|
|
1897
|
+
crossOffset = availSpace
|
|
1898
|
+
break
|
|
1899
|
+
case C.ALIGN_CENTER:
|
|
1900
|
+
crossOffset = availSpace / 2
|
|
1901
|
+
break
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
if (isRow) {
|
|
1906
|
+
if (Number.isNaN(child.layout.top)) {
|
|
1907
|
+
const cMarginT = resolveEdgeValue(cstyle.margin, 1, style.flexDirection, contentWidth, direction)
|
|
1908
|
+
child.layout.top = Math.round(cMarginT + crossOffset)
|
|
1909
|
+
} else if (crossOffset !== 0) {
|
|
1910
|
+
child.layout.top += Math.round(crossOffset)
|
|
1911
|
+
}
|
|
1912
|
+
} else {
|
|
1913
|
+
if (Number.isNaN(child.layout.left)) {
|
|
1914
|
+
const cMarginL = resolveEdgeValue(cstyle.margin, 0, style.flexDirection, contentWidth, direction)
|
|
1915
|
+
child.layout.left = Math.round(cMarginL + crossOffset)
|
|
1916
|
+
} else if (crossOffset !== 0) {
|
|
1917
|
+
child.layout.left += Math.round(crossOffset)
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1542
1926
|
// =========================================================================
|
|
1543
1927
|
// PHASE 10: Final Output - Set Node Layout
|
|
1544
1928
|
// =========================================================================
|
|
@@ -1606,9 +1990,10 @@ function layoutNode(
|
|
|
1606
1990
|
const hasAutoMarginBottom = isEdgeAuto(childStyle.margin, 3, style.flexDirection, direction)
|
|
1607
1991
|
|
|
1608
1992
|
// Position offsets from setPosition(edge, value)
|
|
1609
|
-
|
|
1993
|
+
// Resolve logical EDGE_START/EDGE_END to physical left/right based on direction
|
|
1994
|
+
const leftPos = resolvePositionEdge(childStyle.position, 0, direction)
|
|
1610
1995
|
const topPos = childStyle.position[1]
|
|
1611
|
-
const rightPos = childStyle.position
|
|
1996
|
+
const rightPos = resolvePositionEdge(childStyle.position, 2, direction)
|
|
1612
1997
|
const bottomPos = childStyle.position[3]
|
|
1613
1998
|
|
|
1614
1999
|
const hasLeft = leftPos.unit !== C.UNIT_UNDEFINED
|
|
@@ -1616,10 +2001,11 @@ function layoutNode(
|
|
|
1616
2001
|
const hasTop = topPos.unit !== C.UNIT_UNDEFINED
|
|
1617
2002
|
const hasBottom = bottomPos.unit !== C.UNIT_UNDEFINED
|
|
1618
2003
|
|
|
1619
|
-
|
|
1620
|
-
const
|
|
1621
|
-
const
|
|
1622
|
-
const
|
|
2004
|
+
// Yoga resolves percentage position offsets against the content box dimensions
|
|
2005
|
+
const leftOffset = resolveValue(leftPos, absContentBoxW)
|
|
2006
|
+
const topOffset = resolveValue(topPos, absContentBoxH)
|
|
2007
|
+
const rightOffset = resolveValue(rightPos, absContentBoxW)
|
|
2008
|
+
const bottomOffset = resolveValue(bottomPos, absContentBoxH)
|
|
1623
2009
|
|
|
1624
2010
|
// Calculate available size for absolute child using padding box
|
|
1625
2011
|
const contentW = absPaddingBoxW
|
|
@@ -1692,28 +2078,45 @@ function layoutNode(
|
|
|
1692
2078
|
const childHeight = child.layout.height
|
|
1693
2079
|
|
|
1694
2080
|
// Apply alignment when no explicit position set
|
|
1695
|
-
// For absolute children, align-items
|
|
2081
|
+
// For absolute children, align-items applies on cross axis, justify-content on main axis
|
|
2082
|
+
// Row: X = main axis (justifyContent), Y = cross axis (alignItems)
|
|
2083
|
+
// Column: X = cross axis (alignItems), Y = main axis (justifyContent)
|
|
1696
2084
|
if (!hasLeft && !hasRight) {
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
2085
|
+
if (isRow) {
|
|
2086
|
+
// Row: X is main axis, use justifyContent
|
|
2087
|
+
const freeSpaceX = contentW - childWidth - childMarginLeft - childMarginRight
|
|
2088
|
+
switch (style.justifyContent) {
|
|
2089
|
+
case C.JUSTIFY_CENTER:
|
|
2090
|
+
childX = childMarginLeft + freeSpaceX / 2
|
|
2091
|
+
break
|
|
2092
|
+
case C.JUSTIFY_FLEX_END:
|
|
2093
|
+
childX = childMarginLeft + freeSpaceX
|
|
2094
|
+
break
|
|
2095
|
+
default: // FLEX_START
|
|
2096
|
+
childX = childMarginLeft
|
|
2097
|
+
break
|
|
2098
|
+
}
|
|
2099
|
+
} else {
|
|
2100
|
+
// Column: X is cross axis, use alignItems/alignSelf
|
|
2101
|
+
let alignment = style.alignItems
|
|
2102
|
+
if (childStyle.alignSelf !== C.ALIGN_AUTO) {
|
|
2103
|
+
alignment = childStyle.alignSelf
|
|
2104
|
+
}
|
|
2105
|
+
const freeSpaceX = contentW - childWidth - childMarginLeft - childMarginRight
|
|
2106
|
+
switch (alignment) {
|
|
2107
|
+
case C.ALIGN_CENTER:
|
|
2108
|
+
childX = childMarginLeft + freeSpaceX / 2
|
|
2109
|
+
break
|
|
2110
|
+
case C.ALIGN_FLEX_END:
|
|
2111
|
+
childX = childMarginLeft + freeSpaceX
|
|
2112
|
+
break
|
|
2113
|
+
case C.ALIGN_STRETCH:
|
|
2114
|
+
// Stretch: already handled by setting width to fill
|
|
2115
|
+
break
|
|
2116
|
+
default: // FLEX_START
|
|
2117
|
+
childX = childMarginLeft
|
|
2118
|
+
break
|
|
2119
|
+
}
|
|
1717
2120
|
}
|
|
1718
2121
|
} else if (!hasLeft && hasRight) {
|
|
1719
2122
|
// Position from right edge
|
|
@@ -1739,19 +2142,41 @@ function layoutNode(
|
|
|
1739
2142
|
}
|
|
1740
2143
|
|
|
1741
2144
|
if (!hasTop && !hasBottom) {
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
2145
|
+
if (isRow) {
|
|
2146
|
+
// Row: Y is cross axis, use alignItems/alignSelf
|
|
2147
|
+
let alignment = style.alignItems
|
|
2148
|
+
if (childStyle.alignSelf !== C.ALIGN_AUTO) {
|
|
2149
|
+
alignment = childStyle.alignSelf
|
|
2150
|
+
}
|
|
2151
|
+
const freeSpaceY = contentH - childHeight - childMarginTop - childMarginBottom
|
|
2152
|
+
switch (alignment) {
|
|
2153
|
+
case C.ALIGN_CENTER:
|
|
2154
|
+
childY = childMarginTop + freeSpaceY / 2
|
|
2155
|
+
break
|
|
2156
|
+
case C.ALIGN_FLEX_END:
|
|
2157
|
+
childY = childMarginTop + freeSpaceY
|
|
2158
|
+
break
|
|
2159
|
+
case C.ALIGN_STRETCH:
|
|
2160
|
+
// Stretch: already handled by setting height to fill
|
|
2161
|
+
break
|
|
2162
|
+
default: // FLEX_START
|
|
2163
|
+
childY = childMarginTop
|
|
2164
|
+
break
|
|
2165
|
+
}
|
|
2166
|
+
} else {
|
|
2167
|
+
// Column: Y is main axis, use justifyContent
|
|
2168
|
+
const freeSpaceY = contentH - childHeight - childMarginTop - childMarginBottom
|
|
2169
|
+
switch (style.justifyContent) {
|
|
2170
|
+
case C.JUSTIFY_CENTER:
|
|
2171
|
+
childY = childMarginTop + freeSpaceY / 2
|
|
2172
|
+
break
|
|
2173
|
+
case C.JUSTIFY_FLEX_END:
|
|
2174
|
+
childY = childMarginTop + freeSpaceY
|
|
2175
|
+
break
|
|
2176
|
+
default: // FLEX_START
|
|
2177
|
+
childY = childMarginTop
|
|
2178
|
+
break
|
|
2179
|
+
}
|
|
1755
2180
|
}
|
|
1756
2181
|
} else if (!hasTop && hasBottom) {
|
|
1757
2182
|
// Position from bottom edge
|
|
@@ -1786,6 +2211,8 @@ function layoutNode(
|
|
|
1786
2211
|
flex.lastAvailH = availableHeight
|
|
1787
2212
|
flex.lastOffsetX = offsetX
|
|
1788
2213
|
flex.lastOffsetY = offsetY
|
|
2214
|
+
flex.lastAbsX = absX
|
|
2215
|
+
flex.lastAbsY = absY
|
|
1789
2216
|
flex.lastDir = direction
|
|
1790
2217
|
flex.layoutValid = true
|
|
1791
2218
|
_t?.layoutExit(_tn, layout.width, layout.height)
|