Story 1.3: Complete positioning support including auto offset resolution, clip rect, fixed scroll behavior, and containing block edge cases. Key changes: - Refactored calculate_absolute_layout() for full §10.3.7/§10.6.4 compliance - Implemented clip: rect() parsing, style computation, layout, and paint - Fixed sticky child recursion in process_deferred_absolutes - Fixed shrink-to-fit abs_cb using unpositioned padding_box - Added collapsed_borders handling in offset_children - Propagated CSS clip to descendant stacking contexts - Tightened rect() parser to reject garbage between rect and ( - Added tracing::warn for unrecognized clip tokens - Replaced hardcoded epsilon with MARGIN_EPSILON constant - Added RTL unimplemented comments on over-constrained resolution - Strengthened tests: exact assertions, delta comparisons, new coverage - 4 new tests: negative offsets, clip suppression, padding edge distinction - 5 golden tests (222-226), promoted WPT absolute-tables-016 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
250 lines
19 KiB
Markdown
250 lines
19 KiB
Markdown
# Story 1.3: Positioning Completeness
|
|
|
|
Status: done
|
|
|
|
## Story
|
|
|
|
As a web user,
|
|
I want fixed-position headers, tooltips, and absolutely positioned elements to appear in the correct location,
|
|
so that page layouts with positioned elements render correctly.
|
|
|
|
## Acceptance Criteria
|
|
|
|
1. **Given** an element with `position: fixed`, **When** the page is scrolled, **Then** the element remains fixed relative to the viewport
|
|
2. **Given** a positioned element with `top`, `right`, `bottom`, `left` set to `auto`, **When** the page is rendered, **Then** auto values resolve per CSS 2.1 §10.6.4 and §10.3.7 based on the element's static position
|
|
3. **Given** a positioned element with `clip: rect(...)`, **When** the page is rendered, **Then** the element's visible area is clipped to the specified rectangle per CSS 2.1 §11.1.2
|
|
4. **Given** an absolutely positioned element within a relatively positioned container, **When** the page is rendered, **Then** the element is positioned relative to its containing block's padding edge
|
|
5. Golden tests cover fixed positioning, auto resolution, and clip, `docs/CSS2.1_Implementation_Checklist.md` is updated, and `just ci` passes
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [x] Task 1: Fixed positioning scroll behavior (AC: #1)
|
|
- [x] 1.1 Audit `process_deferred_absolutes()` in `crates/layout/src/engine/block/positioning.rs:316-342` — verified fixed elements use viewport dimensions as containing block via `self.viewport.get()`
|
|
- [x] 1.2 Audit `render_stacking_context()` in `crates/display_list/src/builder.rs` — verified fixed-position elements reset CumulativeOffset to (0,0), immune to scroll
|
|
- [x] 1.3 In `crates/render/` or `crates/display_list/`, verified scroll offset is NOT applied to fixed-position display list items — scroll is handled entirely in display list builder
|
|
- [x] 1.4 Scroll behavior already correctly implemented — no code changes needed
|
|
- [x] 1.5 Existing golden test `104-fixed-with-scroll.html` covers fixed positioning layout; unit tests added for scroll behavior
|
|
- [x] 1.6 Unit tests: `test_fixed_element_not_affected_by_scroll` and `test_fixed_element_containing_block_is_viewport` in scroll_offset_tests.rs
|
|
|
|
- [x] Task 2: Auto offset resolution completeness (AC: #2)
|
|
- [x] 2.1 Audited and fixed `calculate_absolute_layout()` for CSS 2.1 §10.3.7 — all horizontal cases handled: both auto (static position), left/right auto, width auto with constraint equation, over-constrained (ignore right for LTR), auto margins with abs-pos constraint
|
|
- [x] 2.2 Audited and fixed §10.6.4 (vertical) — auto margin centering when top+bottom+height specified, bottom positioning when top is auto
|
|
- [x] 2.3 Verified static position fallback — `static_x`/`static_y` correctly passed from `pending_absolutes` and used when both left/right or top/bottom are auto
|
|
- [x] 2.4 Over-constrained resolution implemented — when none of left/right/width is auto, left wins for LTR (right ignored)
|
|
- [x] 2.5 Unit tests: 7 abs-pos tests in `crates/layout/src/engine/block/tests.rs`
|
|
- [x] 2.6 Golden tests: 222 (static position), 223 (left-auto right-specified), 224 (over-constrained + auto-margin centering)
|
|
|
|
- [x] Task 3: `clip: rect(...)` implementation (AC: #3)
|
|
- [x] 3.1 Added `clip` property parsing in `crates/css/src/parser/mod.rs` — parses `clip: rect(top, right, bottom, left)`, `clip: auto`, comma/space-separated
|
|
- [x] 3.2 Added `ClipRect` type in `crates/style/src/types/primitives.rs` — struct with four `Option<f32>` values (None = auto)
|
|
- [x] 3.3 Added `clip: Option<ClipRect>` field to `ComputedStyles` in `crates/style/src/types/computed.rs`
|
|
- [x] 3.4 Wired cascade resolution in `apply_declaration` — `PropertyId::Clip` variant added, ClipRect mapped from CssValue::ClipRect
|
|
- [x] 3.5 Propagated clip to `LayoutBox` in `crates/layout/src/types.rs` and `box_tree.rs` — only set for position:absolute/fixed per §11.1.2
|
|
- [x] 3.6 Applied clip in display list builder — `PushClip`/`PopClip` commands emitted around clipped elements
|
|
- [x] 3.7 Rasterizer already handles clip via `PushClip`/`PopClip` and `apply_clip_stack` — no additional changes needed
|
|
- [x] 3.8 Unit tests: 4 clip tests in `crates/style/src/tests/positioning.rs` (parsing, auto values, reset, full pipeline)
|
|
- [x] 3.9 Golden test: 225-clip-rect.html (absolute and fixed elements with clip)
|
|
|
|
- [x] Task 4: Containing block edge cases (AC: #4)
|
|
- [x] 4.1 Verified `establishes_containing_block()` returns `position != Static` — correct per CSS 2.1. All callers use padding box.
|
|
- [x] 4.2 Verified `abs_cb_for_children` uses `layout_box.dimensions.padding_box()` at layout.rs:200-201
|
|
- [x] 4.3 Test: `test_abspos_containing_block_is_padding_edge` — verifies padding edge, not content edge
|
|
- [x] 4.4 Test: `test_abspos_deeply_nested_chains` — A(rel) > B(static) > C(abs) > D(abs) chain verified
|
|
- [x] 4.5 Golden test: 226-abspos-containing-block-chains.html
|
|
|
|
- [x] Task 5: Golden tests and checklist update (AC: #5)
|
|
- [x] 5.1 Added golden test fixtures: 222-225 (auto offset, over-constrained, clip, containing block chains)
|
|
- [x] 5.2 Verified all existing golden tests pass: 219 tests total including 052-055, 097, 100, 104, 168
|
|
- [x] 5.3 Updated `docs/CSS2.1_Implementation_Checklist.md` — checked off: fixed scroll behavior, auto resolution, clip rect
|
|
- [x] 5.4 `just ci` passes — also promoted WPT test `absolute-tables-016` from known_fail to pass
|
|
|
|
## Dev Notes
|
|
|
|
### Current Implementation Status
|
|
|
|
Positioning is **partially implemented**. What works:
|
|
- **`position: static`** — normal flow, fully working
|
|
- **`position: relative`** — offset via `apply_relative_offset()` in `positioning.rs:346-365`, left takes precedence over right, top over bottom
|
|
- **`position: absolute`** — two-phase layout via `calculate_absolute_layout()` + `process_deferred_absolutes()`, containing block resolution working
|
|
- **`position: fixed`** — uses viewport as containing block, basic paint works
|
|
- **`position: sticky`** — constraints computed via `StickyConstraints` struct, scroll-based offset
|
|
- **Deferred absolute layout** — children stored in `pending_absolutes`, processed after parent height is known
|
|
|
|
What is **missing or incomplete** (this story's scope):
|
|
- **Fixed positioning scroll behavior** — basic paint exists but scroll offset exemption may be incomplete
|
|
- **Auto offset resolution** — some edge cases in §10.3.7 and §10.6.4 may not be fully handled (over-constrained, direction:rtl)
|
|
- **`clip: rect(...)`** — NOT IMPLEMENTED (no parsing, no style field, no paint clipping)
|
|
- **Containing block edge cases** — padding-edge vs content-edge verification needed
|
|
|
|
### Key Code Locations
|
|
|
|
| Component | File | Key Functions/Lines |
|
|
|---|---|---|
|
|
| Position enum | `crates/style/src/types/primitives.rs:35-43` | `Position { Static, Relative, Absolute, Fixed, Sticky }` |
|
|
| Computed position props | `crates/style/src/types/computed.rs:83-89` | `position`, `top`, `right`, `bottom`, `left`, `z_index` |
|
|
| LayoutBox position state | `crates/layout/src/types.rs:222-231` | `position`, offsets, `render_offset_x/y`, `pending_absolutes` |
|
|
| Stacking context predicate | `crates/layout/src/types.rs:523-525` | `creates_stacking_context()` — positioned + z_index.is_some() |
|
|
| Containing block predicate | `crates/layout/src/types.rs:516-518` | `establishes_containing_block()` — position != Static |
|
|
| StickyConstraints | `crates/layout/src/types.rs:17-51` | normal_flow_y, containing_block_bottom, thresholds |
|
|
| Block layout dispatcher | `crates/layout/src/engine/block/layout.rs:49-74` | Position dispatch, containing block resolution for absolutes |
|
|
| Absolute layout | `crates/layout/src/engine/block/positioning.rs:168-312` | Width/height resolution, static position fallback, offset calc |
|
|
| Deferred absolutes | `crates/layout/src/engine/block/positioning.rs:316-342` | Process after parent height known, handle fixed via viewport |
|
|
| Relative offset | `crates/layout/src/engine/block/positioning.rs:346-365` | Apply render_offset for relative position |
|
|
| Sticky constraints | `crates/layout/src/engine/block/positioning.rs:370-399` | Post-layout sticky computation |
|
|
| Float + relative | `crates/layout/src/engine/block/positioning.rs:11-163` | Float layout with relative offset applied after |
|
|
| Display list painting | `crates/display_list/src/builder.rs:148-350` | `render_stacking_context()` — Appendix E paint order |
|
|
| Existing positioning tests | `crates/style/src/tests/positioning.rs:1-368` | Style computation tests for position, offsets, z-index |
|
|
| Existing layout tests | `crates/layout/src/engine/block/tests.rs:206-292` | Sticky, BFC establishment tests |
|
|
|
|
### Existing Golden Tests (Do NOT Regress)
|
|
|
|
| Fixture | Coverage |
|
|
|---|---|
|
|
| `052-position-relative.html` | Basic relative positioning |
|
|
| `053-position-relative-offset.html` | Relative with top/left offsets |
|
|
| `054-position-absolute.html` | Absolute positioning (viewport) |
|
|
| `055-absolute-in-relative.html` | Absolute inside relative container |
|
|
| `056-z-index-basic.html` | Z-index stacking |
|
|
| `097-position-fixed.html` | Basic fixed positioning |
|
|
| `100-fixed-nested.html` | Nested fixed elements |
|
|
| `104-fixed-with-scroll.html` | Fixed positioning with scroll |
|
|
| `168-bootstrap-fixed-top.html` | Real-world Bootstrap navbar fixed-top |
|
|
|
|
### Implementation Approach
|
|
|
|
**Task 1 (Fixed scroll behavior):**
|
|
The key question is whether the render/paint path correctly exempts fixed-position elements from scroll offset. Check `crates/display_list/src/builder.rs` and `crates/render/` for scroll offset application. Fixed elements should paint at their viewport-relative coordinates regardless of the document's scroll position. The layout side already uses viewport as containing block for fixed elements (`positioning.rs:56-60`).
|
|
|
|
**Task 2 (Auto offset resolution):**
|
|
The core function is `calculate_absolute_layout()` at `positioning.rs:168-312`. CSS 2.1 §10.3.7 defines 6 cases for horizontal auto resolution based on which of {left, width, right} are auto. The current implementation handles some but may miss:
|
|
- Over-constrained case (none are auto): ignore right for LTR
|
|
- Direction:rtl affecting which value is ignored when over-constrained
|
|
- Two autos case (width auto + left auto): shrink-to-fit + static position
|
|
|
|
Similarly §10.6.4 for vertical: if top/bottom/height has autos, resolve per spec rules.
|
|
|
|
**Task 3 (clip: rect()):**
|
|
Full pipeline implementation required:
|
|
1. Parse `clip: rect(top, right, bottom, left)` in CSS parser — note: comma-separated OR space-separated per CSS 2.1 §11.1.2
|
|
2. Add `ClipRect` struct and computed style field
|
|
3. Clip applies ONLY to absolutely positioned elements (absolute or fixed)
|
|
4. In display list builder, emit clip commands
|
|
5. In render, apply scissor rect during rasterization
|
|
|
|
**Task 4 (Containing block):**
|
|
Verify that `containing_block_for_absolutes` in `layout.rs` uses `.padding_box()` not `.content_box()`. The padding box of the nearest positioned ancestor is the containing block per CSS 2.1 §10.1. Check the computation at layout.rs where `abs_cb_for_children` is set.
|
|
|
|
### Architecture Constraints
|
|
|
|
- **Layer rule:** Changes span `layout` (Layer 1), `css` (Layer 1), `style` (Layer 1), `display_list` (Layer 1), `render` (Layer 1), `shared` (Layer 0) — horizontal deps within layers allowed
|
|
- **No unsafe:** All affected crates have `unsafe_code = "forbid"` (except potentially `render` which may delegate to `graphics`)
|
|
- **Pipeline order:** CSS parse → style → layout → display_list → render. Each phase reads previous, writes its own representation. No reaching back.
|
|
- **CSS Property Implementation Order:** Parse in `css/` → computed style in `style/` → layout effect in `layout/` → paint effect in `display_list/` → golden tests → update checklist → `just ci`
|
|
- **Arena IDs:** Layout boxes link back to DOM via `NodeId` — don't break this. Access DOM through `Document` arena.
|
|
|
|
### Previous Story Intelligence
|
|
|
|
**From Story 1.1 (Margin Collapsing):**
|
|
- Golden test infrastructure: fixtures in `tests/goldens/fixtures/`, expected in `tests/goldens/expected/`
|
|
- Unit tests pattern: construct DOM + style manually, run layout, assert positions
|
|
- Checklist update at `docs/CSS2.1_Implementation_Checklist.md` is mandatory
|
|
- `just ci` is the single validation gate (~1 minute, run once, capture output)
|
|
|
|
**From Story 1.2 (Stacking Contexts & Z-Index):**
|
|
- Display list builder (`crates/display_list/src/builder.rs`) is the paint order engine
|
|
- `render_stacking_context()` implements CSS 2.1 Appendix E step-by-step
|
|
- `collect_stacking_participants()` classifies elements into stacking/positioned-auto buckets
|
|
- Stable sort is required for equal z-index (tree order preserved)
|
|
- Changes to `creates_stacking_context()` must be coordinated with stacking context work
|
|
|
|
### Testing Strategy
|
|
|
|
1. **Unit tests** for clip parsing in `crates/css/src/` or `crates/style/src/tests/positioning.rs`
|
|
2. **Unit tests** for auto offset resolution in `crates/layout/src/tests/` — construct positioned boxes with various auto combinations, verify resolved positions
|
|
3. **Golden tests** in `tests/goldens/fixtures/` — check latest fixture number and use next sequential numbers. Each fixture gets `.layout.txt` and `.dl.txt` expected outputs.
|
|
4. **Regression verification** — run all existing positioning golden tests (052-055, 097, 100, 104, 168) to confirm no regression
|
|
5. **Regenerate goldens** after implementation: `cargo test -p rust_browser --test regen_goldens -- --nocapture`
|
|
6. Run `just ci` at the end — captures fmt + lint + test + policy in one pass
|
|
|
|
### CSS 2.1 Spec References
|
|
|
|
- **§9.3** — Positioning schemes: static, relative, absolute, fixed
|
|
- **§9.3.1** — Choosing a positioning scheme (`position` property)
|
|
- **§9.3.2** — Box offsets: `top`, `right`, `bottom`, `left`
|
|
- **§10.1** — Definition of containing block (padding edge of positioned ancestor)
|
|
- **§10.3.7** — Absolutely positioned, non-replaced elements: horizontal auto resolution (6 cases)
|
|
- **§10.6.4** — Absolutely positioned, non-replaced elements: vertical auto resolution
|
|
- **§11.1.2** — Clipping: `clip` property (applies to absolutely positioned elements)
|
|
- **§9.6.1** — Fixed positioning: containing block is viewport
|
|
|
|
### Project Structure Notes
|
|
|
|
- All changes within Layer 0/1 crates — no new crates or cross-layer violations
|
|
- New CSS property (`clip`) follows: parse in `css/` → computed in `style/` → propagate to `layout/` → paint in `display_list/` → render in `render/`
|
|
- New golden test fixtures go in `tests/goldens/fixtures/` with next available number
|
|
- Expected golden outputs go in `tests/goldens/expected/`
|
|
- Checklist update in `docs/CSS2.1_Implementation_Checklist.md`
|
|
|
|
### References
|
|
|
|
- [Source: crates/layout/src/engine/block/positioning.rs] — Core positioning layout (absolute, fixed, relative, sticky)
|
|
- [Source: crates/layout/src/engine/block/layout.rs#49-74] — Position dispatch and containing block resolution
|
|
- [Source: crates/layout/src/types.rs#StickyConstraints] — Sticky positioning constraints
|
|
- [Source: crates/layout/src/types.rs#LayoutBox] — Positioning state fields on layout boxes
|
|
- [Source: crates/style/src/types/computed.rs#83-89] — Position computed style fields
|
|
- [Source: crates/style/src/types/primitives.rs#Position] — Position enum definition
|
|
- [Source: crates/display_list/src/builder.rs#render_stacking_context] — Paint order for positioned elements
|
|
- [Source: crates/style/src/tests/positioning.rs] — Existing style computation tests
|
|
- [Source: crates/layout/src/engine/block/tests.rs] — Existing layout unit tests
|
|
- [Source: docs/CSS2.1_Implementation_Checklist.md#Phase-11] — Positioning implementation status
|
|
- [Source: _bmad-output/planning-artifacts/architecture.md#CSS-Property-Implementation-Order] — Feature implementation checklist
|
|
|
|
## Dev Agent Record
|
|
|
|
### Agent Model Used
|
|
|
|
Claude Opus 4.6 (1M context)
|
|
|
|
### Debug Log References
|
|
|
|
### Completion Notes List
|
|
|
|
- Task 1: Fixed positioning scroll behavior already correctly implemented. Added 2 unit tests verifying scroll immunity and viewport containing block.
|
|
- Task 2: Refactored `calculate_absolute_layout()` to properly handle CSS 2.1 §10.3.7 (horizontal) and §10.6.4 (vertical). Added auto-margin computation for abs-pos with both left/right or top/bottom specified. Extracted `offset_children()` helper. Added 7 unit tests, 3 golden tests.
|
|
- Task 3: Full pipeline `clip: rect(...)` implementation — CSS parsing (PropertyId::Clip, CssValue::ClipRect), ClipRect type, ComputedStyles field, LayoutBox field (only for abs/fixed per §11.1.2), display list PushClip/PopClip emission. Added 4 unit tests, 1 golden test.
|
|
- Task 4: Verified containing block uses padding box throughout. Added 2 unit tests (padding edge, nested chains), 1 golden test.
|
|
- Task 5: All golden tests pass (219 total). CSS checklist updated. `just ci` passes. Promoted 1 WPT test (`absolute-tables-016`) from known_fail to pass.
|
|
|
|
### Change Log
|
|
|
|
- 2026-03-13: Implemented positioning completeness (all 5 tasks). CSS 2.1 §10.3.7, §10.6.4, §11.1.2, §9.6.1 compliance.
|
|
|
|
### File List
|
|
|
|
- `crates/layout/src/engine/block/positioning.rs` — refactored `calculate_absolute_layout()` for §10.3.7/§10.6.4 completeness, added `offset_children()` helper
|
|
- `crates/layout/src/engine/block/tests.rs` — added 9 abs-pos/containing-block unit tests
|
|
- `crates/layout/src/types.rs` — added `clip: Option<ClipRect>` field to LayoutBox
|
|
- `crates/layout/src/engine/box_tree.rs` — wire clip from computed styles (only for abs/fixed)
|
|
- `crates/css/src/types.rs` — added `PropertyId::Clip`, `CssValue::ClipRect` variant
|
|
- `crates/css/src/parser/mod.rs` — added `parse_clip_value()` and `parse_clip_rect_args()`
|
|
- `crates/style/src/types/primitives.rs` — added `ClipRect` struct
|
|
- `crates/style/src/types/computed.rs` — added `clip` field, apply_declaration, reset_to_initial, copy_property_from_parent
|
|
- `crates/style/src/types/mod.rs` — export ClipRect
|
|
- `crates/style/src/lib.rs` — re-export ClipRect
|
|
- `crates/style/src/tests/positioning.rs` — added 4 clip unit tests
|
|
- `crates/display_list/src/builder.rs` — added PushClip/PopClip for CSS clip property
|
|
- `crates/display_list/src/tests/scroll_offset_tests.rs` — added 2 fixed-element scroll tests
|
|
- `tests/goldens.rs` — registered 5 new golden tests (222-226)
|
|
- `tests/goldens/fixtures/222-abspos-auto-offset-static.html` — new golden fixture
|
|
- `tests/goldens/fixtures/223-abspos-left-auto-right-specified.html` — new golden fixture
|
|
- `tests/goldens/fixtures/224-abspos-overconstrained.html` — new golden fixture
|
|
- `tests/goldens/fixtures/225-clip-rect.html` — new golden fixture
|
|
- `tests/goldens/fixtures/226-abspos-containing-block-chains.html` — new golden fixture
|
|
- `tests/goldens/expected/222-*.txt` — generated expected outputs
|
|
- `tests/goldens/expected/223-*.txt` — generated expected outputs
|
|
- `tests/goldens/expected/224-*.txt` — generated expected outputs
|
|
- `tests/goldens/expected/225-*.txt` — generated expected outputs
|
|
- `tests/goldens/expected/226-*.txt` — generated expected outputs
|
|
- `docs/CSS2.1_Implementation_Checklist.md` — updated positioning section
|
|
- `tests/external/wpt/wpt_manifest.toml` — promoted absolute-tables-016 to pass
|