diff --git a/.agent/plans/grid_layout_support_plan.md b/.agent/plans/grid_layout_support_plan.md new file mode 100644 index 000000000..486467e8b --- /dev/null +++ b/.agent/plans/grid_layout_support_plan.md @@ -0,0 +1,862 @@ +# CSS Grid Layout Support Plan + +> Status: PROPOSED - CSS Grid Layout Module Level 1 implementation plan. +> Target spec: https://www.w3.org/TR/css-grid-1/ (W3C Candidate Recommendation Draft, 26 March 2025). + +## Goal + +Implement CSS Grid Layout Module Level 1 in eepp's HTML compatibility layer as a spec-compliant +`UILayouter` implementation, following the same successful pattern used by `FlexLayouter`: + +- wire CSS display/property parsing first, +- keep per-feature state compact and lazily allocated, +- collect grid items in order-modified document order, +- implement the spec algorithms directly, +- add phase-local unit tests and realistic `UIHTML` fixture tests, +- validate each phase with focused tests before the full suite. + +Grid is not a replacement for the existing `UIGridLayout` widget. `UIGridLayout` is an eepp UI +layout container with its own XML/properties. CSS Grid must live in the HTML/CSS layout path and be +routed through `UILayouterManager`, like `FlexLayouter`, `BlockLayouter`, and `TableLayouter`. + +## Reference Specifications + +- CSS Grid Layout Module Level 1: https://www.w3.org/TR/css-grid-1/ + - Section 5: Grid containers + - Section 6: Grid items + - Section 7: Defining the grid + - Section 8: Placing grid items + - Section 9: Absolute positioning + - Section 10: Alignment and spacing + - Section 11: Grid layout algorithm +- CSS Box Alignment Level 3: used by grid for `justify-*`, `align-*`, and `place-*`. +- CSS Display Level 3: automatic blockification for children of grid containers. +- CSS Sizing Level 3: intrinsic sizing terms used by the track sizing algorithm. + +Spec facts that should guide implementation: + +- `display: grid` establishes a block-level grid container; `display: inline-grid` establishes an + inline-level grid container. +- A grid container establishes an independent grid formatting context; floats do not intrude and + margins of its contents do not collapse. +- Children of a grid container are blockified, and text that is not only inter-element whitespace + becomes an anonymous grid item. +- `order` applies to grid items and affects auto-placement and paint order. +- Grid placement is visual only and must not change DOM order, speech order, or navigation order. +- Gutters are treated as fixed-size tracks for track sizing and appear only between tracks. +- Absolute-positioned boxes whose containing block is a grid container are part of the Level 1 + spec, but W3C marks "application of grid placement to absolutely-positioned boxes" as at-risk. + +## Scope + +Complete Level 1 support for: + +- `display: grid` and `display: inline-grid` +- explicit grid definition: + - `grid-template-rows` + - `grid-template-columns` + - `grid-template-areas` + - `grid-template` +- implicit grid definition: + - `grid-auto-rows` + - `grid-auto-columns` + - `grid-auto-flow: row | column | dense | row dense | column dense` + - `grid` +- line placement: + - `grid-row-start` + - `grid-row-end` + - `grid-column-start` + - `grid-column-end` + - `grid-row` + - `grid-column` + - `grid-area` +- track sizing: + - fixed lengths and percentages + - `auto` + - `min-content` + - `max-content` + - `minmax()` + - `fit-content()` + - `fr` + - `repeat(, ...)` + - `repeat(auto-fill, ...)` and `repeat(auto-fit, ...)` +- gutters: + - `row-gap` + - `column-gap` + - `gap` +- grid alignment: + - item alignment: `justify-items`, `justify-self`, `align-items`, `align-self` + - grid alignment: `justify-content`, `align-content` + - auto margins in both axes +- nested grids, grid inside flex, flex inside grid +- blockification and anonymous grid item text nodes +- grid item automatic minimum size +- `order`-modified auto-placement and paint order +- out-of-flow children skipped from normal grid sizing and positioned through the existing + containing-block path + +## Non-Scope For Initial Level 1 Completion + +- CSS Subgrid, which belongs to Grid Level 2. +- Masonry layout, which is not Grid Level 1. +- Writing modes beyond current eepp horizontal LTR assumptions. Keep the data model axis-aware so + later `direction` / `writing-mode` support does not require rewriting the layouter. +- Fragmentation and print/columns. +- `display: contents`, unless it is already independently supported by the HTML layer. +- Full CSSOM used-value serialization of track lists. Implement enough `getPropertyString()` to be + debuggable and testable, but do not block layout correctness on browser-perfect serialization. +- Grid placement for absolute-positioned boxes in phase 1. Treat this as a hardening phase because + it is at-risk in the spec and must integrate with `UIHTMLWidget::getContainingBlock()` carefully. + +## Current State + +What exists: + +- `FlexLayouter` provides the implementation template for a modern CSS layout algorithm: + compact item structs, lazy widget state, style reads during item collection, phase-specific helper + methods, intrinsic width APIs, final pixel application, and baseline exposure. +- `UIHTMLWidget` already stores flex state lazily in `UIHTMLWidgetFlexState`. +- `UILayouterManager::create()` already routes `Flex` / `InlineFlex` and blockifies children of + flex containers before display dispatch. +- `UIHTMLWidget::drawChildren()` already supports `order`-modified painting for flex. Grid should + reuse or generalize this mechanism instead of adding a parallel draw path. +- `row-gap`, `column-gap`, `gap`, `justify-content`, `align-items`, `align-content`, `align-self`, + and `order` already exist for flex, but their CSS Alignment value model is not broad enough for + grid. Grid needs `normal`, `start`, `end`, `self-start`, `self-end`, and `justify-items` / + `justify-self`. +- `src/tests/unit_tests/uihtml_flex_test.cpp` is the model for spec-phased unit tests. + +What does not exist: + +- `CSSDisplay::Grid` / `CSSDisplay::InlineGrid` +- a CSS Grid track-list parser +- grid placement value parsing +- named area parsing and validation +- `GridLayouter` +- `src/tests/unit_tests/uihtml_grid_test.cpp` + +## Design Decisions + +### 1. Create `GridLayouter`, Do Not Reuse `UIGridLayout` + +Create: + +| File | Purpose | +|---|---| +| `include/eepp/ui/gridlayouter.hpp` | `GridLayouter` declaration and compact structs | +| `src/eepp/ui/gridlayouter.cpp` | CSS Grid placement, sizing, alignment, and pixel placement | +| `src/tests/unit_tests/uihtml_grid_test.cpp` | Unit tests grouped by spec phase | + +`UIGridLayout` is unrelated to CSS Grid semantics and should remain untouched except for avoiding +name confusion in comments and docs. + +### 2. Add Lazy Grid State To `UIHTMLWidget` + +Follow the final flex implementation, not the original flex plan draft. Flex now uses +`UIHTMLWidgetFlexState`, because repeated direct style parsing in the hot layout path was not the +best end state. + +Add a sibling: + +```cpp +struct UIHTMLWidgetGridState { + std::string templateRows{ "none" }; + std::string templateColumns{ "none" }; + std::string templateAreas{ "none" }; + std::string autoRows{ "auto" }; + std::string autoColumns{ "auto" }; + CSSGridAutoFlow autoFlow{ CSSGridAutoFlow::Row }; + bool autoFlowDense{ false }; + std::string rowStart{ "auto" }; + std::string rowEnd{ "auto" }; + std::string columnStart{ "auto" }; + std::string columnEnd{ "auto" }; + std::string area{ "auto" }; + CSSJustifyItems justifyItems{ CSSJustifyItems::Normal }; + CSSJustifySelf justifySelf{ CSSJustifySelf::Auto }; +}; +``` + +Keep `row-gap`, `column-gap`, `align-items`, `align-content`, `align-self`, `justify-content`, and +`order` in shared layout state where possible, but expand their value model so flex behavior remains +unchanged and grid can use CSS Alignment's grid values. + +### 3. Parse To Structured Values Once Per Layout Pass + +Grid values are too rich for ad hoc string checks in the algorithm. `GridLayouter::readContainerStyle()` +should parse strings into structured, pass-local data: + +```cpp +struct GridTrackSize; +struct GridTrackList; +struct GridLinePlacement; +struct GridAreaTemplate; +struct GridItem; +struct GridTrack; +``` + +Keep parser helpers private to `gridlayouter.cpp` unless they become useful for tests or CSSOM. +Prefer stack-backed `SmallVector` where bounds are modest. Use heap-backed containers only for +track lists and named-line maps that can grow with author CSS. + +### 4. Store Lines As 1-Based CSS Lines, Convert Late + +The spec and authors use 1-based line numbers and negative indexes from the explicit grid end. +Use 1-based inclusive start / exclusive end lines in placement structs: + +```cpp +struct GridSpan { + int startLine{ 1 }; + int endLine{ 2 }; +}; +``` + +Only convert to zero-based track indexes when indexing `mRows` / `mColumns`. + +### 5. Keep Axes Separate + +Unlike flex, grid always sizes both axes. Use the same algorithm helpers twice: + +- columns are the inline axis, +- rows are the block axis. + +This keeps future writing-mode support possible without baking `x == column` and `y == row` into +every helper. + +### 6. Reuse Paint Sorting + +Generalize `UIHTMLWidget::mNeedsOrderSort` from "flex only" to "order-modified layout context". +Grid should set it when any grid item has non-zero or differing `order`, and `drawChildren()` should +sort grid children with the same stable order used for flex. If z-index handling needs more than +the current path, add that in the paint-order phase with focused tests. + +## Files To Modify + +| File | Change Summary | +|---|---| +| `include/eepp/ui/csslayouttypes.hpp` | Add `Grid`, `InlineGrid`, grid auto-flow enum, grid/box alignment enums or aliases | +| `src/eepp/graphics/csslayouttypes.cpp` | Add string conversion helpers for new display and grid/alignment enums | +| `include/eepp/ui/css/propertydefinition.hpp` | Add grid property IDs and `justify-items` / `justify-self` | +| `src/eepp/ui/css/stylesheetspecification.cpp` | Register grid properties, initial values, and shorthands | +| `src/eepp/ui/css/stylesheetproperty.cpp` | Expand `grid-template`, `grid`, `grid-row`, `grid-column`, `grid-area`, and place shorthands | +| `include/eepp/ui/uihtmlwidget.hpp` | Add `UIHTMLWidgetGridState`, getters/setters, `isGrid()` | +| `src/eepp/ui/uihtmlwidget.cpp` | Apply, serialize, invalidate, draw-sort, and baseline integration for grid | +| `include/eepp/ui/gridlayouter.hpp` | New `GridLayouter` class | +| `src/eepp/ui/gridlayouter.cpp` | New CSS Grid Level 1 implementation | +| `src/eepp/ui/uilayoutermanager.cpp` | Route `Grid` / `InlineGrid`; blockify children of grid containers | +| `src/eepp/ui/uirichtext.cpp` | Treat children of grid containers like flex items for rich-text exclusion and text sizing | +| `include/eepp/ui/uinode.hpp` | Add `friend class GridLayouter` only if direct internal pixel setters are needed | +| `premake4.lua` and generated makefiles | Add `gridlayouter.cpp` and `uihtml_grid_test.cpp` | +| `src/tests/unit_tests/uihtml_grid_test.cpp` | New focused grid unit tests | +| `src/tests/unit_tests/uihtml_tests.cpp` | Add realistic HTML fixture tests | +| `.agent/rules/html-layout-architecture.md` | Document `GridLayouter` routing and supported subset after implementation | + +## Property And Initial Value Table + +| Property | Initial Value | Applies To | +|---|---|---| +| `display` | existing | add `grid`, `inline-grid` | +| `grid-template-rows` | `none` | grid containers | +| `grid-template-columns` | `none` | grid containers | +| `grid-template-areas` | `none` | grid containers | +| `grid-template` | `none` | grid containers | +| `grid-auto-rows` | `auto` | grid containers | +| `grid-auto-columns` | `auto` | grid containers | +| `grid-auto-flow` | `row` | grid containers | +| `grid` | `none` | grid containers | +| `grid-row-start` | `auto` | grid items and relevant abspos boxes | +| `grid-column-start` | `auto` | grid items and relevant abspos boxes | +| `grid-row-end` | `auto` | grid items and relevant abspos boxes | +| `grid-column-end` | `auto` | grid items and relevant abspos boxes | +| `grid-row` | `auto` | shorthand | +| `grid-column` | `auto` | shorthand | +| `grid-area` | `auto` | shorthand or named area | +| `justify-items` | `normal` | grid containers | +| `justify-self` | `auto` | grid items | +| `row-gap` / `column-gap` / `gap` | `normal` | already exists; grid `normal` resolves to 0 | +| `order` | `0` | already exists; now also applies to grid items | + +Important compatibility note: existing flex registration treats `align-items` initial value as +`stretch`, because that is Flexbox's effective initial behavior in this implementation. CSS Grid's +alignment initial behavior is `normal`, which usually stretches non-replaced items. Do not regress +flex tests while expanding alignment parsing. A practical approach is: + +- preserve flex getters' defaults as they are today, +- add grid-specific alignment getters that interpret absent values as grid `normal`, +- parse `normal` and `start` / `end` values without forcing flex to store invalid states. + +## Implementation Plan + +### Phase 0: CSS Infrastructure And Display Routing + +Purpose: parse grid declarations and route `display: grid` safely without layout behavior beyond a +minimal no-crash layouter. + +Steps: + +1. Add `CSSDisplay::Grid` and `CSSDisplay::InlineGrid`. +2. Add grid property IDs. +3. Register grid longhands and simple shorthands. +4. Add `UIHTMLWidgetGridState`, getters, setters, and `isGrid()`. +5. Add `GridLayouter` skeleton with no-op collection and intrinsic width stubs. +6. Route `Grid` / `InlineGrid` to `GridLayouter`. +7. Add `parentIsGridContainer()` blockification in `UILayouterManager::create()`. +8. Update `UIRichText::rebuildRichText()` so direct child spans/text under a grid container are not + incorrectly owned by the parent's inline formatting context. + +Gate tests: + +- `GridProperties.displayGridEnumConversions` +- `GridProperties.longhandDefaults` +- `GridContainer.emptyContainerDoesNotCrash` +- existing `FlexContainer*` tests still pass + +### Phase 1: Track List Parser + +Purpose: parse grid track syntax into a structured representation before implementing placement. + +Supported in this phase: + +- `none` +- fixed lengths and percentages +- `auto` +- `min-content` +- `max-content` +- `fr` +- `minmax(, )` +- `fit-content()` +- line-name brackets `[foo bar]` +- `repeat(, )` + +Defer to Phase 12: + +- `repeat(auto-fill, ...)` +- `repeat(auto-fit, ...)` + +Data model: + +```cpp +enum class GridTrackBreadthType { + Length, + Percentage, + Flex, + Auto, + MinContent, + MaxContent, + FitContent +}; + +struct GridTrackBreadth { + GridTrackBreadthType type; + Float value; + std::string raw; +}; + +struct GridTrackSize { + GridTrackBreadth min; + GridTrackBreadth max; +}; + +struct GridExplicitTrack { + SmallVector beforeLineNames; + GridTrackSize size; +}; +``` + +Parser tests: + +- fixed tracks: `100px 1fr auto` +- line names: `[start] 1fr [mid a] 2fr [end]` +- `repeat(3, 10px [x] 1fr)` +- invalid repeat count +- `minmax(100px, 1fr)` +- `fit-content(200px)` +- percentage tokens remain percentages until layout resolves them + +### Phase 2: Named Areas Parser + +Purpose: implement `grid-template-areas` validation and named area resolution. + +Rules: + +- each string creates one row, +- each non-dot token creates or extends a named area, +- all rows must have equal token count and at least one token, +- each named area must form one filled rectangle, +- named areas generate implicit `foo-start` and `foo-end` line names for both axes. + +Tests: + +- rectangular named areas pass, +- `.` null cells are accepted, +- uneven row widths invalidate the declaration, +- disconnected area tokens invalidate the declaration, +- named area placement via `grid-area: header`, +- implicit line names from areas resolve with line placement. + +### Phase 3: Grid Item Collection And Blockification + +Purpose: mirror `FlexLayouter::collectFlexItems()` for grid semantics. + +Collection rules: + +- include in-flow visible children, +- skip `display: none`, +- skip absolute/fixed children from normal grid layout, +- include `visibility: collapse` as normal for now unless a spec-specific collapse behavior is + implemented later, +- ignore `float` and `clear` on grid items, +- ignore inter-element whitespace text nodes, +- wrap non-whitespace direct text into anonymous grid items. + +Implementation note: + +Flex currently lays out `UITextNode` anonymous flex items directly. Grid should initially do the +same for text nodes, but keep a clear boundary for future anonymous wrapper boxes if inline text needs +multiple child fragments inside one grid item. + +Tests: + +- `GridContainer.collectsInFlowChildren` +- `GridContainer.skipsOutOfFlowChildren` +- `GridContainer.skipsDisplayNoneChildren` +- `GridContainer.ignoresWhitespaceTextNodes` +- `GridContainer.anonymousTextNodeItem` +- `GridContainer.blockifiesInlineChildren` +- `GridContainer.floatDoesNotAffectGridItemPlacement` + +### Phase 4: Definite Placement + +Purpose: support line-based placement without auto-placement complexity. + +Implement: + +- parse ``: + - `auto` + - integer line + - negative integer line against explicit grid end + - `span ` + - custom identifier + - ` ` + - `span ` and `span ` +- `grid-row` and `grid-column` shorthand expansion, +- `grid-area` four-value shorthand expansion, +- conflict handling: + - swap start/end if reversed, + - remove equal end line, + - remove end span if both sides are spans, + - named-only span becomes span 1, +- implicit track growth for explicit placements outside declared bounds. + +Tests: + +- `grid-column: 1 / 3` +- `grid-row: 2` +- `grid-column: 1 / span 2` +- negative end line: `grid-column: 1 / -1` +- reversed placement swaps lines, +- equal start/end becomes one-track span, +- named line placement, +- named area shorthand placement, +- explicit placement creates implicit tracks outside declared bounds. + +### Phase 5: Sparse Auto-Placement + +Purpose: implement CSS Grid Section 8.5 sparse item placement in order-modified document order. + +Implement: + +- `grid-auto-flow: row` +- `grid-auto-flow: column` +- auto-placement cursor, +- fully auto items, +- one-axis definite and one-axis auto items, +- auto spans, +- implicit row/column generation. + +Tests: + +- row flow fills columns then creates new rows, +- column flow fills rows then creates new columns, +- explicitly placed items occupy cells before auto items, +- auto-placement honors `order`, +- sparse placement does not backtrack into earlier holes, +- item with `grid-column: span 2` skips too-small holes. + +### Phase 6: Dense Auto-Placement + +Purpose: implement `grid-auto-flow: dense`. + +Tests: + +- dense backfills earlier holes, +- dense can visually reorder later items, +- dense still uses order-modified document order as the item iteration order, +- dense with spans does not overlap occupied cells. + +### Phase 7: Basic Track Sizing + +Purpose: implement enough of Section 11 to produce correct sizes for common explicit and implicit +grids, then harden intrinsic sizing in later phases. + +Implement: + +1. Establish explicit and implicit tracks. +2. Resolve fixed lengths. +3. Resolve percentages against definite container content size. +4. Treat indefinite percentages as `auto`. +5. Initialize intrinsic tracks with base size 0 and growth limit infinity. +6. Size `auto`, `min-content`, and `max-content` tracks from non-spanning item contributions. +7. Account for gutters as fixed tracks in the calculations. +8. Compute container auto block size as max-content grid size. + +Tests: + +- fixed columns/rows position items at exact offsets, +- percentage columns resolve against container width, +- percentage rows resolve against definite container height, +- indefinite percentage behaves as auto, +- auto column sizes to max child width, +- max-content track uses max intrinsic width, +- min-content track uses min intrinsic width, +- implicit tracks use `grid-auto-rows` / `grid-auto-columns`, +- gaps affect item positions and container auto size. + +### Phase 8: Flexible Tracks + +Purpose: implement `fr` tracks per Section 7.2.4 and Section 11.7. + +Rules: + +- distribute leftover space after non-flexible tracks hit max, +- divide by flex factors, +- preserve partial-fill behavior for total flex factors under 1, +- when available space is indefinite, resolve `fr` from max-content contributions and flex factors. + +Tests: + +- `1fr 1fr` splits leftover space equally, +- `1fr 2fr` uses 1:2 ratio, +- fixed plus `fr` subtracts fixed track first, +- `.25fr .25fr .25fr` consumes 75 percent of leftover, leaving the rest unfilled, +- indefinite container sizes `fr` tracks from content, +- `minmax(100px, 1fr)` respects the minimum. + +### Phase 9: Spanning Item Track Sizing + +Purpose: complete Section 11.5 distribution for items spanning multiple tracks. + +Implement: + +- intrinsic contributions from span > 1 items, +- distribute extra space across spanned tracks, +- respect growth limits, +- handle spanned gutters, +- support min/max track sizing functions with spanning items, +- keep the implementation iterative but bounded. + +Tests: + +- spanning item grows two auto columns, +- spanning item respects fixed track plus flexible/intrinsic track, +- max-content spanning item distributes required width, +- minmax growth limit caps distribution, +- gaps crossed by spans are included. + +### Phase 10: Grid Alignment And Auto Margins + +Purpose: implement Section 10 item alignment and grid alignment. + +Implement: + +- `justify-items` and `justify-self` in the inline axis, +- `align-items` and `align-self` in the block axis, +- `normal` behavior: + - stretch non-replaced items without preferred aspect ratio or natural size, + - use natural/fit-content sizing for replaced/aspect-ratio-like items where eepp exposes it, +- `start`, `end`, `center`, `stretch`, +- auto margins absorb positive free space before self-alignment, +- overflowing auto margins resolve to zero, +- `justify-content` and `align-content` align the whole grid inside the container, +- `space-between`, `space-around`, `space-evenly`, and `stretch` for track distribution. + +Tests: + +- default `normal` stretches ordinary item to its grid area, +- `justify-self: center`, +- `align-self: end`, +- `justify-items` inherited by items unless overridden, +- auto margin centers item and disables self-alignment on that axis, +- `justify-content: center` moves all tracks, +- `align-content: space-between` changes row gaps after track sizing, +- `align-content: stretch` expands auto tracks. + +### Phase 11: `inline-grid`, Intrinsic Sizes, And Baseline + +Purpose: integrate grid containers with the surrounding formatting context. + +Implement: + +- `display: inline-grid` as an atomic inline-level box like `inline-block` / `inline-flex`, +- shrink-wrap inline-grid using max-content contributions, +- `GridLayouter::computeIntrinsicWidths()`, +- grid container baseline for use by parent inline/flex/grid contexts, +- nested grid baseline from first/last relevant grid item per spec, with practical fallback to the + first in-flow item baseline or content-box bottom when unavailable. + +Tests: + +- inline-grid participates in a line and exposes non-zero size, +- inline-grid shrink-wraps fixed and intrinsic tracks, +- grid inside flex can baseline-align, +- flex inside grid receives its assigned area size before flex layout, +- nested grid inside grid lays out once without re-entry loops. + +### Phase 12: `repeat(auto-fill)` And `repeat(auto-fit)` + +Purpose: complete auto-repeat support. + +Implement: + +- compute repetition count from definite container size, +- honor gaps, +- clamp to at least one repetition when required by the spec, +- collapse empty `auto-fit` tracks after placement, +- keep `auto-fill` tracks present even when empty. + +Tests: + +- `repeat(auto-fill, 100px)` creates the expected number of columns, +- `auto-fill` includes empty tracks, +- `auto-fit` collapses empty tracks, +- auto-repeat with gaps, +- auto-repeat with minmax. + +### Phase 13: Shorthand Completion + +Purpose: finish the author-facing property surface after core layout is correct. + +Implement: + +- `grid-template` shorthand, +- `grid` shorthand, +- `place-items`, +- `place-self`, +- `place-content` if not already supported globally. + +Tests: + +- `grid-template: "head head" auto "nav main" 1fr / 100px 1fr` +- `grid: auto-flow 1fr / 100px` +- `grid: none / auto-flow 1fr` +- shorthand resets unspecified subproperties to their initial values, +- `place-items: center stretch`, +- `place-self: end center`, +- `place-content: center space-between`. + +### Phase 14: Absolute Positioning In Grid Containing Blocks + +Purpose: integrate Section 9 after normal grid layout is stable. + +Implement carefully: + +- out-of-flow children do not affect normal grid sizing, +- grid placement properties on abspos children can define their containing grid area, +- offsets then resolve against that containing block, +- if placement is `auto`, use the grid container padding box / existing containing block behavior, +- preserve current `UIHTMLWidget::positionOutOfFlowChildren()` validation. + +Tests: + +- absolute child skipped by normal grid layout, +- absolute child with normal `left/top` positions against grid container, +- absolute child with `grid-area` uses that area as containing block, +- fixed child remains scene-root relative, +- existing `UIHTMLWidget.positionOutOfFlow_*` tests pass. + +### Phase 15: Paint Order, Z-Index, And Overlap + +Purpose: finish visual ordering behavior for overlapping grid items. + +Implement: + +- stable order-modified paint order for grid children, +- reverse nothing for grid; placement controls geometry, not source order direction, +- z-index stacking behavior for grid items with `z-index` other than auto, even when static, +- avoid changing non-grid child paint order. + +Tests: + +- overlapping grid items paint in order-modified document order, +- negative order paints before default order, +- equal order remains stable source order, +- z-index item paints above later source-order item, +- flex paint-order tests still pass. + +### Phase 16: Real HTML Fixtures And Browser Comparison + +Purpose: validate real-world CSS Grid usage beyond algorithm unit tests. + +Add `UIHTML` fixture tests for: + +- card gallery: `repeat(auto-fill, minmax(...))` +- app shell: named template areas with header/nav/main/footer +- form layout: labels and controls with `grid-auto-flow: row dense` +- nested grid inside flex header/body layout +- overlapping named areas with z-index +- inline-grid inside paragraph + +When practical, manually compare one or two fixtures against a browser capture and encode layout +invariants rather than screenshot pixels. + +## Suggested `GridLayouter` Shape + +```cpp +class EE_API GridLayouter : public UILayouter { + public: + GridLayouter( UIWidget* container ) : UILayouter( container ) {} + + void updateLayout() override; + void computeIntrinsicWidths() override; + Float getMinIntrinsicWidth() override; + Float getMaxIntrinsicWidth() override; + Float getBaseline() const { return mContainerBaseline; } + + protected: + void readContainerStyle(); + void collectGridItems(); + void resolveTemplateAreas(); + void resolveDefinitePlacements(); + void autoPlaceItems(); + void buildImplicitTracks(); + void sizeTracksForAxis( GridAxis axis ); + void alignTracks(); + void alignItems(); + void applyLayout(); + + SmallVector mItems; + std::vector mColumns; + std::vector mRows; + GridAreaTemplate mTemplateAreas; + Float mColumnGap{ 0.f }; + Float mRowGap{ 0.f }; + Float mContainerBaseline{ 0.f }; +}; +``` + +Keep the public API as small as `FlexLayouter`. Most algorithm detail should stay private and +test-covered through behavior, not through a broad exported helper surface. + +## Test Plan + +Create `src/tests/unit_tests/uihtml_grid_test.cpp` and mirror the flex test organization: + +- `GridProperties.*` for enum conversion, defaults, parser, and shorthand expansion. +- `GridContainer.*` for direct layout behavior. +- `UIHTML.Grid*` for realistic fixtures in `uihtml_tests.cpp`. + +Minimum unit groups: + +| Group | Coverage | +|---|---| +| Phase 0 | display routing, empty container, property defaults | +| Parser | track lists, line names, repeat, minmax, fit-content, template areas | +| Collection | in-flow children, out-of-flow skip, text nodes, blockification | +| Placement | definite lines, spans, negative lines, names, areas, conflicts | +| Auto-placement | row/column flow, sparse/dense, order | +| Track sizing | fixed, percentage, auto, min/max-content, implicit tracks | +| Flexible tracks | `fr`, partial-fill, indefinite container | +| Spanning | multi-track intrinsic contribution distribution | +| Alignment | item self alignment, content alignment, auto margins, gaps | +| Integration | inline-grid, nested grid/flex, baselines, abspos, paint order | + +Name tests after the behavior, not the implementation helper. Example: + +```cpp +UTEST( GridContainer, fixedTracksPlaceItemsAtGridLines ) +UTEST( GridContainer, templateAreasPlaceNamedItems ) +UTEST( GridContainer, sparseAutoPlacementDoesNotBackfillHoles ) +UTEST( GridContainer, frTracksDistributeLeftoverSpace ) +UTEST( GridContainer, spanningItemGrowsAutoTracks ) +UTEST( GridContainer, justifySelfCenterPositionsInsideGridArea ) +``` + +## Validation Gate + +Before considering a phase complete: + +```sh +make -C make/linux -j$(nproc) +projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="GridProperties*" +projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="GridContainer*" +projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTML.Grid*" +projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="FlexContainer*" +projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="FlexProperties*" +projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug +git diff --check +``` + +If a phase touches out-of-flow positioning, also run: + +```sh +projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTML.HeightExpansion" +projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTMLWidget.positionOutOfFlow_*" +``` + +After editing C++ sources: + +```sh +git diff --name-only -- '*.c' '*.cpp' '*.h' '*.hpp' | xargs clang-format -i +``` + +## Checkpoint Policy + +Use the same checkpoint discipline as the flex implementation. + +At the end of every passing phase: + +1. Run phase-specific focused tests. +2. Run relevant flex regression tests if shared alignment, gap, order, display, or rich-text code + changed. +3. Run the full suite when the phase touches shared layout infrastructure. +4. Run `git diff --check`. +5. Run `clang-format` on modified C/C++ files. +6. Create a recoverable stash checkpoint: + ```sh + git stash push -u -m "grid-layout phase N: " + ``` +7. Re-apply it immediately: + ```sh + git stash apply stash@{0} + ``` +8. Record validation and stash message in phase notes. + +Do not checkpoint a failing phase as complete. + +## Risk Register + +| Risk | Mitigation | +|---|---| +| Track sizing algorithm complexity | Implement fixed/auto/fr first, then spanning distribution, each with focused tests | +| Alignment enum changes regress flex | Preserve flex defaults and run `FlexContainer*` / `FlexProperties*` after shared CSS alignment edits | +| RichText accidentally owns grid children | Mirror flex's parent-container checks and add tests for inline spans/text under grid | +| Auto-placement overlap bugs | Maintain an explicit occupancy matrix and test sparse/dense with spanning items | +| Infinite implicit grid growth | Cap internal growth to the highest required line plus bounded cursor progress; fail safe on pathological input | +| Percentage/intrinsic cycles | Follow Section 11 order, resolve columns then rows, repeat only the specified once-only intrinsic update | +| Out-of-flow regressions | Keep abspos grid placement until late phase and run existing positioned-layout tests | +| Existing `UIGridLayout` confusion | Keep CSS Grid names under `GridLayouter` and document that `UIGridLayout` is unrelated | +| Performance on large grids | Use stack vectors for normal cases, avoid per-cell heap allocations where track spans can be represented as ranges | + +## Definition Of Done + +- `display: grid` and `display: inline-grid` route through `GridLayouter`. +- All scoped Level 1 properties parse, serialize reasonably, invalidate layout, and participate in + layout where applicable. +- Grid placement and track sizing follow CSS Grid Sections 8 and 11. +- Grid item and grid content alignment follow Section 10 for current horizontal LTR mode. +- Anonymous text grid items, blockification, nested flex/grid, auto margins, gaps, order, and paint + order are covered by tests. +- Realistic `UIHTML.Grid*` fixture tests cover common web layout patterns. +- Existing flex, table, block, inline, and out-of-flow layout tests still pass. +- `.agent/rules/html-layout-architecture.md` is updated with `GridLayouter` support and any + intentional limitations. diff --git a/.agent/plans/white_space_collapse_support_plan.md b/.agent/plans/white_space_collapse_support_plan.md new file mode 100644 index 000000000..e9d810834 --- /dev/null +++ b/.agent/plans/white_space_collapse_support_plan.md @@ -0,0 +1,336 @@ +# CSS `white-space-collapse` Support Plan + +## Goal + +Implement real CSS Text Level 4 `white-space-collapse` behavior in the HTML/RichText pipeline. + +The immediate product goal is that `
` can render as normal HTML/RichText content, preserving
+code indentation, blank lines, and line breaks without the current `UICodeEditor` fallback in
+`UIRichText::loadFromXmlNode()`.
+
+Reference specs:
+
+- CSS Text Module Level 4: https://drafts.csswg.org/css-text-4/#white-space-collapsing
+- Stable TR snapshot: https://www.w3.org/TR/css-text-4/#white-space-collapsing
+
+## Current State
+
+- `UIRichText` already stores:
+  - `WhiteSpaceCollapse::Collapse`
+  - `WhiteSpaceCollapse::Preserve`
+  - `WhiteSpaceCollapse::PreserveBreaks`
+  - `WhiteSpaceCollapse::PreserveSpaces`
+  - `WhiteSpaceCollapse::BreakSpaces`
+- CSS parsing already recognizes `white-space` and `white-space-collapse`.
+- `white-space: nowrap` is partially working because it maps to `mLineWrap = false`.
+- The actual text processing is incomplete:
+  - `UIRichText::loadFromXmlNode()` collapses all PCDATA through
+    `UIRichText::collapseInternalWhitespace()` before style resolution can know the effective
+    `white-space-collapse` value.
+  - `UIRichText::rebuildRichText()` only distinguishes `Collapse` from all other values.
+  - `preserve-breaks`, `preserve-spaces`, and `break-spaces` do not get their spec behavior.
+  - `break-spaces` has no special line-breaking or intrinsic-size support.
+- `
` is special-cased in `UIRichText::loadFromXmlNode()` into a read-only `UICodeEditor`.
+  That workaround blocks normal HTML styling/layout for code blocks and should become unnecessary.
+
+## Spec Behavior To Support
+
+Implement the full `white-space-collapse` value set:
+
+| Value | Required behavior |
+|---|---|
+| `collapse` | Collapse white-space runs and segment breaks into a single space, with existing block-boundary trimming. |
+| `preserve` | Preserve spaces, tabs, and segment breaks. Segment breaks become forced line breaks. |
+| `preserve-breaks` | Collapse spaces/tabs, but preserve segment breaks as forced line breaks. |
+| `preserve-spaces` | Preserve spaces; convert tabs and segment breaks to spaces. |
+| `break-spaces` | Preserve spaces/tabs/segment breaks like `preserve`, but allow a wrap opportunity after every preserved space and make trailing preserved spaces measurable/non-hanging. |
+| `discard` | Spec-defined but not broadly implemented by browsers. Add enum/parser support and implement as "remove all collapsible white-space characters" so the engine is complete and deterministic. |
+
+`white-space` shorthand must continue mapping correctly:
+
+| `white-space` | collapse mode | wrap mode |
+|---|---|---|
+| `normal` | `collapse` | wrap |
+| `nowrap` | `collapse` | nowrap |
+| `pre` | `preserve` | nowrap |
+| `pre-wrap` | `preserve` | wrap |
+| `pre-line` | `preserve-breaks` | wrap |
+| `break-spaces` | `break-spaces` | wrap |
+
+Also accept two-keyword Level 4 forms where practical, e.g. `white-space: preserve nowrap`,
+`white-space: preserve wrap`, `white-space: collapse nowrap`, and direct
+`white-space-collapse: preserve-spaces`.
+
+## Implementation Plan
+
+### Phase 1: Preserve Source Text Until Layout
+
+**Files:**
+
+- `src/eepp/ui/uirichtext.cpp`
+- `include/eepp/ui/uirichtext.hpp`
+- `src/tests/unit_tests/richtext_tests.cpp`
+- `src/tests/unit_tests/uihtml_tests.cpp`
+
+Steps:
+
+1. Stop calling `collapseInternalWhitespace()` when creating `UITextNode` from PCDATA in
+   `UIRichText::loadFromXmlNode()`.
+2. Store raw parsed text in `UITextNode`; CSS whitespace processing must happen only during rich-text
+   rebuild/layout.
+3. Keep `collapseInternalWhitespace()` temporarily for existing callers/tests, but move toward a new
+   policy-driven helper.
+4. Add a focused regression proving that a raw text node under `white-space-collapse: preserve`
+   keeps repeated spaces and `\n` characters through `rebuildRichText()`.
+
+This phase must keep existing collapsed HTML indentation behavior by moving that collapse decision
+from load time to `rebuildRichText()`, not by preserving all source indentation in default layout.
+
+### Phase 2: Add A Whitespace Normalization Policy
+
+**Files:**
+
+- `include/eepp/ui/uirichtext.hpp`
+- `src/eepp/ui/uirichtext.cpp`
+
+Introduce a small helper around the current enum:
+
+```cpp
+struct WhiteSpaceProcessing {
+    WhiteSpaceCollapse collapse;
+    bool lineWrap;
+    bool preservesSegmentBreaks;
+    bool preservesSpaces;
+    bool convertsTabsToSpaces;
+    bool breakAfterSpaces;
+    bool discardWhitespace;
+};
+```
+
+The exact shape can be simplified, but the call site needs explicit booleans instead of repeated
+`collapse == X` checks.
+
+Add a helper with one responsibility:
+
+```cpp
+static String processWhiteSpaceForLayout(
+    String::View raw,
+    WhiteSpaceProcessing policy,
+    WhiteSpaceBoundary boundary );
+```
+
+`WhiteSpaceBoundary` should carry only the cross-node state needed by inline formatting:
+
+- whether the previous emitted text ended with collapsible space,
+- whether the logical previous/next item is inline,
+- whether block-boundary trimming is allowed,
+- whether the caller is inside an inline box.
+
+Keep this allocation-conscious:
+
+- Continue using `String::View` where text is passed through unchanged.
+- Allocate a transformed `String` only when the policy actually changes the bytes.
+- Reuse the existing local transformed string pattern already used for `text-transform`.
+
+### Phase 3: Apply Policy In `rebuildRichText()`
+
+**File:** `src/eepp/ui/uirichtext.cpp`
+
+Replace the current `shouldCollapse` boolean with a full effective policy:
+
+1. Resolve the effective whitespace mode from the nearest `UIRichText`/`UITextSpan` ancestor.
+2. Apply the same policy to:
+   - standalone `UITextNode` content,
+   - inline `UITextSpan::getText()` content,
+   - `UIRichText` self text when a `UITextSpan` is being rebuilt directly.
+3. Preserve the existing logical-prev/logical-next handling for default collapsed HTML whitespace.
+4. Make block-boundary trimming conditional:
+   - allowed for `collapse` and `preserve-breaks` around collapsible spaces,
+   - not allowed for `preserve`, `preserve-spaces`, or `break-spaces`.
+5. Treat `
` as a forced line break independent of the whitespace collapse mode. +6. Make `UITextNode::setLayoutCharCount()` reflect the processed layout string length, while leaving + raw node text intact. + +Expected examples: + +- Default HTML source indentation around block children still disappears. +- `white-space-collapse: preserve` keeps `" a\n b"`. +- `white-space-collapse: preserve-breaks` turns `" a\n b"` into `" a\n b"` behavior. +- `white-space-collapse: preserve-spaces` turns tabs/newlines into spaces without collapsing spaces. +- `white-space-collapse: discard` emits no spaces/tabs/newlines. + +### Phase 4: Teach `RichText` About `break-spaces` + +**Files:** + +- `include/eepp/graphics/richtext.hpp` +- `src/eepp/graphics/richtext.cpp` +- `include/eepp/graphics/linewrap.hpp` +- `src/eepp/graphics/linewrap.cpp` +- `src/tests/unit_tests/richtext_tests.cpp` + +`break-spaces` cannot be fully implemented only by preprocessing strings. It changes wrapping and +intrinsic sizing: + +1. Add a compact wrap behavior flag to `RichText`, for example: + + ```cpp + enum class WhiteSpaceWrapMode { + Normal, + Preserve, + BreakSpaces + }; + ``` + + Or use smaller booleans if that fits the existing code better. + +2. Set it from `UIRichText::rebuildRichText()` based on the effective container policy. +3. Thread the behavior into `RichTextInlineLayouter` and `LineWrap::computeLineBreaksEx()`. +4. For `break-spaces`, add a soft wrap opportunity after every preserved space/tab. +5. Ensure trailing preserved spaces contribute to rendered width and min/max intrinsic widths. +6. Keep the default `RichText` behavior unchanged for non-HTML callers. + +This is the phase most likely to expose assumptions in line wrapping and intrinsic sizing. Keep the +first patch narrow: only add the flags needed by `break-spaces`, then expand tests. + +### Phase 5: CSS Shorthand And Enum Completeness + +**Files:** + +- `include/eepp/ui/uirichtext.hpp` +- `src/eepp/ui/uirichtext.cpp` +- `src/eepp/ui/css/stylesheetspecification.cpp` +- `src/tests/unit_tests/uihtml_tests.cpp` + +Steps: + +1. Add `WhiteSpaceCollapse::Discard`. +2. Fix redundant checks in `toWhiteSpaceCollapse()` and include `discard`. +3. Extend `fromWhiteSpaceCollapse()` and `fromWhiteSpace()`. +4. Extend `applyWhiteSpace()` to parse: + - legacy single-keyword values, + - `break-spaces`, + - direct collapse keywords, + - `wrap` / `nowrap` when present as Level 4 shorthand components. +5. Keep `white-space-trim` out of scope unless it already exists elsewhere; explicitly document this + limitation near `applyWhiteSpace()` or in `html-layout-architecture.md`. + +### Phase 6: Replace The `
` Fallback
+
+**File:** `src/eepp/ui/uirichtext.cpp`
+
+Once the whitespace behavior is covered:
+
+1. Remove the `mTag == "pre" && child.name() == "code"` branch that creates `UICodeEditor`.
+2. Let `` be created through `UIWidgetCreator::createFromName()` or the existing unknown-element
+   fallback to `UITextSpan`.
+3. Ensure base HTML CSS gives the right defaults:
+   - `pre { white-space: pre; }` or equivalent.
+   - `code { font-family: monospace; }` if not already present.
+   - `pre code` remains normal inline/flow content inside the `
`.
+4. Preserve `data-language` as ordinary data/style metadata. Do not reintroduce syntax-highlighting
+   behavior through the generic HTML path in this feature.
+
+### Phase 7: Tests
+
+Add tests before removing the fallback.
+
+#### RichText Unit Tests
+
+Add focused tests in `src/tests/unit_tests/richtext_tests.cpp`:
+
+- `WhiteSpaceCollapseCollapseDefault`
+- `WhiteSpaceCollapsePreserveSpacesAndBreaks`
+- `WhiteSpaceCollapsePreserveBreaks`
+- `WhiteSpaceCollapsePreserveSpaces`
+- `WhiteSpaceCollapseBreakSpacesWrapsAfterSpaces`
+- `WhiteSpaceCollapseDiscard`
+- `WhiteSpaceCollapseAcrossInlineSpanBoundaries`
+
+Assertions should inspect generated lines, text fragments, or text-node layout char counts. Avoid
+pixel-only assertions unless testing trailing-space width.
+
+#### UIHTML Tests
+
+Add focused tests in `src/tests/unit_tests/uihtml_tests.cpp`:
+
+- `WhiteSpaceCollapsePreCodePreservesIndentation`
+- `WhiteSpaceCollapsePreCodeUsesRichTextNotCodeEditor`
+- `WhiteSpaceCollapsePreLinePreservesBreaksOnly`
+- `WhiteSpaceCollapseBreakSpacesAffectsIntrinsicWidth`
+
+For the `
` regression:
+
+```html
+
if (x) {
+    return 1;
+}
+
+``` + +Verify: + +- `code` is not a `UICodeEditor`. +- indentation before `return` survives. +- line count is at least 3. +- the `
`/`` content participates in normal RichText layout.
+
+#### Existing Tests To Re-run
+
+Focused:
+
+```sh
+projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIRichText.*WhiteSpace*"
+projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTML.WhiteSpace*"
+projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UITextNode.*"
+projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTMLFloat.*whitespace*"
+```
+
+Broader:
+
+```sh
+make -C make/linux -j$(nproc)
+projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTML.*"
+projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug
+git diff --check
+```
+
+If the local runner uses the older direct `xvfb-run` form, keep the same ASAN/screen settings:
+
+```sh
+ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug --filter="UIHTML.WhiteSpace*"
+```
+
+## Risks And Constraints
+
+- Do not collapse raw text at parse time. The spec says whitespace processing is for rendering and
+  must not mutate underlying document data.
+- Do not make a tag-specific `
` or `` workaround. The feature belongs to CSS text
+  processing and inline formatting.
+- Be careful with source indentation in fixture HTML. Removing parse-time collapse will expose many
+  indentation-only text nodes unless default `collapse` behavior remains correct in `rebuildRichText()`.
+- `break-spaces` affects the line breaker and intrinsic sizing. It is not enough to preserve bytes.
+- Keep changes allocation-conscious. The RichText rebuild path is hot during layout invalidation.
+- Preserve current inline metadata behavior: backgrounds, borders, selection, anchor hit boxes,
+  inline-block baselines, floats, and out-of-flow skipping.
+
+## Suggested Patch Order
+
+1. Add tests documenting current failure for preserved raw text and `
`.
+2. Move PCDATA collapse from load time to policy-driven rebuild time.
+3. Implement all non-`break-spaces` collapse modes.
+4. Add RichText/LineWrap support for `break-spaces`.
+5. Add `discard` parser/state behavior.
+6. Remove the `
` `UICodeEditor` fallback.
+7. Update `html-layout-architecture.md` with the final whitespace-processing notes.
+8. Run focused tests, then `UIHTML.*`, then the full suite.
+
+## Completion Criteria
+
+- All `white-space-collapse` values have parser, property-string, layout, and test coverage.
+- Existing `white-space: nowrap` behavior remains passing.
+- `
` renders through normal HTML/RichText nodes, not `UICodeEditor`.
+- Code indentation and blank lines are preserved in `
` and `white-space: pre` content.
+- Default HTML whitespace still collapses around block and inline boundaries.
+- `break-spaces` wraps and measures trailing spaces according to CSS Text Level 4.
+- Full unit suite passes under Xvfb/ASAN.
diff --git a/.agent/rules/html-layout-architecture.md b/.agent/rules/html-layout-architecture.md
index a49a6385c..2f18c5f97 100644
--- a/.agent/rules/html-layout-architecture.md
+++ b/.agent/rules/html-layout-architecture.md
@@ -16,6 +16,7 @@ Primary references:
 - CSS Visual Formatting Model, including inline formatting and positioning: https://www.w3.org/TR/CSS22/visuren.html
 - CSS line height and inline-block baseline rules: https://www.w3.org/TR/CSS22/visudet.html#line-height
 - CSS Lists and Counters: https://www.w3.org/TR/css-lists-3/
+- CSS Flexbox Module Level 1: https://www.w3.org/TR/css-flexbox-1/
 
 Required workflow for new HTML/CSS behavior:
 
@@ -41,12 +42,20 @@ Important responsibilities:
 
 Layout math is separated from widgets into `UILayouter` implementations:
 
-- `BlockLayouter`: Handles block-like containers, including `display: block`, `inline-block`, `list-item`, `table-cell`, and the current `flex` placeholder path. It delegates inline formatting to `RichText`, then maps the resulting physical spans back to child widgets.
+- `BlockLayouter`: Handles block-like containers, including `display: block`, `inline-block`, `list-item`, and `table-cell`. It delegates inline formatting to `RichText`, then maps the resulting physical spans back to child widgets. Also used for blockified flex item children.
+- `FlexLayouter`: Full CSS Flexbox Level 1 implementation for `display: flex` and `display: inline-flex`. Handles all flex container/item properties, the flex layout algorithm (item collection, flex basis resolution, flexible length distribution, main/cross-axis alignment, wrapping, gaps), auto margins, `order`-based paint sorting, `visibility: collapse`, min-width:auto, baseline alignment, and anonymous flex item text wrapping.
 - `TableLayouter`: Handles `display: table` and encapsulates HTML table column width distribution, rows, sections, and cell positioning.
 - `InlineLayouter`: A no-op layouter for true inline text-span elements. Inline formatting is owned by the nearest rich-text/block formatting context so normal widget layout does not override text flow.
 - `NoneLayouter`: Handles `display: none` by skipping layout/rendering participation.
 
-`UILayouterManager::create()` is the dispatch point. Before adding a new display mode, check whether it should create a new formatting context, participate in an existing one, or be represented as a rich-text custom box.
+`UILayouterManager::create()` is the dispatch point. Key routing rules:
+- `Block`/`InlineBlock`/`ListItem`/`TableCell` → `BlockLayouter`
+- `Flex`/`InlineFlex` → `FlexLayouter`
+- `Inline` → `InlineLayouter` (for text spans) or `BlockLayouter`
+- `Table` → `TableLayouter`
+- `None` → `NoneLayouter`
+
+**Blockification per CSS Flexbox §4:** Children of flex containers are automatically blockified — they receive `BlockLayouter` regardless of their own `display` value (unless they are themselves a flex container, which keeps `FlexLayouter`). This is enforced in `UILayouterManager::create()` before the display-based dispatch.
 
 ## RichText Integration
 
@@ -73,7 +82,9 @@ Atomic inline widgets (`RichText::RenderSpan::Type::AtomicBox`) do not call `UIT
 
 ### UITextNode
 
