Root element now creates a stacking context unconditionally. Refactored display list builder to correctly bucket stacking participants into three groups (negative/zero/positive z-index) per Appendix E steps 2/6/7. Fixed z-index:0 elements painting at step 7 instead of step 6, and descendant stacking contexts being trapped inside positioned z-index:auto ancestors instead of participating in the parent stacking context. Added StackingBuckets struct, unified step-6 rendering with tree-order merge by node_id, and 7 unit tests for creates_stacking_context(). Added 6 golden tests (213-218) and 4 integration tests asserting paint-order invariants programmatically. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
209 lines
16 KiB
Markdown
209 lines
16 KiB
Markdown
# Story 1.2: Stacking Contexts & Z-Index
|
|
|
|
Status: done
|
|
|
|
## Story
|
|
|
|
As a web user,
|
|
I want overlapping elements to render in the correct front-to-back order,
|
|
so that menus, modals, and layered content display correctly.
|
|
|
|
## Acceptance Criteria
|
|
|
|
1. **Given** an element with `position: relative/absolute/fixed` and a `z-index` value other than `auto`, **When** the page is rendered, **Then** a new stacking context is created per CSS 2.1 §9.9.1
|
|
2. **Given** elements within the same stacking context, **When** the page is rendered, **Then** painting order follows CSS 2.1 §E.2: backgrounds/borders → negative z-index → block flow → floats → inline flow → z-index 0/auto → positive z-index
|
|
3. **Given** nested stacking contexts, **When** the page is rendered, **Then** child stacking contexts are painted atomically within their parent's z-order position
|
|
4. Golden tests cover overlapping positioned elements with various z-index values, checklist is updated, and `just ci` passes
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [x] Task 1: Root element stacking context (AC: #1, #2)
|
|
- [x] 1.1 Update `creates_stacking_context()` in `crates/layout/src/types.rs` (~L523-525) to return `true` for the root element (NodeId(0) or document root), regardless of positioning/z-index
|
|
- [x] 1.2 Unit test confirming root element creates stacking context without positioning
|
|
|
|
- [x] Task 2: Verify and fix Appendix E paint order completeness (AC: #2)
|
|
- [x] 2.1 Audit `render_stacking_context()` in `crates/display_list/src/builder.rs` (L148-350) against CSS 2.1 §E.2 step-by-step
|
|
- [x] 2.2 Verify Step 2 (negative z-index descendants) sorts and renders correctly — infrastructure exists but checklist shows incomplete
|
|
- [x] 2.3 Verify Step 6 (positioned descendants with `z-index: auto`) renders in tree order within their stacking context
|
|
- [x] 2.4 Verify stable sort preserves tree order for elements with equal z-index values
|
|
- [x] 2.5 Fix any deviations found in the audit
|
|
- [x] 2.6 Golden tests for each Appendix E step: negative z-index, block flow behind positioned, floats between blocks and inlines, positioned-auto tree order
|
|
|
|
- [x] Task 3: Atomic painting of nested stacking contexts (AC: #3)
|
|
- [x] 3.1 Verify that child stacking contexts paint atomically — all descendants within a stacking context paint together before the next stacking context at the same level
|
|
- [x] 3.2 Test nested stacking contexts: parent z-index:1 with children at various z-indices should all paint within parent's z-order slot
|
|
- [x] 3.3 Golden test with 3+ levels of nesting to confirm atomicity
|
|
|
|
- [x] Task 4: Edge cases and correctness (AC: #1, #2, #3)
|
|
- [x] 4.1 Verify `position: relative` + `z-index: 0` creates a stacking context (z-index is not auto)
|
|
- [x] 4.2 Verify `position: relative` + `z-index: auto` does NOT create a stacking context (per §9.9.1)
|
|
- [x] 4.3 Verify `position: static` + `z-index` value has no effect (z-index only applies to positioned elements per §9.5.1)
|
|
- [x] 4.4 Test interleaved positioned and non-positioned siblings — non-positioned paints at Step 3/4/5, positioned paints at Step 6/7
|
|
- [x] 4.5 Unit tests for `creates_stacking_context()` covering all position/z-index combinations
|
|
|
|
- [x] Task 5: Golden tests and checklist update (AC: #4)
|
|
- [x] 5.1 Add golden test fixtures covering: nested stacking contexts with mixed z-indices, negative z-index behind parent background, positioned-auto tree order, interleaved positioned/non-positioned
|
|
- [x] 5.2 Update `docs/CSS2.1_Implementation_Checklist.md` — check off stacking context creation rules and z-index stacking contexts
|
|
- [x] 5.3 Run `just ci` and ensure all tests pass
|
|
|
|
## Dev Notes
|
|
|
|
### Current Implementation Status
|
|
|
|
Stacking contexts and z-index are **partially implemented**. What works:
|
|
- **Z-index parsing** — `z-index` property parsed in CSS, stored as `Option<i32>` in `ComputedStyles` and `LayoutBox`
|
|
- **Stacking context creation** — positioned elements with explicit z-index create stacking contexts
|
|
- **Positive z-index sorting** (Step 7) — children sorted ascending and rendered
|
|
- **Appendix E steps 1, 3, 4, 5** — background/borders, block flow, floats, inlines all render in order
|
|
- **Existing golden tests** — 9 golden tests already cover basic z-index scenarios (056, 057, 058, 177, 178, 179, 123, 192, 194)
|
|
|
|
What needs **verification and fixing** (this story's scope):
|
|
- **Root element stacking context** — `creates_stacking_context()` does NOT handle root element
|
|
- **Negative z-index (Step 2)** — infrastructure exists but checklist shows incomplete
|
|
- **Positioned-auto (Step 6)** — renders but tree-order correctness unverified
|
|
- **Stable sort / tree order** — equal z-index elements must paint in tree order
|
|
- **Nested atomicity** — needs verification that nested contexts paint atomically
|
|
|
|
### Key Code Locations
|
|
|
|
| Component | File | Key Functions/Lines |
|
|
|---|---|---|
|
|
| Stacking context creation | `crates/layout/src/types.rs:523-525` | `creates_stacking_context()` — currently `is_positioned() && z_index.is_some()` |
|
|
| is_positioned() | `crates/layout/src/types.rs:510-512` | Returns `position != Static` |
|
|
| z_index field | `crates/layout/src/types.rs:228` | `z_index: Option<i32>` on LayoutBox |
|
|
| Paint order algorithm | `crates/display_list/src/builder.rs:148-350` | `render_stacking_context()` — CSS 2.1 Appendix E implementation |
|
|
| StackingParticipant | `crates/display_list/src/builder.rs:13-20` | Struct collecting positioned/stacking descendants |
|
|
| RenderPhase enum | `crates/display_list/src/builder.rs:53-62` | Controls phased rendering for Appendix E |
|
|
| Participant collection | `crates/display_list/src/builder.rs:529-598` | `collect_stacking_participants()` — recursive tree walk |
|
|
| Z-index sorting | `crates/display_list/src/builder.rs:236-249` | Stable sort of negative/positive children |
|
|
| Z-index computed style | `crates/style/src/types/computed.rs:89` | `z_index: Option<i32>` — None = auto |
|
|
| Positioned layout | `crates/layout/src/engine/block/positioning.rs` | Absolute/fixed/relative layout calculations |
|
|
| Existing golden tests | `tests/goldens/expected/056-*.txt`, `057-*`, `058-*`, `177-*`, `178-*`, `179-*` | Basic z-index, negative, stacking, deep nesting, tree order, overflow clip |
|
|
|
|
### Implementation Approach
|
|
|
|
**Task 1 (Root stacking context):**
|
|
Modify `creates_stacking_context()` in `crates/layout/src/types.rs`. Add a check: if this LayoutBox represents the root element (check `node_id == NodeId(0)` or parent is document), return `true` unconditionally. The root element always creates a stacking context per CSS 2.1 §9.9.1.
|
|
|
|
**Task 2 (Appendix E audit):**
|
|
Walk through `render_stacking_context()` in `builder.rs` line by line against the spec:
|
|
- Step 1 (L178-186): Background, borders — likely correct
|
|
- Step 2 (L251-257): Negative z-index — verify sorting is ascending (most negative first) and rendering is recursive
|
|
- Step 3 (L259-280): Block descendants BG/borders only — verify `RenderPhase::BackgroundsOnly`
|
|
- Step 4 (L282-302): Floats — verify they paint as mini stacking contexts
|
|
- Step 5 (L304-310): Inline content — verify inline formatting context rendering
|
|
- Step 6 (L312-332): Positioned-auto — verify tree-order rendering for `z-index: auto` positioned elements
|
|
- Step 7 (L334-340): Positive z-index — verify ascending sort
|
|
|
|
Key question: Does `collect_stacking_participants()` correctly distinguish between "creates stacking context" (goes into negative/positive buckets) vs "positioned-auto" (goes into auto bucket)?
|
|
|
|
**Task 3 (Nested atomicity):**
|
|
This should work by design — each stacking context calls `render_stacking_context()` recursively, which renders all its descendants before returning. Verify with a golden test showing a z-index:1 parent with z-index:999 child does NOT paint above a z-index:2 sibling.
|
|
|
|
### Architecture Constraints
|
|
|
|
- **Layer rule:** Changes span `layout` (Layer 1) and `display_list` (Layer 1) — both at same layer, horizontal deps allowed
|
|
- **No unsafe:** Both crates have `unsafe_code = "forbid"`
|
|
- **Pipeline order:** Display list building reads layout tree (after layout phase). Do not modify layout boxes during display list building.
|
|
- **CSS Property Implementation Order:** This story is primarily display_list (paint) with a small layout touch. Follow: verify parsing → verify style computation → verify layout propagation → fix display list painting → tests → docs.
|
|
|
|
### Previous Story Intelligence (Story 1.1)
|
|
|
|
From Story 1.1 (Margin Collapsing):
|
|
- Golden test infrastructure is well-established: fixtures in `tests/goldens/fixtures/`, expected in `tests/goldens/expected/`
|
|
- Unit tests follow the pattern of constructing DOM + style manually, running layout, asserting positions
|
|
- CSS 2.1 checklist at `docs/CSS2.1_Implementation_Checklist.md` must be updated
|
|
- `just ci` is the single validation gate
|
|
|
|
### Testing Strategy
|
|
|
|
1. **Unit tests** for `creates_stacking_context()` — add tests in `crates/layout/src/tests/` or alongside existing positioning tests in `crates/style/src/tests/positioning.rs`
|
|
2. **Golden tests** — add new fixtures (check latest number in `tests/goldens/fixtures/`). Key scenarios:
|
|
- Nested stacking contexts with mixed positive/negative z-indices
|
|
- Root element as stacking context
|
|
- Positioned-auto elements in tree order
|
|
- Interleaved positioned + non-positioned siblings
|
|
- Atomicity test: child z-index:999 inside parent z-index:1 must NOT paint above sibling z-index:2
|
|
3. **Verify existing golden tests still pass** — 9 existing z-index goldens should not regress
|
|
4. Run `just ci` at the end
|
|
|
|
### CSS 2.1 Spec References
|
|
|
|
- **§9.9.1** — Stacking context creation rules: positioned elements with z-index != auto, root element
|
|
- **§E.2** — Painting order within a stacking context (Appendix E): 7-step algorithm
|
|
- **§9.5.1** — z-index only applies to positioned elements
|
|
- **§9.3** — Positioning schemes: static, relative, absolute, fixed
|
|
|
|
### WPT Impact
|
|
|
|
Stacking context and z-index tests are spread across WPT `css2-visuren` and `css2-zindex` categories. Fixing root stacking context and verifying Appendix E compliance should improve WPT pass rates in these areas.
|
|
|
|
### Project Structure Notes
|
|
|
|
- Changes primarily in `crates/display_list/src/builder.rs` (paint order) and `crates/layout/src/types.rs` (stacking context predicate)
|
|
- No new crates or cross-layer changes
|
|
- 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/display_list/src/builder.rs#render_stacking_context] — Core paint order algorithm (Appendix E)
|
|
- [Source: crates/display_list/src/builder.rs#collect_stacking_participants] — Stacking participant collection
|
|
- [Source: crates/layout/src/types.rs#creates_stacking_context] — Stacking context creation predicate
|
|
- [Source: crates/layout/src/types.rs#is_positioned] — Position check helper
|
|
- [Source: crates/style/src/types/computed.rs] — z_index computed style field
|
|
- [Source: tests/goldens/expected/056-z-index-basic.*] — Existing basic z-index golden tests
|
|
- [Source: tests/goldens/expected/058-stacking-context.*] — Existing stacking context golden tests
|
|
- [Source: tests/goldens/expected/177-z-index-deep-stacking.*] — Deep nesting golden tests
|
|
- [Source: docs/CSS2.1_Implementation_Checklist.md] — Implementation status checklist
|
|
- [Source: _bmad-output/planning-artifacts/architecture.md#CSS-Property-Implementation-Order] — CSS feature implementation checklist
|
|
|
|
## Dev Agent Record
|
|
|
|
### Agent Model Used
|
|
|
|
Claude Opus 4.6 (1M context)
|
|
|
|
### Debug Log References
|
|
|
|
None required — implementation was straightforward with no blocking issues.
|
|
|
|
### Completion Notes List
|
|
|
|
- **Task 1:** Added `is_root_element` check to `creates_stacking_context()` in `crates/layout/src/types.rs`. The existing `is_root_element` field (from story 1.1) was leveraged instead of checking NodeId(0).
|
|
- **Task 2:** Audit of `render_stacking_context()` found two bugs: (1) z-index:0 stacking contexts were in the step-7 bucket instead of step-6, (2) descendant stacking contexts under positioned z-index:auto ancestors were trapped instead of participating in the parent stacking context. Both fixed by refactoring `collect_stacking_participants` into three buckets (negative/zero/positive) with a `StackingBuckets` struct, recursing into positioned-auto ancestors, and merging all step-6 participants by node_id for tree-order rendering. Golden tests 218 and 219 exercise both fixed scenarios.
|
|
- **Task 3:** Verified atomic painting of nested stacking contexts. The recursive `render_stacking_context()` design inherently ensures atomicity — all descendants render before returning. Confirmed with golden test 216 (z:999 child inside z:1 parent paints before z:2 sibling).
|
|
- **Task 4:** Added 7 unit tests for `creates_stacking_context()` covering all position/z-index combinations including root element, positioned+z-index, positioned+auto, static+z-index.
|
|
- **Task 5:** Added 7 new golden tests (213-219), updated CSS2.1 checklist, registered 4 pre-existing margin collapse golden tests (209-212). `just ci` passes with 0 failures.
|
|
|
|
### Change Log
|
|
|
|
- 2026-03-13: Implemented stacking contexts and z-index compliance (Story 1.2). Root element now creates stacking context per CSS 2.1 §9.9.1. Fixed z-index:0 step-6 bucketing and descendant SC hoisting through positioned-auto ancestors. Added 7 unit tests and 7 golden tests.
|
|
- 2026-03-13: Code review fixes — updated stale comments (steps 2/7 → 2/6/7), added NodeId monotonicity invariant comment, updated `collect_stacking_participants` doc comment for three-bucket design, clarified `owns_stacking_context` relationship to unconditional SC skip, added golden test 220 exercising all three buckets (negative+zero+positive) in one container.
|
|
|
|
### File List
|
|
|
|
- `crates/layout/src/types.rs` — Modified `creates_stacking_context()` to include root element check; added 7 unit tests
|
|
- `crates/display_list/src/builder.rs` — Added `StackingBuckets` struct; refactored `collect_stacking_participants` to three-bucket collection; fixed z-index:0 to step 6; recurse into positioned-auto ancestors; unified step-6 rendering with tree-order merge; `render_stacking_context` skips SC collection for non-SC boxes
|
|
- `tests/goldens.rs` — Added golden test functions for 209-219
|
|
- `tests/goldens/fixtures/213-z-index-negative.html` — New golden fixture
|
|
- `tests/goldens/fixtures/214-z-index-positioned-auto-tree-order.html` — New golden fixture
|
|
- `tests/goldens/fixtures/215-z-index-interleaved-positioned.html` — New golden fixture
|
|
- `tests/goldens/fixtures/216-z-index-nested-atomicity.html` — New golden fixture
|
|
- `tests/goldens/fixtures/217-z-index-three-level-nesting.html` — New golden fixture
|
|
- `tests/goldens/fixtures/218-z-index-zero-step6.html` — New golden fixture (z-index:0 at step 6)
|
|
- `tests/goldens/fixtures/219-z-index-auto-ancestor-descendant-sc.html` — New golden fixture (SC hoisting through auto ancestor)
|
|
- `tests/goldens/expected/213-z-index-negative.{layout,dl}.txt` — New golden expected
|
|
- `tests/goldens/expected/214-z-index-positioned-auto-tree-order.{layout,dl}.txt` — New golden expected
|
|
- `tests/goldens/expected/215-z-index-interleaved-positioned.{layout,dl}.txt` — New golden expected
|
|
- `tests/goldens/expected/216-z-index-nested-atomicity.{layout,dl}.txt` — New golden expected
|
|
- `tests/goldens/expected/217-z-index-three-level-nesting.{layout,dl}.txt` — New golden expected
|
|
- `tests/goldens/expected/218-z-index-zero-step6.{layout,dl}.txt` — New golden expected
|
|
- `tests/goldens/expected/219-z-index-auto-ancestor-descendant-sc.{layout,dl}.txt` — New golden expected
|
|
- `tests/goldens/expected/211-margin-collapse-nested-chain.layout.txt` — Updated (stale NodeIds)
|
|
- `docs/CSS2.1_Implementation_Checklist.md` — Checked off stacking context and z-index items
|
|
- `tests/goldens/fixtures/220-z-index-three-buckets.html` — New golden fixture (all three buckets)
|
|
- `tests/goldens/expected/220-z-index-three-buckets.{layout,dl}.txt` — New golden expected
|
|
- `_bmad-output/implementation-artifacts/sprint-status.yaml` — Updated story status
|