mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-03 03:31:04 +03:00
Add grid layout and white space collapse support plans.
This commit is contained in:
@@ -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(<integer>, ...)`
|
||||||
|
- `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(<track-breadth>, <track-breadth>)`
|
||||||
|
- `fit-content(<length-percentage>)`
|
||||||
|
- line-name brackets `[foo bar]`
|
||||||
|
- `repeat(<positive-integer>, <track-list>)`
|
||||||
|
|
||||||
|
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<std::string, 2> 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 `<grid-line>`:
|
||||||
|
- `auto`
|
||||||
|
- integer line
|
||||||
|
- negative integer line against explicit grid end
|
||||||
|
- `span <integer>`
|
||||||
|
- custom identifier
|
||||||
|
- `<integer> <custom-ident>`
|
||||||
|
- `span <custom-ident>` and `span <integer> <custom-ident>`
|
||||||
|
- `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<GridItem, 16> mItems;
|
||||||
|
std::vector<GridTrack> mColumns;
|
||||||
|
std::vector<GridTrack> 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: <short description>"
|
||||||
|
```
|
||||||
|
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.
|
||||||
@@ -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 `<pre><code>` 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.
|
||||||
|
- `<pre><code>` 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 `<br>` 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 `<pre><code>` 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 `<code>` 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 `<pre>`.
|
||||||
|
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 `<pre><code>` regression:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<pre id="pre"><code id="code">if (x) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
- `code` is not a `UICodeEditor`.
|
||||||
|
- indentation before `return` survives.
|
||||||
|
- line count is at least 3.
|
||||||
|
- the `<pre>`/`<code>` 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 `<pre>` or `<code>` 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 `<pre><code>`.
|
||||||
|
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 `<pre><code>` `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.
|
||||||
|
- `<pre><code>` renders through normal HTML/RichText nodes, not `UICodeEditor`.
|
||||||
|
- Code indentation and blank lines are preserved in `<pre>` 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.
|
||||||
@@ -16,6 +16,7 @@ Primary references:
|
|||||||
- CSS Visual Formatting Model, including inline formatting and positioning: https://www.w3.org/TR/CSS22/visuren.html
|
- 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 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 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:
|
Required workflow for new HTML/CSS behavior:
|
||||||
|
|
||||||
@@ -41,12 +42,20 @@ Important responsibilities:
|
|||||||
|
|
||||||
Layout math is separated from widgets into `UILayouter` implementations:
|
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.
|
- `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.
|
- `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.
|
- `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
|
## RichText Integration
|
||||||
|
|
||||||
@@ -73,7 +82,9 @@ Atomic inline widgets (`RichText::RenderSpan::Type::AtomicBox`) do not call `UIT
|
|||||||
|
|
||||||
### UITextNode
|
### 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
|
### 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.
|
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
|
## Display And Flow Rules
|
||||||
|
|
||||||
### Inline Content
|
### Inline Content
|
||||||
@@ -120,6 +133,22 @@ Requirements:
|
|||||||
- `<summary>` defaults to `display: list-item` and uses disclosure marker defaults as defined by HTML rendering rules.
|
- `<summary>` 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.
|
- `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
|
### Out-Of-Flow Positioning
|
||||||
|
|
||||||
Elements with `position: absolute` or `position: fixed`:
|
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.
|
- Unit-level tests for parser/style/layout primitives.
|
||||||
- RichText tests for inline formatting, wrapping, baselines, custom blocks, floats, and line-height.
|
- 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.
|
- 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.
|
When possible, compare against browser behavior manually or with a reference capture, then encode the spec behavior in assertions.
|
||||||
|
|||||||
@@ -377,7 +377,7 @@
|
|||||||
"working_dir": "${project_root}/bin"
|
"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",
|
"command": "${project_root}/bin/eepp-ui-html-debug",
|
||||||
"name": "eepp-ui-html-debug",
|
"name": "eepp-ui-html-debug",
|
||||||
"working_dir": "${project_root}/bin"
|
"working_dir": "${project_root}/bin"
|
||||||
|
|||||||
Reference in New Issue
Block a user