-`UITextNode` is a lightweight non-rendering node for raw parsed text (`node_pcdata`). Its text is extracted during `rebuildRichText()` and rendered by the parent `UIRichText`. After wrapping, `BlockLayouter` assigns it position and size for debugging and hit-box accounting, but `UITextNode::draw()` remains a no-op.
+`UITextNode` is a lightweight non-rendering node for raw parsed text (`node_pcdata`). Its text is extracted during `rebuildRichText()` and rendered by the parent `UIRichText`. After wrapping, `BlockLayouter` assigns it position and size for debugging and hit-box accounting.
+
+**Flex item special case:** When a `UITextNode` becomes an anonymous flex item (bare text child of a flex container), it uses a cached `Text* mFlexText` object for multi-line word wrapping. `FlexLayouter::resolveCrossSizes()` configures `mFlexText` with the target font, text, and `setMaxWrapWidth(targetMainSize)`, then measures `getTextHeight()` for cross sizing. `UITextNode::draw()` renders `mFlexText` at the flex-assigned position instead of being a no-op.
 
 ### Custom Blocks And Baselines
 
@@ -94,6 +105,8 @@ This is required for `display: inline-block` and for nested rich-text widgets su
 
 Do not fix baseline problems by special-casing individual elements, zeroing `line-height`, or changing element display defaults. The correct layer is generic inline formatting and custom-block baseline propagation.
 
+**Flex container baseline:** `FlexLayouter` stores the container's baseline after layout in `mContainerBaseline`. For row-direction flex containers, the baseline comes from the first flex line's maximum baseline offset. For column-direction, it comes from the last flex line. `UIHTMLWidget::getBaseline()` delegates to `FlexLayouter::getBaseline()` when the widget is a flex container, allowing outer flex containers to baseline-align nested flex containers correctly.
+
 ## Display And Flow Rules
 
 ### Inline Content
@@ -120,6 +133,22 @@ Requirements:
 - `` defaults to `display: list-item` and uses disclosure marker defaults as defined by HTML rendering rules.
 - `disclosure-open` and `disclosure-closed` should use eepp's primitive marker drawing facilities, not textual `v` or `>` approximations.
 
+### Flex Layout
+
+`display: flex` and `display: inline-flex` use `FlexLayouter` implementing the full CSS Flexbox Level 1 layout algorithm. Key behaviors:
+
+- **Item collection:** In-flow children are collected, sorted by CSS `order` property. Out-of-flow (`absolute`/`fixed`) and `display: none` children are skipped. Bare `UITextNode` children become anonymous flex items with text-based sizing.
+- **Flex basis resolution:** `flex-basis` resolves against the container's inner main size (percentage) or uses the item's explicit main size / content size (`auto`). `flex-basis: content` bypasses the explicit main-size check.
+- **Flexible length resolution:** Iterative §9.7 algorithm: distribute free space, clamp by min/max, freeze items that hit constraints, redistribute remaining space to unfrozen items.
+- **Main-axis alignment:** `justify-content` with all values (flex-start, flex-end, center, space-between, space-around, space-evenly). Auto margins on main axis absorb free space before justify-content applies (§8.1).
+- **Cross-axis alignment:** `align-items`/`align-self` with flex-start, flex-end, center, baseline, stretch. Auto margins on cross axis absorb free space before alignment. Baseline alignment matches text baselines across items on the same flex line.
+- **Multi-line:** `flex-wrap: wrap`/`wrap-reverse` with per-line cross sizing and `align-content` distribution (all values including space-evenly). When container main size is indefinite, all items stay on a single line.
+- **Gaps:** `row-gap`/`column-gap` with `gap` shorthand. `gap: normal` resolves to 0px in flexbox.
+- **`min-width: auto` / `min-height: auto`:** Content-based minimum prevents flex items from collapsing below their min-content size. The `overflow` property suppresses this minimum when set to non-visible.
+- **`visibility: collapse`:** Collapsed items are invisible and zero-sized on the main axis, but their cross size still contributes to the flex line cross size (preserving layout stability).
+- **`order`-modified paint order:** `UIHTMLWidget::drawChildren()` override stable-sorts children by CSS `order` when flex items have differing values. For `row-reverse`/`column-reverse`, the sorted list is also reversed before painting.
+- **Container baseline:** After layout, the flex container's baseline is available via `FlexLayouter::getBaseline()` for use by outer formatting contexts.
+
 ### Out-Of-Flow Positioning
 
 Elements with `position: absolute` or `position: fixed`:
