Skip to content

feat(layout): converge inline pipeline — relpos/sticky inline sub-flow (slice 3p-b)#276

Merged
send merged 5 commits into
mainfrom
feat/inline-pipeline-slice3pb
Jun 3, 2026
Merged

feat(layout): converge inline pipeline — relpos/sticky inline sub-flow (slice 3p-b)#276
send merged 5 commits into
mainfrom
feat/inline-pipeline-slice3pb

Conversation

@send

@send send commented Jun 3, 2026

Copy link
Copy Markdown
Owner

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/sticky inline subtrees into the InlineFlow geometry 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 6 walk(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 own InlineFlow keyed on its run-start — render's run[0] for the emit_inline_run that paints it. The parent flow advances past the positioned inline (in-flow gap preserved) but records it in the sub-flow.

Entirely a layout changeelidex-ecs (InlineFlow) and elidex-render are unchanged: render's existing run[0] consume gate reads the sub-flow off walk(span)'s run[0] automatically. Render is a read-only beneficiary.

Correctness gains (CSS 2 §9.4.3)

  • In-flow gap preserved. The legacy single-linear-pass painted the surrounding text contiguous (no gap where the positioned inline sits) → the positioned inline overlapped the following text. The sub-flow leaves the gap.
  • Double-paint eliminated. A relpos inline nested in a static inline (<div>a<em>x<span style=position:relative>b</span>y</em>c</div>) was flattened into the Layer-5 run by legacy collect_styled_inline_text and painted again by Layer-6 walk(span) — painted twice. The sub-flow routes it to Layer 6 only.

Mechanism

  • collect threads a render-run-group key; a relpos/sticky inline starts a sub-flow keyed on its first eligible child (first_eligible_child, which mirrors render's paint_non_sc/Layer-5 run[0] rule exactly, over composed_children_flat). Run-start realignment: the top-level key is the first eligible child, not children.first().
  • packer buckets members per group; coalescing is gated on a contiguity check so a<span rel>b</span>c does not merge a/c across the gap.
  • multi-flow persist + set-based staleness reconcile (clear InlineFlow on every candidate key not persisted; candidates = a superset of all prior-pass keys).
  • gate narrowed has_relpos_stickyhas_relpos_sticky_atomic: relpos/sticky atomic inlines stay gated (offset-preserving reposition is a different mechanism, deferred to slice 3p-b-2). positioned_subflow_key returns None (→ 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:none key agreement, WM-override gate, matching-WM vertical sub-flow, display:contents key flatten) + 1 render end-to-end test (gap + single paint). No shell golden churn.

Gate

/elidex-plan-review (0C) → cargo fmtmise 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, and display:contents keying + a latent block-in-display:contents double-paint.

Cap-neutral (One-issue-one-way + CSS 2 §9.4.3 conformance/correctness fix, no new web-platform feature).

🤖 Generated with Claude Code

send and others added 3 commits June 3, 2026 21:29
…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>
Copilot AI review requested due to automatic review settings June 3, 2026 13:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_child and sub-flow gating via positioned_subflow_key.
  • Update the line packer to bucket persisted InlineFlow members 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.

Comment thread crates/layout/elidex-layout-block/src/inline/tests/inline_flow.rs Outdated
…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment thread crates/layout/elidex-layout-block/src/inline/mod.rs
Comment thread crates/layout/elidex-layout-block/src/inline/mod.rs
…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>
Copilot AI review requested due to automatic review settings June 3, 2026 14:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@send send merged commit 0d85bb5 into main Jun 3, 2026
7 checks passed
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.

2 participants