Files
rust_browser/_bmad-output/implementation-artifacts/1-3-positioning-completeness.md
Zachary D. Rowitsch 327e31795b Implement positioning completeness (CSS 2.1 §10.3.7, §10.6.4, §11.1.2, §9.6.1)
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>
2026-03-13 20:20:08 -04:00

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