diff --git a/.agent/plans/flexbox_support_plan.md b/.agent/plans/flexbox_support_plan.md new file mode 100644 index 000000000..203863487 --- /dev/null +++ b/.agent/plans/flexbox_support_plan.md @@ -0,0 +1,1274 @@ +# CSS Flexbox Support Plan + +## Goal + +Implement CSS Flexible Box Layout Module Level 1 (https://www.w3.org/TR/css-flexbox-1/) in eepp's +HTML compatibility layer, as the foundation for modern web layouts. This is a spec-compliant +implementation mapped onto eepp's `UILayouter` architecture. + +## Scope + +Complete `display: flex` and `display: inline-flex` support including: + +- Flex container properties: `flex-direction`, `flex-wrap`, `flex-flow` (shorthand), `justify-content`, + `align-items`, `align-content`, `row-gap` / `column-gap` / `gap` +- Flex item properties: `order`, `flex-grow`, `flex-shrink`, `flex-basis`, `flex` (shorthand), `align-self` +- The CSS flex layout algorithm: generating flex lines, resolving flexible lengths, main-axis + distribution, cross-axis alignment, multi-line cross-axis content distribution +- Flex items with auto margins +- Absolute/fixed-positioned flex children (out-of-flow, skipped by flex layout, placed via containing block) +- Percentage-based flex-basis resolution against the flex container's inner main size +- `min-width: auto` / `min-height: auto` default behavior on flex items (content-based minimum) + +## Non-Scope + +- `display: flex` on non-HTML eepp widgets (only `UIHTMLWidget` children) +- Nested flex containers (works naturally once the layouter is correct, but no special treatment) +- `order` with CSS Grid integration (order is ignored for non-flex/grid contexts) +- `align-content: space-evenly` unless time permits after core algorithm completion +- `margin: auto` on the cross axis for single-line containers (treat as missing if not easily + compatible with current margin model) +- Column-reverse reordering of stacking context / z-order (focus on geometric layout first) + +## Current State + +**What exists now (as of the implementation checkpoint):** + +- `CSSDisplay::Flex` and `CSSDisplay::InlineFlex` exist in the enum. +- `CSSDisplay::Flex` is routed to `BlockLayouter` in `UILayouterManager::create()` + (`src/eepp/ui/uilayoutermanager.cpp`). +- `CSSDisplay::InlineFlex` is routed to `FlexLayouter`. +- Flex-related `PropertyId` values exist (`FlexDirection`, `FlexWrap`, `FlexFlow`, + `JustifyContent`, `AlignItems`, `AlignContent`, `AlignSelf`, `FlexGrow`, `FlexShrink`, + `FlexBasis`, `Flex`, `Order`, `RowGap`, `ColumnGap`, `Gap`). +- Flex CSS enums exist (`CSSFlexDirection`, `CSSFlexWrap`, `CSSJustifyContent`, + `CSSAlignItems`, `CSSAlignContent`, `CSSAlignSelf`) in `csslayouttypes.hpp`. +- `FlexLayouter` class exists with a complete flex layout algorithm (Phases 1–11). +- `UIHTMLWidget::getPropertiesImplemented()` includes flex properties. +- `UIHTMLWidget::setDisplay()` handles `Flex` and `InlineFlex` with correct size policies. +- `friend class FlexLayouter` added to `UINode` for `setInternalPixelsWidth/Height` access. +- **Blocker:** `CSSDisplay::Flex` cannot be routed to `FlexLayouter` because real-world + HTML uses `display: flex` on elements containing RichText (text nodes, inline elements), + and FlexLayouter's single-pass layout cannot resolve the recursive size dependency + between flex items and their text content. See "RichText Integration" below. + +## Reference Specifications + +- CSS Flexbox Module Level 1: https://www.w3.org/TR/css-flexbox-1/ + - Section 5: Flex Items + - Section 7: Ordering and Orientation + - Section 8: Flex Item Margins and Paddings + - Section 9: Flex Layout Algorithm (THE critical section) + - Section 10: Fragmenting Flex Layout (printed/column media — skip) + +Initial value table (Section 2): + +| Property | Initial Value | +|---|---| +| `flex-direction` | `row` | +| `flex-wrap` | `nowrap` | +| `justify-content` | `flex-start` | +| `align-items` | `stretch` | +| `align-content` | `stretch` | +| `align-self` | `auto` | +| `flex-grow` | `0` | +| `flex-shrink` | `1` | +| `flex-basis` | `auto` | +| `order` | `0` | +| `row-gap` / `column-gap` | `normal` | + +## Impact Assessment + +### Files to Create + +| File | Purpose | +|---|---| +| `include/eepp/ui/flexlayouter.hpp` | `FlexLayouter` class declaration | +| `src/eepp/ui/flexlayouter.cpp` | Full flex layout algorithm implementation | + + +### Files to Modify + +| File | Change Summary | +|---|---| +| `include/eepp/ui/csslayouttypes.hpp` | Add `CSSDisplay::InlineFlex` enum value | +| `src/eepp/graphics/csslayouttypes.cpp` | Add `"inline-flex"` to `CSSDisplayHelper` | +| `include/eepp/ui/css/propertydefinition.hpp` | Add flex-related `PropertyId` values | +| `src/eepp/ui/css/stylesheetspecification.cpp` | Register all flex CSS properties | +| `src/eepp/ui/uilayoutermanager.cpp` | Route `Flex` and `InlineFlex` to `FlexLayouter`; add include | +| `include/eepp/ui/uihtmlwidget.hpp` | Add flex-related state fields (or keep in layouter) | +| `src/eepp/ui/uihtmlwidget.cpp` | Handle flex properties in `applyProperty()` / `getPropertyString()` / `getPropertiesImplemented()` | +| `src/eepp/ui/uirichtext.cpp` | Skip flex-item widget children in `rebuildRichText()` (flex items are block-level, not inline) | +| `include/eepp/ui/uinode.hpp` | Add `friend class FlexLayouter` for `setInternalPixelsWidth/Height` access | +| `make/linux/eepp.make` | Add new source files `src/eepp/ui/flexlayouter.cpp` (via premake, then regenerate) | + +### Design Decision: Where Does Flex State Live? + +**Recommendation: Keep flex state on `FlexLayouter`**, not on `UIHTMLWidget`. + +Reasons: +- `UIHTMLWidget` already stores `CSSDisplay`, `CSSFloat`, `CSSPosition`, etc. Adding ~10 more + enums would bloat every HTML widget, most of which are never flex containers or flex items. +- Flex container properties (`flex-direction`, `flex-wrap`, etc.) only matter when a widget has + `display: flex` or `display: inline-flex`. Those properties can be read from the CSS style + during layout. +- Flex item properties (`order`, `flex-grow`, etc.) only matter when the parent is a flex + container. The parent `FlexLayouter` can read those from each child's style when laying out. + +If profiling shows repeated CSS-style lookups in the hot path are expensive, we can later cache +parsed flex values in helper structs. Start with direct style reads for simplicity. + +## Implementation Plan + +### Phase 0: CSS Infrastructure (Enums, PropertyIds, Parsing) + +**Purpose:** Wire the CSS property system so flex properties parse and store correctly. No layout +behavior changes yet. + +#### Step 0a: Add `CSSDisplay::InlineFlex` + +**Files:** `csslayouttypes.hpp`, `csslayouttypes.cpp` + +```cpp +// csslayouttypes.hpp — add to CSSDisplay enum: +InlineFlex, // after Flex + +// csslayouttypes.cpp — add to toString: +case CSSDisplay::InlineFlex: return "inline-flex"; + +// csslayouttypes.cpp — add to fromString: +else if ( val == "inline-flex" ) display = CSSDisplay::InlineFlex; +``` + +#### Step 0b: Define Flex Enums + +**Files:** `include/eepp/ui/csslayouttypes.hpp`, `src/eepp/graphics/csslayouttypes.cpp` + +Add enums directly to `csslayouttypes.hpp` following the existing pattern: + +```cpp +enum class CSSFlexDirection { Row, RowReverse, Column, ColumnReverse }; +enum class CSSFlexWrap { NoWrap, Wrap, WrapReverse }; +enum class CSSJustifyContent { FlexStart, FlexEnd, Center, SpaceBetween, SpaceAround, SpaceEvenly }; +enum class CSSAlignItems { FlexStart, FlexEnd, Center, Baseline, Stretch }; +enum class CSSAlignContent { FlexStart, FlexEnd, Center, SpaceBetween, SpaceAround, SpaceEvenly, Stretch }; +enum class CSSAlignSelf { Auto, FlexStart, FlexEnd, Center, Baseline, Stretch }; +``` + +Add `toString`/`fromString` helper structs (`CSSFlexDirectionHelper`, `CSSFlexWrapHelper`, etc.) +in `csslayouttypes.cpp` following the established pattern (e.g., `CSSDisplayHelper`). + +Note: `CSSAlignSelf` includes `Auto` (the initial value) which resolves to the parent's +`align-items`. `CSSAlignItems` does NOT include `Auto` since `align-items: auto` is invalid. + +#### Step 0c: Add Flex PropertyIds + +**File:** `propertydefinition.hpp` — add to `PropertyId` enum: + +```cpp +FlexDirection = String::hash( "flex-direction" ), +FlexWrap = String::hash( "flex-wrap" ), +FlexFlow = String::hash( "flex-flow" ), +JustifyContent = String::hash( "justify-content" ), +AlignItems = String::hash( "align-items" ), +AlignContent = String::hash( "align-content" ), +AlignSelf = String::hash( "align-self" ), +FlexGrow = String::hash( "flex-grow" ), +FlexShrink = String::hash( "flex-shrink" ), +FlexBasis = String::hash( "flex-basis" ), +Flex = String::hash( "flex" ), +Order = String::hash( "order" ), +RowGap = String::hash( "row-gap" ), +ColumnGap = String::hash( "column-gap" ), +Gap = String::hash( "gap" ), +``` + +#### Step 0d: Register Flex Properties in StyleSheetSpecification + +**File:** `stylesheetspecification.cpp` — add in `registerDefaultProperties()`: + +```cpp +registerProperty( "flex-direction", "row" ); +registerProperty( "flex-wrap", "nowrap" ); +registerProperty( "flex-flow", "row nowrap" ); +registerProperty( "justify-content", "flex-start" ); +registerProperty( "align-items", "stretch" ); +registerProperty( "align-content", "stretch" ); +registerProperty( "align-self", "auto" ); +registerProperty( "flex-grow", "0", CSS::PropertyType::NumberFloat ); +registerProperty( "flex-shrink", "1", CSS::PropertyType::NumberFloat ); +registerProperty( "flex-basis", "auto" ); +registerProperty( "flex", "0 1 auto" ); +registerProperty( "order", "0", CSS::PropertyType::NumberInt ); +registerProperty( "row-gap", "normal" ); +registerProperty( "column-gap", "normal" ); +registerProperty( "gap", "normal normal" ); +``` + +Also register shorthand expansion in `registerShorthandProperties()`: + +```cpp +registerShorthand( "flex-flow", { "flex-direction", "flex-wrap" } ); +registerShorthand( "gap", { "row-gap", "column-gap" } ); +``` + +The `flex` shorthand requires custom expansion logic (see Step 0f below). + +#### Step 0e: Add Flex Property Handling in UIHTMLWidget + +**File:** `uihtmlwidget.cpp` + +- Add `PropertyId::FlexDirection`, etc. to `getPropertiesImplemented()` return list. +- Add cases in `applyProperty()` that parse the string/value and return `true` (acknowledged). + The layouter reads from the style directly; no need to cache on the widget. +- Add cases in `getPropertyString()` that return the serialized string from the widget's style. + +**Note on performance:** FlexLayouter reads 6–8 style properties per item during layout. With +<100 flex items this is negligible. If profiling shows overhead, cache parsed values in +`FlexLayouter::FlexItem` during `collectFlexItems()` (already done in the implementation). + +#### Step 0f: Expand `flex` Shorthand + +**File:** `src/eepp/ui/css/stylesheetproperty.cpp` — in `expandShorthand()`: + +The `flex` shorthand has complex expansion rules per CSS §7.1: + +| Input | Expanded to | +|---|---| +| `flex: auto` | `flex-grow: 1; flex-shrink: 1; flex-basis: auto` | +| `flex: none` | `flex-grow: 0; flex-shrink: 0; flex-basis: auto` | +| `flex: ` | `flex-grow: ; flex-shrink: 1; flex-basis: 0%` | +| `flex: ` | `flex-grow: ; flex-shrink: ; flex-basis: 0%` | +| `flex: ` | `flex-grow: ; flex-shrink: 1; flex-basis: ` | +| `flex: ` | `flex-grow: ; flex-shrink: ; flex-basis: ` | + +**Critical distinction:** `flex: 1` expands to `flex: 1 1 0%`, NOT `flex: 1 1 auto`. +`flex-basis: 0%` is different from `flex-basis: 0` — `0%` resolves against the container, +while `0` is an absolute length. In practice, treat `0%` as `0px` for flex-basis resolution. + +#### Step 0g: Route Display Values to FlexLayouter + +**File:** `uilayoutermanager.cpp` + +**Step 0g.1 (safe — do first):** Route `InlineFlex` only: +```cpp +case CSSDisplay::Block: +case CSSDisplay::TableCell: +case CSSDisplay::InlineBlock: +case CSSDisplay::ListItem: +case CSSDisplay::Flex: + return eeNew( BlockLayouter, ( container ) ); +case CSSDisplay::InlineFlex: + return eeNew( FlexLayouter, ( container ) ); +``` + +**Step 0g.2 (after RichText integration is complete):** Also route `Flex`: +```cpp +case CSSDisplay::Block: +case CSSDisplay::TableCell: +case CSSDisplay::InlineBlock: +case CSSDisplay::ListItem: + return eeNew( BlockLayouter, ( container ) ); +case CSSDisplay::Flex: +case CSSDisplay::InlineFlex: + return eeNew( FlexLayouter, ( container ) ); +``` + +**Why defer `Flex` routing:** Real-world HTML test pages (`body_height_miscalculation.html`, +`lobsters_simple.html`) use `display: flex` on text-containing elements. Routing `Flex` to +`FlexLayouter` before RichText integration is fixed breaks these tests. `InlineFlex` is safe +because no existing content uses it. + +### Phase 1: FlexLayouter Skeleton And Item Collection + +**Purpose:** Create the `FlexLayouter` class with the minimum scaffolding to avoid crashes when a +flex container is created. Item collection (gathering in-flow children into a flex item list) with +absolute position filtering. + +#### Step 1a: Create FlexLayouter Header + +**File:** `include/eepp/ui/flexlayouter.hpp` + +```cpp +class EE_API FlexLayouter : public UILayouter { + public: + FlexLayouter( UIWidget* container ); + void updateLayout() override; + void computeIntrinsicWidths() override; + Float getMinIntrinsicWidth() override; + Float getMaxIntrinsicWidth() override; + + private: + // Flex item representation during layout + struct FlexItem { + UIHTMLWidget* widget; + Float flexBaseSize; // flex base size (hypothetical main size) + Float hypotheticalCrossSize; + Float targetMainSize; // resolved main size after flexing + Float minMainSize; // min-content contribution in main axis + Float maxMainSize; // max-content contribution in main axis + Float crossSize; // final cross size + CSSAlignSelf alignSelf; + CSSFlexDirection itemDirection; // Item's own internal writing mode (placeholder) + Float flexGrow; + Float flexShrink; + Float flexBasisValue; // parsed flex-basis (px) or NaN if auto + int order; + bool frozen; // frozen during flex resolution + Vector2f marginMainStart; + Vector2f marginMainEnd; + Vector2f marginCrossStart; + Vector2f marginCrossEnd; + bool hasAutoMarginMainStart; + bool hasAutoMarginMainEnd; + bool hasAutoMarginCrossStart; + bool hasAutoMarginCrossEnd; + }; + + struct FlexLine { + std::vector items; + Float mainSize; // total flex base size of items on this line + Float crossSize; // line cross size (max item cross size) + Float remainingFreeSpace; // free space after item collection + }; + + // Axis helpers + bool isHorizontalMainAxis() const; + Float mainSize(const Sizef& s) const; + Float crossSize(const Sizef& s) const; + void setMainSize(Sizef& s, Float v) const; + void setCrossSize(Sizef& s, Float v) const; + Float mainPos(const Vector2f& p) const; + Float crossPos(const Vector2f& p) const; + void setMainPos(Vector2f& p, Float v) const; + void setCrossPos(Vector2f& p, Float v) const; + + // Container state (read from style during layout) + CSSFlexDirection mFlexDirection{CSSFlexDirection::Row}; + CSSFlexWrap mFlexWrap{CSSFlexWrap::NoWrap}; + CSSJustifyContent mJustifyContent{CSSJustifyContent::FlexStart}; + CSSAlignItems mAlignItems{CSSAlignItems::Stretch}; + CSSAlignContent mAlignContent{CSSAlignContent::Stretch}; + Float mRowGap{0}; + Float mColumnGap{0}; + + std::vector mFlexItems; + std::vector mFlexLines; +}; +``` + +#### Step 1b: Implement Item Collection + +**File:** `src/eepp/ui/flexlayouter.cpp` + +`FlexLayouter::updateLayout()` outline: + +1. Retrieve CSS properties from container's style (or from `UIHTMLWidget` if cached). +2. Collect in-flow children: iterate `mContainer`'s child list, skip `display: none`, + `position: absolute/fixed`, text nodes in element children (flex items must be elements). +3. Blockify inline children per CSS Flexbox §4: if a child is `display: inline`, + `display: inline-block`, `display: inline-table` — compute as block-level. The child's own + layout is unaffected; this is about how it participates in the flex container. +4. For each flex item, read `order`, `flex-grow`, `flex-shrink`, `flex-basis`, `align-self` + from the child's style. Sort `mFlexItems` by increasing `order`. +5. If `flex-wrap: nowrap`, collect all items into a single flex line. + If `flex-wrap: wrap` or `wrap-reverse`, distribute items across lines per §9 Flex Layout + Algorithm. + +**Text node handling:** In HTML, bare text between flex items generates anonymous flex items +(anonymous block boxes wrapping the text). For a first implementation, text nodes that are +direct children of a flex container can be treated as a `0px` flex item or skipped with a warning. +Full anonymous flex item wrapping adds complexity and is rare in practice (users use `` +or `
` as flex children). Document the limitation. + +**Gate:** Create a minimal test with `display: flex` that has children. The children should be +positioned somewhere valid (not stepping on each other or the container origin), even if +alignment is not yet correct. No crash. + +### Phase 2: Flex Layout Algorithm — Main Axis + +**Purpose:** Implement §9.2–9.7 of the CSS Flexbox spec: determining flex base size, resolving +flexible lengths, distributing free space on the main axis. + +#### Step 2a: Determine Flex Base Size And Hypothetical Main Size + +For each flex item, per §9.2: + +1. **If `flex-basis` is a definite length or percentage** (not `auto` and not `content`), + the flex base size is that value. Percentages resolve against the flex container's + inner main size if definite; otherwise they behave as `auto`. +2. **If `flex-basis` is `auto`**, determine the flex base size from the item's main size + property (`width` for row direction, `height` for column direction). +3. **If the main size property is definite**, use that as the flex base size. + Otherwise, use the item's **content-based size** — for eepp, this is the widget's + current pixel size in the main axis (`getPixelsSize().getWidth()` for row, + `getPixelsSize().getHeight()` for column), which reflects the laid-out content size. +4. The **hypothetical main size** is the flex base size clamped by any definite `min`/`max` + main size constraints (`min-width`/`max-width` for row, `min-height`/`max-height` for column). + +**Note:** The flex base size depends on `flex-basis` and the main size property, NOT on the +item's cross size. Cross size only affects the flex base size for aspect-ratio calculations, +which eepp does not support (no aspect-ratio CSS property). The item's cross size is determined +in Step 3a (cross-axis sizing) and is independent of the flex base size computation. + +#### Step 2b: Resolve Flexible Lengths (§9.7) + +1. Determine the used main size of the flex container (the "available main space"): + - If the container has a definite main size, use that. + - If max-content constrained (e.g., `width: max-content`), use the total max-content + contributions of items, clamped. + - Otherwise, use the containing block's available space. +2. Compute the initial free space: container inner main size minus sum of outer hypothetical + main sizes of items on the line, minus gaps between items. +3. If free space is positive and `flex-grow` factors sum to > 0: distribute free space. + If free space is negative and `flex-shrink` factors sum to > 0: distribute negative space. +4. Clamp each item's target main size by its min/max main size constraints. + - **`min-width: auto` on flex items resolves to content size** per §4.5. +5. Iterate: re-distribute freed/added space after clamping until all items are at valid sizes + or no further change. + +#### Step 2c: Main-Axis Positioning (Justify) + +After final main sizes are resolved, distribute remaining free space (if any after resolution) +according to `justify-content`: + +- `flex-start`: items at main-start edge, no extra space between them. +- `flex-end`: items at main-end edge. +- `center`: items centered. +- `space-between`: first item at main-start, last at main-end, equal space between. +- `space-around`: equal space on both sides of each item (half-size spaces at edges). +- `space-evenly`: equal space between all items and edges. + +For single-item lines, `space-between` behaves as `flex-start`. + +Auto margins in the main axis absorb free space before `justify-content` applies +per §8.1: if an item has `margin-left: auto` (in row direction), it absorbs all +free space to its left. If two items have opposite auto-margins, they split the free space. + +#### Step 2d: Intrinsic Sizing + +`computeIntrinsicWidths()`: The max-content main size of a flex container is the sum of the +max-content contributions of items on the largest line, plus gaps. The min-content main size +is the largest min-content contribution among items on each line (since items can flex-shrink +below their base size), but the spec says min-content size of a flex container is computed by +setting `flex-basis: min-content` on all items and running a simplified pass. + +**Gate:** Flex items are sized on the main axis correctly. Fixed-width items keep their width. +Items with `flex-grow` expand to fill available space. Items with `flex-shrink` shrink below +their natural size. The Flex Container Research tests from the CSS WG test suite or equivalent +hand-crafted tests should pass. + +### Phase 3: Flex Layout Algorithm — Cross Axis + +**Purpose:** Implement §9.4 (Cross Size Determination) and §9.5 (Main-Axis Alignment) / §9.6 +(Cross-Axis Alignment). + +#### Step 3a: Determine Hypothetical Cross Size + +For each flex item on each line, if the cross size property is `auto`: +- Use `align-self: stretch` and the item has `auto` cross size → stretch the item to the + line cross size. +- Otherwise, use the item's content-based cross size (intrinsic height for row direction, + intrinsic width for column direction). + +#### Step 3b: Determine Line Cross Size + +For each flex line: +- Single-line container: line cross size = container inner cross size. +- Multi-line container: line cross size = maximum outer hypothetical cross size among items + on the line, clamped by the container's min/max cross size constraints. + +#### Step 3c: Handle `align-content` (Multi-line) + +For multi-line containers, distribute remaining cross-axis free space among lines +according to `align-content`. Same values as `justify-content`, plus `stretch` which +stretches all lines to fill the container cross size. + +#### Step 3d: Handle `align-items` / `align-self` (Within each line) + +For each item on a line: +- Resolve `align-self`: if `auto`, use the container's `align-items`. +- Align the item within the line cross size: + - `flex-start`: item at cross-start edge. + - `flex-end`: item at cross-end edge. + - `center`: item centered. + - `baseline`: items baseline-aligned (cross-start position such that the first line box + baseline of all items on the line shares the same cross-start offset). + - `stretch`: item stretched to line cross size if its cross size is `auto`. + +#### Step 3e: Cross-Axis Auto Margins + +Auto margins on the cross axis absorb free space before `align-items`/`align-self` +apply. An item with `margin-top: auto` and `margin-bottom: auto` in row direction +gets vertically centered. + +**Gate:** Flex items are fully sized and positioned in both axes. A horizontal flex container +with `align-items: center` vertically centers its children. `align-items: stretch` makes all +children the same height as the tallest one. Multi-line wrap containers distribute lines +correctly. + +### Phase 4: Flex Direction And Wrapping Variants + +**Purpose:** Implement all `flex-direction` and `flex-wrap` combinations with correct axis +mapping. + +#### Step 4a: Direction Modes + +| `flex-direction` | Main Axis | Cross Axis | +|---|---|---| +| `row` | horizontal, left to right | vertical, top to bottom | +| `row-reverse` | horizontal, right to left | vertical, top to bottom | +| `column` | vertical, top to bottom | horizontal, left to right | +| `column-reverse` | vertical, bottom to top | horizontal, left to right | + +The axis helpers (`isHorizontalMainAxis()`, etc.) in the header must handle all four cases. +Main-start/main-end and cross-start/cross-end positions are defined by the direction and +writing mode. For Phase 4, assume LTR writing mode only. + +#### Step 4b: `wrap-reverse` + +In `wrap-reverse`, flex lines are stacked in the cross-end to cross-start direction. +The first line is at the cross-end edge, subsequent lines stack toward the cross-start edge. + +#### Step 4c: `column`/`column-reverse` Intrinsic Sizing + +In column direction, the intrinsic main size depends on the number of items and their +cross sizes. The container's intrinsic width is the sum of the largest item's max-content +width on the first line. + +**Gate:** All four direction values and three wrap values produce correct layout. +A full 3×4 = 12-combination test matrix should pass. + +### Phase 5: Gaps (row-gap, column-gap, gap shorthand) + +**Purpose:** Implement gap properties per CSS Box Alignment §10. + +#### Step 5a: Parse gap properties + +- `row-gap: ` — spacing between flex lines (cross-axis gap). +- `column-gap: ` — spacing between flex items on the same line (main-axis gap). +- `gap: ` shorthand. +- `normal` initial value means `0px` in flexbox (unlike grid where `normal` may differ). + +#### Step 5b: Apply gaps in layout + +- Main-axis: add `column-gap` between items within each flex line. Total free space is + reduced by `(item_count - 1) * column_gap`. Gaps appear between items, not at the edges. +- Cross-axis: add `row-gap` between flex lines. Total cross free space is reduced by + `(line_count - 1) * row_gap`. + +**Gate:** Gaps visibly separate items. `gap: 10px` adds 10px between all items. + +### Phase 6: Flex Item Layout (Setting Child Positions/Sizes) + +**Purpose:** After computing all flex item positions and sizes via the flex algorithm, +write those back to the actual widget children. + +#### Step 6a: Position And Size Children + +After the flex algorithm resolves all `FlexItem::targetMainSize` and `FlexItem::crossSize`, +and computes the main-axis and cross-axis positions: + +1. Convert the axis-relative results back to screen coordinates (x, y, width, height). +2. Set each child widget's `setPixelsPosition()` and `setPixelsSize()`. +3. Handle children that still need their own internal layout (call `updateLayout()` on + child widgets if they have their own layouter — e.g., a flex item that is itself a + flex container). + +#### Step 6b: Handle MatchParent Children + +Flex items with `width: 100%` in a row-direction container should resolve that percentage +against the container's defined size, not get stretched to 100% of the container after +flex base size computation. This is part of the definite/indefinite size resolution in §9.2. + +If the flex container has a definite main size, percentage main-size children resolve +against that. If indefinite (e.g., `width: auto`), treat as `auto`. + +#### Step 6c: Handle Shrink-Wrap Container + +A flex container with `width: auto` (or `height: auto` for column direction) should use +max-content sizing: its outer width is the sum of max-content contributions of items +on the largest line. + +However, in the eepp widget system, if the flex container is `MatchParent`, it fills +its parent. If `WrapContent`, it shrinks to fit content. The container's size policy +must be set correctly in `setDisplay()` like `InlineBlock` and `Block` do. + +**Flex containers are block-level by default** (`display: flex`) and inline-level +(`display: inline-flex`). A block-level flex container fills its containing block width +(match-parent); an inline-flex shrink-wraps. + +#### Step 6d: Relayout if re-entrancy + +After sizing children, if a child's size changed from what was assumed during the flex +algorithm (e.g., because the child's layouter made a different choice), a second pass +may be needed. `FlexLayouter` should: +- Track whether any child's actual laid-out size differs from the target computed by the + flex algorithm. +- If so and within a reasonable iteration limit (e.g., 2 passes), re-run the flex algorithm + with the updated child sizes. + +**Gate:** Children of a flex container appear at correct positions and sizes. +Nested flex containers work. The full layout round-trip from DOM to screen is correct. + +### Phase 7: `flex` Shorthand Parsing + +> **Note:** This is implemented as part of Phase 0 (Step 0f). This section remains as a +> reference for the shorthand behavior. The suggested implementation order (above) merges +> this into Phase 0. + +**Purpose:** Parse the `flex` shorthand property. + +The `flex` shorthand sets `flex-grow`, `flex-shrink`, and `flex-basis` together. + +CSS syntax (§7.1): +- `flex: auto` → `flex: 1 1 auto` +- `flex: none` → `flex: 0 0 auto` +- `flex: ` → `flex: 1 0%` (single unitless number) +- `flex: ` → `flex: 0%` +- `flex: ` → `flex: 1 ` (when basis is a width) +- `flex: ` → all three + +Initial values: `flex-grow: 0, flex-shrink: 1, flex-basis: auto`. + +Implementation: Add a helper function that parses the shorthand string and sets +the three individual properties on the style. Since `StyleSheetProperty` stores +strings, this means generating serialized values for the individual properties. + +**Gate:** `flex: 1` on a child makes it grow. `flex: none` makes it inflexible. +`flex: 0 0 200px` sets a fixed 200px flex item. + +### Phase 8: `min-width: auto` On Flex Items + +**Purpose:** Implement the CSS Flexbox §4.5 rule: the initial `min-width` (and `min-height` +in column direction) of a flex item is `auto`, which resolves to `min-content` (content-based +minimum), NOT `0`. + +Without this, flex items with `flex-shrink` can collapse to 0px even when they contain +text, images, or side-by-side floats. This is the #1 source of "content disappears in +flexbox" bugs in naive implementations. + +Implementation: +1. When a flex item has no explicit `min-width`/`min-height` (the `auto` initial value), + compute its content-based minimum size. +2. For a flex item with text content (RichText), the min-content width is the width of + the longest word. The min-content height is 0 (inline elements don't have intrinsic height). +3. The item's final target main size must be at least this minimum, even if `flex-shrink` + would otherwise push it below. + +In eepp, the `UILayouter::getMinIntrinsicWidth()` / `getMaxIntrinsicWidth()` methods +on child widgets provide exactly this. Call `child->getLayouter()->getMaxIntrinsicWidth()` +for the main-axis max-content contribution and `getMinIntrinsicWidth()` for the min-content. + +**Gate:** A flex item containing text with `flex-shrink: 1` on a narrow container does not +collapse below a single word's width. A `min-width: 50px` on the item overrides the +content-based minimum. + +### Phase 9: Inline-Flex Behavior + +**Purpose:** Make `display: inline-flex` create an inline-level flex container. + +This mostly requires: +- Setting the container's size policy to `WrapContent` (shrink-to-fit). +- Ensuring the inline-flex container participates in the parent's inline formatting context + as an atomic inline-level box (like `inline-block`). +- The container's internal flex layout is identical to `display: flex`. + +Most of this is handled in `UIHTMLWidget::setDisplay()` which already sets size policies +based on display mode. `InlineFlex` should behave like `InlineBlock` for the outer display +type while using `FlexLayouter` for the inner formatting context. + +The `CSSDisplay::InlineFlex` case in `setDisplay()` should: +- Set width policy to `WrapContent`, height policy to `WrapContent`. +- The layouter is `FlexLayouter`. + +**Gate:** `display: inline-flex` flows inline in a block-level parent. The container +shrink-wraps its content. + +### Phase 10: Out-Of-Flow Flex Children + +**Purpose:** Handle `position: absolute` and `position: fixed` children inside a flex container. + +Per CSS Flexbox §4.1: +> An absolutely-positioned child of a flex container does not participate in flex layout. + +The flex container's static position for an absolutely-positioned child is where the child +would have been placed had it been the sole flex item in the container (at the main-start/ +cross-start position with no stretching applied). + +Implementation: +1. Skip out-of-flow children during `FlexLayouter::updateLayout()` item collection. +2. After regular flex items are laid out, position out-of-flow children via + `positionOutOfFlowChildren()` (already handled by `UIHTMLWidget::updateLayout()`). +3. The static position for out-of-flow children within a flex container: the spec says + "the static position of an absolutely-positioned child of a flex container is determined + such that the child is positioned as if it were the sole flex item in the flex container." + For now, position at the container's padding-box origin (content-start) since the + `updateOutOfFlowPosition()` method in `UIHTMLWidget` will compute the actual position + based on top/left/right/bottom CSS properties. + +**Gate:** Absolutely-positioned children inside a flex container don't crash and are +positioned by their CSS offsets relative to the flex container's content area. + +### Phase 11: Edge Cases And Polish + +#### 11a: Empty Flex Container +A flex container with no in-flow children should collapse to its cross size (usually 0) +unless the container has an explicit `height` or `min-height`. + +#### 11b: Flex Items With Definite Cross Size But Auto Main Size +When the main size is indefinite and the cross size is definite, the flex base size in +a row-direction container is computed from the aspect ratio or intrinsic sizing. +Since eepp widgets don't typically have aspect ratios, fall back to intrinsic sizing +(layout the child with the given cross size, measure the resulting main size). + +#### 11c: Baseline Alignment +`align-items: baseline` / `align-self: baseline`: the baseline of a flex item is: +- If the item is a flex container, the baseline of its first/last line box (per + `flex-direction`). +- If the item has text content, the ascent of its first line box. +- Otherwise, the bottom edge of the item's margin box. + +In eepp, use the existing baseline computation from `UIRichText` / `RichText`: +```cpp +margin.Top + contentOffset.Top + firstLine.y + firstLine.maxAscent +``` + +#### 11d: Replaced Elements +``, ``, ``, `