Skip to content

Hoist out-of-flow (absolute/fixed) boxes to their containing block - #1170

Open
nicoburns wants to merge 6 commits into
devin/1787345886-position-static-fixedfrom
devin/1787748926-oof-hoisting-latebound
Open

Hoist out-of-flow (absolute/fixed) boxes to their containing block#1170
nicoburns wants to merge 6 commits into
devin/1787345886-position-static-fixedfrom
devin/1787748926-oof-hoisting-latebound

Conversation

@nicoburns

Copy link
Copy Markdown
Member

Objective

Implement browser-correct out-of-flow hoisting: position: absolute/fixed boxes are now laid out by their containing block (nearest positioned ancestor for absolute, the root for fixed) rather than always by their DOM parent, with Layout.location relative to that containing block. Stacked on #1140 (Position::Static/Fixed).

Architecture follows Blink's model (candidate bubbling + per-fragment persistence), as discussed in the design conversation.

This PR replaces #1141 (closed; branch preserved as backup/oof-hoisting-prelatebound). The only difference from #1141 is that static positions are now recorded late-bound (see below), addressing the lossy-encoding concern raised by the Servo compatibility analysis.

Context

Candidate bubbling (Blink's oof_positioned_candidates/oof_positioned_descendants)

  • New OofCandidate { node, order, position, static_position: Point<StaticPosition> } where the static position is recorded late-bound per axis:

    pub struct StaticPosition {
        pub area: Line<f32>,     // the alignment area (degenerate point for block layout)
        pub align: StaticAlign,
    }
    pub struct StaticAlign {
        pub keyword: StaticEdge,        // resolved physical Start | Center | End
        pub safety: AlignmentSafety,
        pub fallback: StaticEdge,       // used when `safe` alignment overflows
    }

    Emitters (block/flex/grid) resolve writing-mode/flex-direction/RTL down to a physical keyword but do not collapse the area to an anchor coordinate; the final offset is only computed once the box's final size and margins are known, by the new public helper:

    pub fn resolve_static_offset(
        static_position: Point<StaticPosition>,
        final_size: Size<f32>,
        resolved_margin: Rect<f32>,
    ) -> Point<f32>

    This keeps the raw per-axis start/end coordinates and alignment keywords available to consumers driving taffy's layout algorithms directly (Servo's PositioningContext-style late-bound resolution, future anchor positioning), instead of a lossy pre-resolved anchor.

  • Candidates live in LayoutOutput.oof_candidates: OofCandidates — lazy Option<Box<Vec<_>>> storage so the no-OOF case allocates nothing; lists are moved (take/append), not copied. Bubbling translates both area endpoints by the child's location.

  • Block/flex/grid emit their direct OOF children as candidates and merge candidates bubbled out of in-flow children's outputs. A shared containing-block pass, perform_oof_layout (new src/compute/oof.rs, refactored from the old per-algorithm absolute-layout code), claims candidates the current node is the containing block for, lays them out against its padding box, re-sweeps candidates surfaced by OOF children (fixed inside absolute), and passes unclaimed candidates up through its own LayoutOutput.

  • compute_root_layout runs the final pass: the root is the initial containing block for fixed and for absolute with no positioned ancestor.

  • Candidates are persisted in the layout cache (LayoutOutput is now Clone instead of Copy), so a cache hit at an intermediate node re-propagates hoisted descendants without descending into the skipped subtree — this is what keeps incremental layout working.

Containing-block-relative locations + rounding

  • A hoisted box's Layout.location is relative to its containing block's border box; scrollable overflow contributions likewise move to the containing block.
  • Each containing block records the boxes it positioned via new LayoutPartialTree::set_hoisted_children/add_hoisted_children; round_layout skips hoisted boxes when walking DOM children and instead recurses into them via new RoundTree::is_hoisted/hoisted_child_count/get_hoisted_child_id, so rounding accumulates offsets through the containing block (which the location is relative to) and each node is visited exactly once.

Test fixtures

The gentest fixture diff is large but mechanical: the test base stylesheet sets position: relative on all divs (matching taffy's old default), but the generator captured only the inline position style — so with static as the new default, generated tests silently disagreed with what Chrome actually laid out. The generator now records the computed position (and measures OOF boxes against their containing block), making every fixture's position: relative explicit. Hand-written tests in tests/hand_written/oof_hoisting.rs cover hoisting past static parents, containing-block-relative static positions per layout mode, fixed-to-root (including fixed inside absolute re-sweep), cache-hit re-propagation and invalidation, and rounding via the containing block; unit tests in src/compute/oof.rs cover resolve_static_offset (start/center/end, margins, safe fallback boundary, degenerate areas).

Feedback wanted

  • Grid-area resolution for absolute children of positioned grid containers currently still happens in-algorithm (the grid is its own containing block in that case, so behavior is correct); resolving grid areas at positioning time via DetailedGridInfo::resolve_absolute_grid_area (needed for non-direct descendants whose CB is a grid) is the next PR in the stack.
  • Position::Fixed currently only treats the root as its containing block; style-created fixed containing blocks (transforms etc.) are left to embedders (Blitz) via the trait layer.

Link to Devin session: https://dioxus.staging.devinenterprise.com/sessions/65c5463061c8403bade7888271d5eeb4
Open in Devin Desktop: https://dioxus.staging.devinenterprise.com/desktop/session/65c5463061c8403bade7888271d5eeb4?variant=devin-insiders
Requested by: @nicoburns

@staging-devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant