feat(layout): converge inline pipeline — relpos/sticky inline sub-flow (slice 3p-b)#276
Merged
Conversation
…w (slice 3p-b) Converge `position:relative`/`sticky` INLINE subtrees into the InlineFlow pipeline so render consumes their geometry in Layer 6 instead of the legacy single-line path. Layout keeps these inlines in the IFC (CSS 2 §9.4.3) while render paints their subtree in a separate stacking layer, so the IFC's members are partitioned into render-run-groups: the top-level group (Layer 5) and one per positioned-inline subtree (Layer 6 walk(root)). Each group persists its own InlineFlow keyed on its run-start (render's run[0]) — the top-level flow advances past the positioned inline (in-flow gap preserved) and a per-subtree sub-flow holds its members. Entirely in elidex-layout-block; elidex-ecs + elidex-render unchanged (render's existing run[0] consume gate handles top-level and sub-flow uniformly). - collect threads a group key; a relpos/sticky inline starts a sub-flow keyed on its first eligible child (first_eligible_child = render's run[0] rule: skip none/abspos/positioned, stop at the first block). Run-start realignment: the top-level key is the first eligible (non-positioned) child, not children.first(). - packer buckets per group; place_item routes by group key; coalescing is gated on a contiguity check (last_placed_entity) so a<span rel>b</span>c does not merge a and c across the gap. - multi-flow persist + set-based staleness reconcile (clear InlineFlow on every candidate key not persisted; candidates = raw direct children of every run-parent — a superset of all prior-pass keys). - gate narrowed: has_relpos_sticky -> has_relpos_sticky_atomic (relpos/sticky ATOMIC inlines still gate; their offset-preserving reposition is slice 3p-b-2). - anonymous-block-in-inline (a relpos inline with a direct block child) gets no sub-flow (render splits the run) — falls back to legacy, fail-safe. Correctness gains: the in-flow gap (CSS 2 §9.4.3) is preserved, and a relpos inline nested in a static inline is painted once (legacy double-painted it). Tests: 5 layout (sub-flow persist, run-start realignment, nested-no-double-paint, static-atomic-in-subflow reposition, F9 staleness) + 1 render (end-to-end gap + single paint). Per-crate green; no shell golden churn. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pre-push gate (/code-review xhigh + /simplify + /review): - /code-review (correctness): `first_eligible_child` must mirror render's run[0] EXACTLY. It was over-skipping `display:none` elements and non-styled (comment) nodes, but render's run builder skips only `is_positioned` and pushes everything else (incl. display:none/comments) — so a leading display:none/comment child made layout key the InlineFlow on a different entity than render's run[0], silently dropping the converged path to legacy. Old `children.first()` matched render; the realignment broke it. Fixed: skip only `position != Static`, return None at a block-level child, return everything else. - /code-review (correctness, narrow): a `writing-mode` set on a relpos/sticky inline would make render read the sub-flow's axis off the span while layout projected with the IFC root's axis → transposed sub-flow. Gate: no sub-flow when the positioned inline's writing mode differs from the root's (falls back to legacy; aligns with the unimplemented CSS WM 4 §3.2 blockify). Threads `root_horizontal`. - /simplify (altitude / One-issue-one-way): extracted the two `group_key = None` gates (direct-block-child, WM-mismatch) into `positioned_subflow_key` — one named "not single-linear-representable → legacy" boundary so future gates land in one place. Tests: +leading-display:none key agreement, +WM-override gate, +matching-WM vertical sub-flow. Layout 292 / render 130 green; clippy -D warnings clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/elidex-review (5-axis, 0 CRIT / 0 IMP / 3 MIN): - Axis 2 (ECS data-flow): `positioned_subflow_key` walked the RAW composed_children for the sub-flow key + block-split check, but render's `walk(span)` iterates `composed_children_flat` (flattens display:contents). A display:contents first child of a relpos inline keyed the sub-flow on the wrong entity (→ fail-safe legacy fallback), and a block nested inside display:contents was missed by the block-split gate (→ would over-collect into a sub-flow render then splits = double-paint). Fix: key + gate on composed_children_flat, exactly mirroring render's run[0] (completes the helper's documented "mirror render verbatim" invariant). +regression test. - Axis 5 (convention): stale `has_relpos_sticky` reference in a test assert message string (missed in the field rename) → `has_relpos_sticky_atomic`. Deferred (MIN, justified): inline/mod.rs crossed 1000 lines (1035) — the growth is the one cohesive, indivisible sub-flow mechanism (plan §5.2); a submodule extraction is a separate sweep, not warranted mid-slice. Layout 293 / render 130 green; clippy -D warnings clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR advances the render↔layout inline-pipeline convergence by persisting position:relative / position:sticky inline (non-atomic) subtrees as separate InlineFlow sub-flows, keyed to the same run[0] entities that render uses when painting the corresponding Layer-6 walk(span) runs. Layout keeps the positioned inline in-flow (preserving the IFC gap) while enabling render to consume per-subtree geometry without double-paint.
Changes:
- Extend inline collection/persistence to partition a single IFC into render-run-groups (top-level + one per positioned-inline subtree), including run-start realignment via
first_eligible_childand sub-flow gating viapositioned_subflow_key. - Update the line packer to bucket persisted
InlineFlowmembers per group key and prevent coalescing across sub-flow gaps (new contiguity tracking). - Add targeted layout and render tests covering sub-flow persistence, gap preservation, run-start agreement with render, and staleness clearing.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| crates/layout/elidex-layout-block/src/inline/tests/mod.rs | Adjusts tuple destructuring for updated collect_inline_items return shape. |
| crates/layout/elidex-layout-block/src/inline/tests/inline_flow.rs | Adds/updates layout tests validating relpos inline sub-flow persistence, gap behavior, key realignment, and staleness clearing. |
| crates/layout/elidex-layout-block/src/inline/pack.rs | Buckets per-line InlineFlowRuns by render-run-group key and adds contiguity tracking to avoid merging across positioned gaps. |
| crates/layout/elidex-layout-block/src/inline/mod.rs | Introduces group-key plumbing, run-start/key derivation to mirror render (first_eligible_child), sub-flow gating, multi-flow reconciliation, and renames the relpos/sticky gate to atomic-only. |
| crates/layout/elidex-layout-block/src/inline/measure.rs | Updates callers for the expanded collect_inline_items tuple return. |
| crates/layout/elidex-layout-block/src/inline/atomic.rs | Updates pattern match to ignore the new atomic group_key field. |
| crates/core/elidex-render/src/builder/tests/inline_flow.rs | Adds an end-to-end render consumption test asserting gap preservation + single paint with relpos inline sub-flow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…sibling module Copilot R1 (1 MINOR): inline/tests/inline_flow.rs exceeded the repo's ~1000-line convention (1289 lines) after the slice-3p-b additions. Extract the 9 relpos/sticky sub-flow tests + the append_relpos_span helper into a new sibling test module inline/tests/relpos_subflow.rs (registered in tests/mod.rs), mirroring the existing topic-split modules (baseline / inline_flow / text_height). `env` made pub(super) so the sibling reuses it. inline_flow.rs 1289 → 724 lines. Test-only move (no behavior change); 293 layout tests pass, clippy -D warnings + fmt clean, mise run ci green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t scan helpers Copilot R2 (2 MINOR, perf): first_eligible_child and has_direct_block_child used crate::try_get_style (which clones the whole ComputedStyle) per scanned child, when they only read position/display in a per-child loop on the inline-collect path. Switch to a borrowed dom.world().get::<&ComputedStyle>() read — no per-child clone. Behavior-equivalent (293 layout tests unchanged); clippy -D warnings + mise run ci green. (The wholesale try_get_style-clones-ComputedStyle tax across layout remains a separate orthogonal task; only the two new scan helpers are addressed here.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Slice 3p-b of the render↔layout inline-pipeline convergence (One-issue-one-way). Master plan: render↔layout inline-pipeline convergence §6.
What
Converges
position:relative/stickyinline subtrees into theInlineFlowgeometry pipeline. Layout keeps these inlines in the IFC (in-flow, advancing the line cursor — CSS 2 §9.4.3) while render paints their subtree in a separate stacking layer (Layer 6walk(span)). So the IFC's inline-level members are partitioned into render-run-groups: the top-level group (Layer 5) + one per-positioned-subtree sub-flow (Layer 6). Each group persists its ownInlineFlowkeyed on its run-start — render'srun[0]for theemit_inline_runthat paints it. The parent flow advances past the positioned inline (in-flow gap preserved) but records it in the sub-flow.Entirely a layout change —
elidex-ecs(InlineFlow) andelidex-renderare unchanged: render's existingrun[0]consume gate reads the sub-flow offwalk(span)'srun[0]automatically. Render is a read-only beneficiary.Correctness gains (CSS 2 §9.4.3)
<div>a<em>x<span style=position:relative>b</span>y</em>c</div>) was flattened into the Layer-5 run by legacycollect_styled_inline_textand painted again by Layer-6walk(span)— painted twice. The sub-flow routes it to Layer 6 only.Mechanism
first_eligible_child, which mirrors render'spaint_non_sc/Layer-5run[0]rule exactly, overcomposed_children_flat). Run-start realignment: the top-level key is the first eligible child, notchildren.first().a<span rel>b</span>cdoes not mergea/cacross the gap.InlineFlowon every candidate key not persisted; candidates = a superset of all prior-pass keys).has_relpos_sticky→has_relpos_sticky_atomic: relpos/sticky atomic inlines stay gated (offset-preserving reposition is a different mechanism, deferred to slice 3p-b-2).positioned_subflow_keyreturnsNone(→ render legacy, fail-safe) for the two non-single-linear-representable cases: a direct block child (anonymous-block-in-inline) or a writing-mode differing from the IFC root (would transpose).Tests
11 new layout tests (sub-flow persist + gap, run-start realignment, nested-no-double-paint, static-atomic-in-subflow reposition, F9 staleness, leading-
display:nonekey agreement, WM-override gate, matching-WM vertical sub-flow,display:contentskey flatten) + 1 render end-to-end test (gap + single paint). No shell golden churn.Gate
/elidex-plan-review(0C) →cargo fmt→mise run ci(10,682 tests) →/code-review(xhigh) →/simplify→/review→/elidex-review(5-axis, 0C/0IMP). The gate caught and fixed three further regressions:display:none/comment keying divergence, a WM-on-relpos transposition, anddisplay:contentskeying + a latent block-in-display:contentsdouble-paint.Cap-neutral (One-issue-one-way + CSS 2 §9.4.3 conformance/correctness fix, no new web-platform feature).
🤖 Generated with Claude Code