@@ -148,7 +177,8 @@ For HTML/CSS layout work, prefer narrow tests plus one realistic fixture when th
 
 - Unit-level tests for parser/style/layout primitives.
 - RichText tests for inline formatting, wrapping, baselines, custom blocks, floats, and line-height.
-- UIHTML fixture tests for browser-like element interactions such as details/summary, tables, forms, lists, images, and positioned descendants.
+- Flex algorithm tests for all flex container/item properties, direction/wrap modes, alignment, gaps, auto margins, baseline, collapsed items, and edge cases (71 unit tests in `uihtml_flex_test.cpp`).
+- UIHTML fixture tests for browser-like element interactions such as flexbox layouts, details/summary, tables, forms, lists, images, and positioned descendants.
 - Regression assertions should verify layout invariants, not screenshot pixels only.
 
 When possible, compare against browser behavior manually or with a reference capture, then encode the spec behavior in assertions.
diff --git a/.ecode/project_build.json b/.ecode/project_build.json
index 131419d63..185197348 100644
--- a/.ecode/project_build.json
+++ b/.ecode/project_build.json
@@ -377,7 +377,7 @@
         "working_dir": "${project_root}/bin"
       },
       {
-        "args": "-d1 https://asahilinux.org/2026/04/progress-report-7-0/",
+        "args": "-c system --hn-dark",
         "command": "${project_root}/bin/eepp-ui-html-debug",
         "name": "eepp-ui-html-debug",
         "working_dir": "${project_root}/bin"