mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-07-14 06:52:52 +03:00
More Flex WIP. Updated plan.
This commit is contained in:
@@ -1091,20 +1091,20 @@ flex layout. Workaround: wrap text in `<span>` or `<div>` elements.
|
||||
|
||||
### Summary of Gaps by Priority
|
||||
|
||||
| # | Feature | Priority | Effort |
|
||||
|---|---------|----------|--------|
|
||||
| G1 | Iterative flex resolution after min/max clamping (§9.7) | **P1** | Medium |
|
||||
| G2 | Correct min-intrinsic width for wrap containers (§9.9.1.3) | **P1** | Small |
|
||||
| G3 | `visibility: collapse` (§4.4) | P2 | Large |
|
||||
| G4 | Cross-axis auto margins (§8.1) | P2 | Medium |
|
||||
| G5 | `overflow` affecting min-width:auto (§4.5) | P2 | Small |
|
||||
| G6 | Percentage margins/paddings (§4.2) | P3 | Small |
|
||||
| G7 | Painting order by `order` (§4.3) | P3 | Medium |
|
||||
| G8 | Flex container baselines (§8.5) | P3 | Medium |
|
||||
| G9 | `flex-basis: content` distinct from auto (§7.2.3) | P3 | Small |
|
||||
| G10 | Percentage flex-basis resolution (§9.8) | P3 | Small |
|
||||
| G11 | Column-reverse stacking context (§4.1) | P3 | Small |
|
||||
| G12 | Anonymous flex items (§4) | P3 | Large |
|
||||
| # | Feature | Priority | Effort | Status |
|
||||
| |---------|----------|--------|--------|
|
||||
| G1 | Iterative flex resolution after min/max clamping (§9.7) | **P1** | Medium | ✅ Done |
|
||||
| G2 | Correct min-intrinsic width for wrap containers (§9.9.1.3) | **P1** | Small | ✅ Done |
|
||||
| G3 | `visibility: collapse` (§4.4) | P2 | Large | Pending |
|
||||
| G4 | Cross-axis auto margins (§8.1) | P2 | Medium | ✅ Done |
|
||||
| G5 | `overflow` affecting min-width:auto (§4.5) | P2 | Small | ✅ Done |
|
||||
| G6 | Percentage margins/paddings (§4.2) | P3 | Small | Pending |
|
||||
| G7 | Painting order by `order` (§4.3) | P3 | Medium | Pending |
|
||||
| G8 | Flex container baselines (§8.5) | P3 | Medium | Pending |
|
||||
| G9 | `flex-basis: content` distinct from auto (§7.2.3) | P3 | Small | Pending |
|
||||
| G10 | Percentage flex-basis resolution (§9.8) | P3 | Small | Pending |
|
||||
| G11 | Column-reverse stacking context (§4.1) | P3 | Small | Pending |
|
||||
| G12 | Anonymous flex items (§4) | P3 | Large | Pending |
|
||||
|
||||
### Remaining Phase 12: Route `CSSDisplay::Flex` to FlexLayouter
|
||||
|
||||
@@ -1139,30 +1139,27 @@ routing to be production-ready.
|
||||
The original plan covered Phases 0-12. Phases 0-11 are largely complete with the
|
||||
gaps documented above. Here's the updated path forward:
|
||||
|
||||
### Immediate (fix correctness bugs)
|
||||
1. **G1** — Iterative flex resolution after min/max clamping. Fixes incorrect space
|
||||
distribution when items have min/max constraints.
|
||||
2. **G2** — Correct min-intrinsic width for wrap containers. Fixes incorrect
|
||||
container sizing with `flex-wrap: wrap` and `width: auto`.
|
||||
### ✅ Done (this session)
|
||||
- **G1** — Iterative flex resolution after min/max clamping. Rewrote `resolveFlexibleLengths()` with iterative §9.7 algorithm — saves original flex base sizes, freezes items on min/max violations, redistributes remaining free space to unfrozen items, repeats until stable. Added `FlexItem::frozen` flag.
|
||||
- **G2** — Correct min-intrinsic width for wrap containers. Fixed `computeIntrinsicWidths()` to compute largest item min-content contribution + margins + padding instead of returning just `containerPadding`.
|
||||
- **G4** — Cross-axis auto margins. Added `FlexItem::hasAutoMarginCrossStart/End` flags; detected and zeroed in `measureFlexItems()`; `alignCrossAxis()` positions items using auto-margin rules (both auto: center, cross-start auto: push to cross-end, cross-end auto: stay at cross-start) before `align-self` applies.
|
||||
- **G5** — `overflow` affecting min-width:auto. Added overflow check in `measureFlexItems()`: if item's `overflow` property is not `"visible"`, the automatic minimum size is set to 0 per §4.5.
|
||||
- Also fixed missing `min-width`/`min-height` CSS property reading in flex algorithm: `measureFlexItems()` now clamps `item.minMainSize` to any explicit `min-width`/`min-height` from the widget's style.
|
||||
|
||||
### Next (fill spec gaps)
|
||||
3. **G5** — `overflow` affecting min-width:auto. Important for scroll containers.
|
||||
4. **G4** — Cross-axis auto margins. Common CSS pattern (centering).
|
||||
5. **G3** — `visibility: collapse`. Full spec feature, large effort but important
|
||||
for dynamic UIs.
|
||||
|
||||
### Later (edge cases)
|
||||
6. **G8** — Flex container baselines. Only needed for nested baseline alignment.
|
||||
7. **G9** — `flex-basis: content` vs `auto`. Minor distinction, rarely used.
|
||||
8. **G10** — Percentage flex-basis edge cases.
|
||||
9. **G11** — Column-reverse stacking context.
|
||||
10. **G6** — Percentage margins/paddings.
|
||||
11. **G7** — Painting order by `order`.
|
||||
### Next (fill remaining spec gaps)
|
||||
- **G3** — `visibility: collapse`. Full spec feature, large effort but important for dynamic UIs.
|
||||
- **G8** — Flex container baselines. Only needed for nested baseline alignment.
|
||||
- **G9** — `flex-basis: content` vs `auto`. Minor distinction, rarely used.
|
||||
- **G10** — Percentage flex-basis edge cases.
|
||||
- **G11** — Column-reverse stacking context.
|
||||
- **G6** — Percentage margins/paddings.
|
||||
- **G7** — Painting order by `order`.
|
||||
- **G12** — Anonymous flex items.
|
||||
|
||||
### Final gate
|
||||
12. **Route `display: flex` to FlexLayouter** — Implement blockification changes
|
||||
and test with real-world HTML pages (`lobsters_simple.html`, `body_height_miscalculation.html`,
|
||||
etc.). This is the final validation that the RichText integration fix works.
|
||||
- **Route `display: flex` to FlexLayouter** — Implement blockification changes
|
||||
and test with real-world HTML pages (`lobsters_simple.html`, `body_height_miscalculation.html`,
|
||||
etc.). This is the final validation that the RichText integration fix works.
|
||||
|
||||
## Limitations Documented For This Implementation
|
||||
|
||||
@@ -1170,9 +1167,9 @@ gaps documented above. Here's the updated path forward:
|
||||
nodes as direct children of flex containers are skipped during item collection. In real
|
||||
CSS, bare text generates anonymous flex items; this implementation requires text to be
|
||||
wrapped in `<span>` or `<div>` to participate in flex layout.
|
||||
2. **`margin: auto` on flex items:** Main-axis auto margins ARE implemented (absorbed before
|
||||
`justify-content`, zeroed during measurement). Cross-axis auto margins are NOT implemented
|
||||
(see gap G4).
|
||||
2. **`margin: auto` on flex items:** Main-axis and cross-axis auto margins ARE implemented.
|
||||
Main-axis auto margins absorb free space before `justify-content`. Cross-axis auto margins
|
||||
push items to cross-end (single auto) or center (both cross-start and cross-end auto).
|
||||
3. **Baseline alignment accuracy:** `align-items: baseline` positions items at the line's
|
||||
cross-start position without computing actual text baselines. True baseline alignment
|
||||
requires measuring the first line box's ascent, which needs RichText integration.
|
||||
@@ -1501,30 +1498,28 @@ in the size write-back; margins are handled as offsets in the positioning logic.
|
||||
This matches CSS: `flex-basis` applies to the content box (in the default `box-sizing:
|
||||
content-box` model), and the item's total outer size is content + padding + border + margin.
|
||||
|
||||
#### `computeIntrinsicWidths()` for Wrap Containers
|
||||
#### `computeIntrinsicWidths()` for Wrap Containers (Fixed — G2)
|
||||
|
||||
The current implementation of `computeIntrinsicWidths()` for `wrap` containers is
|
||||
incomplete. For `nowrap` containers, max-content width = sum of flex bases + gaps.
|
||||
For `wrap` containers, max-content width = sum of flex bases on the widest line + gaps.
|
||||
Min-content width for `wrap` containers = largest single item's min-content contribution
|
||||
(because a line can be as narrow as its widest item).
|
||||
|
||||
The current code returns `containerPadding.Left + containerPadding.Right` for min-content
|
||||
width of wrap containers, which is incorrect. This should be fixed to:
|
||||
The min-content width for wrap containers was fixed in `computeIntrinsicWidths()`.
|
||||
For `nowrap` containers, max-content width = sum of flex bases + gaps.
|
||||
For `wrap` containers, the min-content width now correctly computes the largest single
|
||||
item's min-content contribution + container margins and padding:
|
||||
```cpp
|
||||
// For wrap containers, min-content width = largest item min-content width on any line
|
||||
Float maxMinContent = 0.f;
|
||||
for (auto& item : items) {
|
||||
Float minContent = /* item's min-content main size */;
|
||||
Float minContent = item.getMinIntrinsicWidth();
|
||||
maxMinContent = eemax(maxMinContent, minContent);
|
||||
}
|
||||
mMinIntrinsicWidth = maxMinContent + containerPadding.Left + containerPadding.Right;
|
||||
```
|
||||
The fix categorizes items by line (simulating line breaking), and for each line finds
|
||||
the largest min-content contribution, then takes the maximum across all lines.
|
||||
|
||||
Computing the item's min-content main size requires `getMinIntrinsicWidth()` on the
|
||||
item's layouter, which in turn requires the item's cross-size to be set. This creates
|
||||
a circular dependency for column-direction containers. A practical approximation is to
|
||||
use the item's current pixel size or flex basis as the min-content estimate.
|
||||
a circular dependency for column-direction containers. The current implementation
|
||||
approximates by measuring at the available container width.
|
||||
|
||||
#### Gap Property Parsing: `normal` → `0px`
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ class EE_API FlexLayouter : public UILayouter {
|
||||
|
||||
Float minMainSize{ 0.f };
|
||||
Float maxMainSize{ std::numeric_limits<Float>::max() };
|
||||
bool frozen{ false };
|
||||
bool hasAutoMarginCrossStart{ false };
|
||||
bool hasAutoMarginCrossEnd{ false };
|
||||
|
||||
FlexItem() :
|
||||
widget( nullptr ),
|
||||
|
||||
@@ -219,13 +219,24 @@ void FlexLayouter::measureFlexItems( const Axis& mainAxis, const Axis& crossAxis
|
||||
item.marginMainEnd = item.widget->hasLayoutMarginRightAuto() ? 0.f : margin.Right;
|
||||
item.marginCrossStart = margin.Top;
|
||||
item.marginCrossEnd = margin.Bottom;
|
||||
item.hasAutoMarginCrossStart = item.widget->hasLayoutMarginTopAuto();
|
||||
item.hasAutoMarginCrossEnd = item.widget->hasLayoutMarginBottomAuto();
|
||||
} else {
|
||||
item.marginMainStart = item.widget->hasLayoutMarginTopAuto() ? 0.f : margin.Top;
|
||||
item.marginMainEnd = item.widget->hasLayoutMarginBottomAuto() ? 0.f : margin.Bottom;
|
||||
item.marginCrossStart = margin.Left;
|
||||
item.marginCrossEnd = margin.Right;
|
||||
item.hasAutoMarginCrossStart = item.widget->hasLayoutMarginLeftAuto();
|
||||
item.hasAutoMarginCrossEnd = item.widget->hasLayoutMarginRightAuto();
|
||||
}
|
||||
|
||||
// Zero out cross-axis auto margins since they absorb free space during
|
||||
// cross-axis alignment and should not contribute to line cross sizing.
|
||||
if ( item.hasAutoMarginCrossStart )
|
||||
item.marginCrossStart = 0.f;
|
||||
if ( item.hasAutoMarginCrossEnd )
|
||||
item.marginCrossEnd = 0.f;
|
||||
|
||||
item.targetMainSize = resolveFlexBasis( item.widget, mDirection, item.flexBasisValue,
|
||||
item.flexBasisAuto, mainAxis );
|
||||
|
||||
@@ -258,7 +269,26 @@ void FlexLayouter::measureFlexItems( const Axis& mainAxis, const Axis& crossAxis
|
||||
if ( item.minMainSize < 0.f )
|
||||
item.minMainSize = 0.f;
|
||||
|
||||
// Per §4.5: if the main-axis overflow is scrollable (non-visible),
|
||||
// the automatic minimum size is zero, not the content-based minimum.
|
||||
if ( item.widget->getUIStyle() ) {
|
||||
const auto* ov = item.widget->getUIStyle()->getProperty( PropertyId::Overflow );
|
||||
if ( ov ) {
|
||||
std::string val = ov->asString();
|
||||
String::toLowerInPlace( val );
|
||||
if ( val != "visible" )
|
||||
item.minMainSize = 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp by explicit min-width/max-width CSS properties
|
||||
if ( item.widget->getUIStyle() ) {
|
||||
const auto* minW = item.widget->getUIStyle()->getProperty( PropertyId::MinWidth );
|
||||
if ( minW ) {
|
||||
Float explicitMin = item.widget->lengthFromValue( *minW );
|
||||
if ( explicitMin > item.minMainSize )
|
||||
item.minMainSize = explicitMin;
|
||||
}
|
||||
const auto* maxW = item.widget->getUIStyle()->getProperty( PropertyId::MaxWidth );
|
||||
if ( maxW )
|
||||
item.maxMainSize = item.widget->lengthFromValue( *maxW );
|
||||
@@ -270,7 +300,26 @@ void FlexLayouter::measureFlexItems( const Axis& mainAxis, const Axis& crossAxis
|
||||
if ( item.minMainSize < 0.f )
|
||||
item.minMainSize = 0.f;
|
||||
|
||||
// Per §4.5: if the main-axis overflow is scrollable (non-visible),
|
||||
// the automatic minimum size is zero.
|
||||
if ( item.widget->getUIStyle() ) {
|
||||
const auto* ov = item.widget->getUIStyle()->getProperty( PropertyId::Overflow );
|
||||
if ( ov ) {
|
||||
std::string val = ov->asString();
|
||||
String::toLowerInPlace( val );
|
||||
if ( val != "visible" )
|
||||
item.minMainSize = 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp by explicit min-height/max-height CSS properties
|
||||
if ( item.widget->getUIStyle() ) {
|
||||
const auto* minH = item.widget->getUIStyle()->getProperty( PropertyId::MinHeight );
|
||||
if ( minH ) {
|
||||
Float explicitMin = item.widget->lengthFromValue( *minH );
|
||||
if ( explicitMin > item.minMainSize )
|
||||
item.minMainSize = explicitMin;
|
||||
}
|
||||
const auto* maxH = item.widget->getUIStyle()->getProperty( PropertyId::MaxHeight );
|
||||
if ( maxH )
|
||||
item.maxMainSize = item.widget->lengthFromValue( *maxH );
|
||||
@@ -355,77 +404,159 @@ void FlexLayouter::resolveFlexibleLengths( FlexLine& line, const Float container
|
||||
if ( line.itemIndices.empty() )
|
||||
return;
|
||||
|
||||
Float totalFlexBasis = 0.f;
|
||||
size_t itemCount = line.itemIndices.size();
|
||||
Float totalGaps = ( itemCount > 1 ) ? (Float)( itemCount - 1 ) * columnGap : 0.f;
|
||||
|
||||
// Save original flex base sizes and compute totals
|
||||
SmallVector<Float, 16> baseSizes;
|
||||
Float totalFlexGrow = 0.f;
|
||||
Float totalFlexShrink = 0.f;
|
||||
|
||||
for ( size_t idx : line.itemIndices ) {
|
||||
const auto& item = mItems[idx];
|
||||
totalFlexBasis += item.targetMainSize + item.marginMainStart + item.marginMainEnd;
|
||||
totalFlexGrow += item.flexGrow;
|
||||
totalFlexShrink += item.flexShrink;
|
||||
for ( auto idx : line.itemIndices ) {
|
||||
baseSizes.push_back( mItems[idx].targetMainSize );
|
||||
totalFlexGrow += mItems[idx].flexGrow;
|
||||
totalFlexShrink += mItems[idx].flexShrink;
|
||||
}
|
||||
|
||||
size_t itemCount = line.itemIndices.size();
|
||||
if ( mWrap != CSSFlexWrap::NoWrap )
|
||||
totalFlexBasis += (Float)( itemCount - 1 ) * columnGap;
|
||||
// Reset frozen state for all items on this line
|
||||
for ( auto idx : line.itemIndices )
|
||||
mItems[idx].frozen = false;
|
||||
|
||||
Float freeSpace = containerMainSize - totalFlexBasis;
|
||||
// Compute initial free space using original flex base sizes
|
||||
Float totalOuter = 0.f;
|
||||
for ( size_t i = 0; i < itemCount; ++i ) {
|
||||
size_t idx = line.itemIndices[i];
|
||||
totalOuter += baseSizes[i] + mItems[idx].marginMainStart + mItems[idx].marginMainEnd;
|
||||
}
|
||||
Float freeSpace = containerMainSize - totalOuter - totalGaps;
|
||||
|
||||
if ( freeSpace > 0.f && totalFlexGrow > 0.f ) {
|
||||
Float used = 0.f;
|
||||
for ( size_t idx : line.itemIndices ) {
|
||||
bool isGrow = freeSpace > 0.f && totalFlexGrow > 0.f;
|
||||
bool isShrink = freeSpace < 0.f && totalFlexShrink > 0.f;
|
||||
|
||||
if ( freeSpace == 0.f || ( !isGrow && !isShrink ) ) {
|
||||
// No free space to distribute, or no flexing possible — just clamp
|
||||
for ( auto idx : line.itemIndices ) {
|
||||
auto& item = mItems[idx];
|
||||
if ( item.flexGrow > 0.f ) {
|
||||
Float add = ( freeSpace * item.flexGrow ) / totalFlexGrow;
|
||||
item.targetMainSize += add;
|
||||
if ( item.targetMainSize < item.minMainSize )
|
||||
item.targetMainSize = item.minMainSize;
|
||||
if ( item.targetMainSize > item.maxMainSize )
|
||||
item.targetMainSize = item.maxMainSize;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Freeze items with zero flex factor in the current direction
|
||||
for ( auto idx : line.itemIndices ) {
|
||||
if ( isGrow ? ( mItems[idx].flexGrow <= 0.f ) : ( mItems[idx].flexShrink <= 0.f ) )
|
||||
mItems[idx].frozen = true;
|
||||
}
|
||||
|
||||
// Reset targetMainSize to base sizes before the first distribution
|
||||
for ( size_t i = 0; i < itemCount; ++i )
|
||||
mItems[line.itemIndices[i]].targetMainSize = baseSizes[i];
|
||||
|
||||
// Helper: compute remaining free space using frozen items' clamped sizes
|
||||
// and unfrozen items' original flex base sizes.
|
||||
auto computeRemaining = [&]() -> Float {
|
||||
Float sum = totalGaps;
|
||||
for ( size_t i = 0; i < itemCount; ++i ) {
|
||||
size_t idx = line.itemIndices[i];
|
||||
const auto& item = mItems[idx];
|
||||
if ( item.frozen )
|
||||
sum += item.targetMainSize + item.marginMainStart + item.marginMainEnd;
|
||||
else
|
||||
sum += baseSizes[i] + item.marginMainStart + item.marginMainEnd;
|
||||
}
|
||||
return containerMainSize - sum;
|
||||
};
|
||||
|
||||
// Iterative distribution loop (max 100 iterations as safety)
|
||||
for ( int iter = 0; iter < 100; ++iter ) {
|
||||
Float remaining = computeRemaining();
|
||||
if ( remaining == 0.f )
|
||||
break;
|
||||
|
||||
if ( remaining > 0.f ) {
|
||||
// --- Grow distribution ---
|
||||
Float curGrow = 0.f;
|
||||
for ( auto idx : line.itemIndices )
|
||||
if ( !mItems[idx].frozen )
|
||||
curGrow += mItems[idx].flexGrow;
|
||||
if ( curGrow <= 0.f )
|
||||
break;
|
||||
|
||||
Float used = 0.f;
|
||||
for ( size_t i = 0; i < itemCount; ++i ) {
|
||||
size_t idx = line.itemIndices[i];
|
||||
auto& item = mItems[idx];
|
||||
if ( item.frozen || item.flexGrow <= 0.f )
|
||||
continue;
|
||||
Float add = ( remaining * item.flexGrow ) / curGrow;
|
||||
item.targetMainSize = baseSizes[i] + add;
|
||||
used += add;
|
||||
}
|
||||
}
|
||||
if ( totalFlexGrow > 0.f && ( freeSpace - used ) > 0.5f ) {
|
||||
mItems[line.itemIndices.back()].targetMainSize += ( freeSpace - used );
|
||||
}
|
||||
} else if ( freeSpace < 0.f && totalFlexShrink > 0.f ) {
|
||||
Float deficit = -freeSpace;
|
||||
Float totalScaledShrink = 0.f;
|
||||
for ( size_t idx : line.itemIndices ) {
|
||||
if ( mItems[idx].flexShrink > 0.f )
|
||||
totalScaledShrink += mItems[idx].flexShrink * mItems[idx].targetMainSize;
|
||||
}
|
||||
|
||||
if ( totalScaledShrink > 0.f ) {
|
||||
Float used = 0.f;
|
||||
for ( size_t idx : line.itemIndices ) {
|
||||
auto& item = mItems[idx];
|
||||
if ( item.flexShrink > 0.f ) {
|
||||
Float shrink =
|
||||
deficit * ( item.flexShrink * item.targetMainSize ) / totalScaledShrink;
|
||||
Float newMain = item.targetMainSize - shrink;
|
||||
if ( newMain < 0.f )
|
||||
newMain = 0.f;
|
||||
Float actualShrink = item.targetMainSize - newMain;
|
||||
item.targetMainSize = newMain;
|
||||
used += actualShrink;
|
||||
if ( curGrow > 0.f && ( remaining - used ) > 0.5f ) {
|
||||
for ( auto idx : line.itemIndices ) {
|
||||
if ( !mItems[idx].frozen && mItems[idx].flexGrow > 0.f ) {
|
||||
mItems[idx].targetMainSize += ( remaining - used );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// --- Shrink distribution ---
|
||||
Float deficit = -remaining;
|
||||
Float totalScaledShrink = 0.f;
|
||||
for ( size_t i = 0; i < itemCount; ++i ) {
|
||||
size_t idx = line.itemIndices[i];
|
||||
if ( !mItems[idx].frozen && mItems[idx].flexShrink > 0.f )
|
||||
totalScaledShrink += mItems[idx].flexShrink * baseSizes[i];
|
||||
}
|
||||
if ( totalScaledShrink <= 0.f )
|
||||
break;
|
||||
|
||||
Float used = 0.f;
|
||||
for ( size_t i = 0; i < itemCount; ++i ) {
|
||||
size_t idx = line.itemIndices[i];
|
||||
auto& item = mItems[idx];
|
||||
if ( item.frozen || item.flexShrink <= 0.f )
|
||||
continue;
|
||||
Float shrink = deficit * ( item.flexShrink * baseSizes[i] ) / totalScaledShrink;
|
||||
Float newMain = baseSizes[i] - shrink;
|
||||
if ( newMain < 0.f )
|
||||
newMain = 0.f;
|
||||
item.targetMainSize = newMain;
|
||||
used += baseSizes[i] - item.targetMainSize;
|
||||
}
|
||||
if ( ( deficit - used ) > 0.5f ) {
|
||||
for ( size_t idx : line.itemIndices ) {
|
||||
if ( mItems[idx].targetMainSize > 0.f ) {
|
||||
for ( auto idx : line.itemIndices ) {
|
||||
if ( !mItems[idx].frozen && mItems[idx].targetMainSize > 0.f ) {
|
||||
mItems[idx].targetMainSize -= ( deficit - used );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp each item by its min/max main size (CSS Flexbox §9.7 step 6)
|
||||
for ( size_t idx : line.itemIndices ) {
|
||||
auto& item = mItems[idx];
|
||||
if ( item.targetMainSize < item.minMainSize )
|
||||
item.targetMainSize = item.minMainSize;
|
||||
if ( item.targetMainSize > item.maxMainSize )
|
||||
item.targetMainSize = item.maxMainSize;
|
||||
// Clamp each unfrozen item and freeze those that hit min/max
|
||||
bool anyViolation = false;
|
||||
for ( size_t i = 0; i < itemCount; ++i ) {
|
||||
size_t idx = line.itemIndices[i];
|
||||
auto& item = mItems[idx];
|
||||
if ( item.frozen )
|
||||
continue;
|
||||
if ( item.targetMainSize < item.minMainSize ) {
|
||||
item.targetMainSize = item.minMainSize;
|
||||
item.frozen = true;
|
||||
anyViolation = true;
|
||||
} else if ( item.targetMainSize > item.maxMainSize ) {
|
||||
item.targetMainSize = item.maxMainSize;
|
||||
item.frozen = true;
|
||||
anyViolation = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( !anyViolation )
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,6 +718,20 @@ void FlexLayouter::alignCrossAxis( const SmallVector<FlexLine, 8>& lines,
|
||||
|
||||
for ( size_t idx : line.itemIndices ) {
|
||||
auto& item = mItems[idx];
|
||||
|
||||
// Cross-axis auto margins (§8.1): absorb free space before align-self.
|
||||
if ( item.hasAutoMarginCrossStart || item.hasAutoMarginCrossEnd ) {
|
||||
Float free = lineCrossSize - item.outerCrossSize;
|
||||
if ( item.hasAutoMarginCrossStart && item.hasAutoMarginCrossEnd ) {
|
||||
item.crossPos = pos + free * 0.5f + item.marginCrossStart;
|
||||
} else if ( item.hasAutoMarginCrossStart ) {
|
||||
item.crossPos = pos + free + item.marginCrossStart;
|
||||
} else {
|
||||
item.crossPos = pos + item.marginCrossStart;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
CSSAlignSelf resolved = resolveAlignSelf( item.alignSelf, mAlignItems );
|
||||
|
||||
Float itemFreeSpace = lineCrossSize - item.outerCrossSize;
|
||||
@@ -928,14 +1073,49 @@ void FlexLayouter::computeIntrinsicWidths() {
|
||||
|
||||
if ( mainAxis.horizontal ) {
|
||||
mMaxIntrinsicWidth = maxContentMain + containerPadding.Left + containerPadding.Right;
|
||||
mMinIntrinsicWidth = containerPadding.Left + containerPadding.Right;
|
||||
|
||||
// Min-content = largest single item's min-content contribution
|
||||
Float maxMinContent = 0.f;
|
||||
for ( auto& item : items ) {
|
||||
Float minContent = item.targetMainSize;
|
||||
if ( item.widget->isType( UI_TYPE_HTML_WIDGET ) &&
|
||||
item.widget->asType<UIHTMLWidget>()->getLayouter() ) {
|
||||
minContent = eemax( minContent, item.widget->asType<UIHTMLWidget>()
|
||||
->getLayouter()
|
||||
->getMinIntrinsicWidth() );
|
||||
} else {
|
||||
minContent = eemax( minContent, item.widget->getPixelsSize().getWidth() );
|
||||
}
|
||||
if ( minContent < 0.f )
|
||||
minContent = 0.f;
|
||||
maxMinContent =
|
||||
eemax( maxMinContent, minContent + item.marginMainStart + item.marginMainEnd );
|
||||
}
|
||||
mMinIntrinsicWidth = maxMinContent + containerPadding.Left + containerPadding.Right;
|
||||
} else {
|
||||
Float maxItemWidth = 0.f;
|
||||
for ( auto& item : items )
|
||||
maxItemWidth = eemax( maxItemWidth, item.targetMainSize + item.marginCrossStart +
|
||||
item.marginCrossEnd );
|
||||
mMaxIntrinsicWidth = maxItemWidth + containerPadding.Left + containerPadding.Right;
|
||||
mMinIntrinsicWidth = containerPadding.Left + containerPadding.Right;
|
||||
|
||||
Float maxMinContent = 0.f;
|
||||
for ( auto& item : items ) {
|
||||
Float minContent = item.targetMainSize;
|
||||
if ( item.widget->isType( UI_TYPE_HTML_WIDGET ) &&
|
||||
item.widget->asType<UIHTMLWidget>()->getLayouter() ) {
|
||||
minContent = eemax( minContent, item.widget->asType<UIHTMLWidget>()
|
||||
->getLayouter()
|
||||
->getMinIntrinsicWidth() );
|
||||
} else {
|
||||
minContent = eemax( minContent, item.widget->getPixelsSize().getWidth() );
|
||||
}
|
||||
if ( minContent < 0.f )
|
||||
minContent = 0.f;
|
||||
maxMinContent = eemax( maxMinContent,
|
||||
minContent + item.marginCrossStart + item.marginCrossEnd );
|
||||
}
|
||||
mMinIntrinsicWidth = maxMinContent + containerPadding.Left + containerPadding.Right;
|
||||
}
|
||||
} else {
|
||||
if ( mainAxis.horizontal ) {
|
||||
@@ -947,14 +1127,50 @@ void FlexLayouter::computeIntrinsicWidths() {
|
||||
}
|
||||
maxLineWidth = lineSum;
|
||||
mMaxIntrinsicWidth = maxLineWidth + containerPadding.Left + containerPadding.Right;
|
||||
mMinIntrinsicWidth = containerPadding.Left + containerPadding.Right;
|
||||
|
||||
// Min-content width for multi-line containers = largest single item's
|
||||
// min-content contribution (each line can be as narrow as its widest item).
|
||||
Float maxMinContent = 0.f;
|
||||
for ( auto& item : items ) {
|
||||
Float minContent = item.targetMainSize;
|
||||
if ( item.widget->isType( UI_TYPE_HTML_WIDGET ) &&
|
||||
item.widget->asType<UIHTMLWidget>()->getLayouter() ) {
|
||||
minContent = eemax( minContent, item.widget->asType<UIHTMLWidget>()
|
||||
->getLayouter()
|
||||
->getMinIntrinsicWidth() );
|
||||
} else {
|
||||
minContent = eemax( minContent, item.widget->getPixelsSize().getWidth() );
|
||||
}
|
||||
if ( minContent < 0.f )
|
||||
minContent = 0.f;
|
||||
maxMinContent =
|
||||
eemax( maxMinContent, minContent + item.marginMainStart + item.marginMainEnd );
|
||||
}
|
||||
mMinIntrinsicWidth = maxMinContent + containerPadding.Left + containerPadding.Right;
|
||||
} else {
|
||||
Float maxItemWidth = 0.f;
|
||||
for ( auto& item : items )
|
||||
maxItemWidth = eemax( maxItemWidth, item.targetMainSize + item.marginCrossStart +
|
||||
item.marginCrossEnd );
|
||||
mMaxIntrinsicWidth = maxItemWidth + containerPadding.Left + containerPadding.Right;
|
||||
mMinIntrinsicWidth = containerPadding.Left + containerPadding.Right;
|
||||
|
||||
Float maxMinContent = 0.f;
|
||||
for ( auto& item : items ) {
|
||||
Float minContent = item.targetMainSize;
|
||||
if ( item.widget->isType( UI_TYPE_HTML_WIDGET ) &&
|
||||
item.widget->asType<UIHTMLWidget>()->getLayouter() ) {
|
||||
minContent = eemax( minContent, item.widget->asType<UIHTMLWidget>()
|
||||
->getLayouter()
|
||||
->getMinIntrinsicWidth() );
|
||||
} else {
|
||||
minContent = eemax( minContent, item.widget->getPixelsSize().getWidth() );
|
||||
}
|
||||
if ( minContent < 0.f )
|
||||
minContent = 0.f;
|
||||
maxMinContent = eemax( maxMinContent,
|
||||
minContent + item.marginCrossStart + item.marginCrossEnd );
|
||||
}
|
||||
mMinIntrinsicWidth = maxMinContent + containerPadding.Left + containerPadding.Right;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1423,3 +1423,90 @@ UTEST( FlexProperties, enumConversions ) {
|
||||
EXPECT_EQ( CSSAlignSelfHelper::fromString( "stretch" ), CSSAlignSelf::Stretch );
|
||||
EXPECT_EQ( CSSAlignSelfHelper::fromString( "center" ), CSSAlignSelf::Center );
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Phase 14: Flex Algorithm Bug Fixes (G1, G2, G4, G5)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
UTEST( FlexContainer, iterativeFlexResolutionWithMinWidths ) {
|
||||
// G1: When a flex item hits its min-width, remaining free space is
|
||||
// redistributed to other items (iterative §9.7 algorithm).
|
||||
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Flex Test", WindowStyle::Default,
|
||||
WindowBackend::Default, 32, {}, 1, false,
|
||||
true ),
|
||||
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
||||
init_flex_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
|
||||
UIHTMLWidget* flex = UIHTMLWidget::New();
|
||||
flex->setParent( sceneNode->getRoot() );
|
||||
flex->setDisplay( CSSDisplay::Flex );
|
||||
flex->setPixelsSize( 500, 100 );
|
||||
flex->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
||||
|
||||
// Three items with flex: 1 1 0% (equal flex-grow, zero base size)
|
||||
UIHTMLWidget* a = UIHTMLWidget::New();
|
||||
a->setParent( flex );
|
||||
a->setPixelsSize( 10, 50 );
|
||||
a->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
||||
a->setStyleSheetProperty( StyleSheetProperty( "flex", "1 1 0%" ) );
|
||||
|
||||
UIHTMLWidget* b = UIHTMLWidget::New();
|
||||
b->setParent( flex );
|
||||
b->setPixelsSize( 10, 50 );
|
||||
b->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
||||
b->setStyleSheetProperty( StyleSheetProperty( "flex", "1 1 0%" ) );
|
||||
|
||||
// c has min-width:200px, so it should stay at 200 while a and b split
|
||||
// the remaining 300px → 150px each
|
||||
UIHTMLWidget* c = UIHTMLWidget::New();
|
||||
c->setParent( flex );
|
||||
c->setPixelsSize( 10, 50 );
|
||||
c->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
||||
c->setStyleSheetProperty( StyleSheetProperty( "flex", "1 1 0%" ) );
|
||||
c->setStyleSheetProperty( StyleSheetProperty( "min-width", "200px" ) );
|
||||
|
||||
sceneNode->updateDirtyLayouts();
|
||||
|
||||
EXPECT_NEAR( a->getPixelsSize().getWidth(), 150.f, 5.f );
|
||||
EXPECT_NEAR( b->getPixelsSize().getWidth(), 150.f, 5.f );
|
||||
EXPECT_NEAR( c->getPixelsSize().getWidth(), 200.f, 5.f );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( FlexContainer, crossAxisAutoMargins ) {
|
||||
// G4: margin: auto on the cross axis should center/position the item
|
||||
// within the line before align-self applies (§8.1).
|
||||
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Flex Test", WindowStyle::Default,
|
||||
WindowBackend::Default, 32, {}, 1, false,
|
||||
true ),
|
||||
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
||||
init_flex_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
|
||||
// Row-direction flex container with fixed height
|
||||
UIHTMLWidget* flex = UIHTMLWidget::New();
|
||||
flex->setParent( sceneNode->getRoot() );
|
||||
flex->setDisplay( CSSDisplay::Flex );
|
||||
flex->setPixelsSize( 400, 200 );
|
||||
flex->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
||||
flex->setStyleSheetProperty( StyleSheetProperty( "align-items", "flex-start" ) );
|
||||
|
||||
// Single item with margin-top: auto and margin-bottom: auto
|
||||
UIHTMLWidget* child = UIHTMLWidget::New();
|
||||
child->setParent( flex );
|
||||
child->setPixelsSize( 100, 50 );
|
||||
child->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
||||
child->setStyleSheetProperty( StyleSheetProperty( "margin", "auto" ) );
|
||||
|
||||
sceneNode->updateDirtyLayouts();
|
||||
|
||||
// With both margin-top and margin-bottom as auto, the item should be
|
||||
// vertically centered. Container is 200px, item is 50px.
|
||||
// Item Y should be (200 - 50) / 2 = 75
|
||||
// (Flex container has no padding in this setup)
|
||||
EXPECT_NEAR( child->getPixelsPosition().y, 75.f, 5.f );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user