Skip to content

feat(layout,render): converge inline pipeline — layout persists InlineFlow, render consumes (slice 1)#270

Merged
send merged 3 commits into
mainfrom
feat/inline-pipeline-convergence
Jun 1, 2026
Merged

feat(layout,render): converge inline pipeline — layout persists InlineFlow, render consumes (slice 1)#270
send merged 3 commits into
mainfrom
feat/inline-pipeline-convergence

Conversation

@send
Copy link
Copy Markdown
Owner

@send send commented Jun 1, 2026

Why

elidex-render carried a parallel inline-text pipeline that re-collected, re-collapsed, re-measured and re-positioned DOM text independently of elidex-layout-block. Crucially, render's pipeline did no line-breaking — a single fixed baseline (emit_styled_segments is literally "a single linear pass") — so wrapped multi-line text, multi-run blocks, and per-line text-align rendered incorrectly (layout sized the box right; render painted the text on one overlapping line). This is the first slice of unifying the two pipelines (One-issue-one-way), and it is a correctness fix, not only dedup.

What

Layout's LinePacker now persists collapsed + positioned line runs as an InlineFlow ECS component (in elidex-ecs, where Entity lives) keyed on the run-start entity. Render consumes it (emit_inline_flow, gated inside emit_inline_run) instead of re-walking the DOM — honouring layout's per-line line-breaking and per-line text-align.

Slice 1 scope = horizontal LTR plain text. A run is gated out of persistence (keeping render's legacy path) when it contains any layout-IFC-vs-render divergence: pseudo/generated content, a relative/sticky positioned inline, an atomic inline (inline-block/-flex/-grid/-table), bidi (RTL) text, text-transform, vertical writing mode, text-align: justify, or fragmentation (paged). Each is deferred to a named later slice (vertical → 2; counters/pseudo → 3; positioned+atomic → 3p; bidi + legacy-path deletion → 4).

Staleness is reconciled by an explicit insert-or-remove per layout passlayout_generation is constant 0 off the paged path, so it cannot signal staleness.

Also unifies render's two inline-run groupings: paint_stacking_context_layers Layer 5 now skips positioned children without splitting the run (matching paint_non_sc + layout), fixing an interspersed-abspos double-paint and a pre-existing overlap bug.

Key files

  • elidex-ecs: InlineFlow / InlineFlowLine / InlineFlowRun components
  • elidex-bidi: text_has_rtl (allocation-free conservative bidi-class scan)
  • LinePacker: record per-line positioned runs (commit-on-content seam, same-entity coalescing) + bake per-line text-align into inline_start
  • collect_inline_itemsRunComplexity gate flags
  • render: shared emit_text_segment + new emit_inline_flow; gate at emit_inline_run

Tests

14 layout (single/multi-line/coalesce/abs-coords/center/8 gates/stale-clear) + 3 render (consume-per-line / fallback / interspersed-abspos no-double-paint) + 1 elidex-bidi (text_has_rtl). mise run ci green; elidex-shell full-pipeline 105 pass (no golden churn — single-line output unchanged; multi-line was previously untested = silently wrong).

Review

Pre-push gate run in full: /code-review (3 gate gaps found+fixed: bidi/text-transform/fragmentation), /simplify (allocation-free text_has_rtl), /review, /elidex-review (Layer-5 unification for interspersed-abspos + a §-citation fix). Plan + 2-round /elidex-plan-review preceded implementation.

🤖 Generated with Claude Code

send and others added 3 commits June 2, 2026 01:31
…eFlow, render consumes (slice 1)

Slice 1 of the render↔layout inline-pipeline convergence (One-issue-one-way).
elidex-render carried a *parallel* inline pipeline that re-collected, re-collapsed,
re-measured and re-positioned DOM text independently of elidex-layout-block — and,
critically, did **no line-breaking** (a single fixed baseline), so wrapped
multi-line text, multi-run blocks and per-line text-align rendered incorrectly.

Layout now persists collapsed + positioned line runs as an `InlineFlow` ECS
component (elidex-ecs) keyed on the run-start entity; render consumes it instead
of re-walking the DOM. This is a correctness fix (per-line positioning), not only
dedup.

Slice 1 scope = horizontal LTR plain text. Runs containing pseudo/generated
content, a relative/sticky positioned inline, an atomic inline
(inline-block/-flex/-grid/-table), vertical writing mode, or text-align:justify
are gated out and keep render's legacy path (each is a layout-IFC-vs-render
membership divergence resolved in later slices). Staleness is reconciled by an
explicit insert-or-remove per layout pass (layout_generation is constant 0 off
the paged path, so it cannot signal staleness).

- elidex-ecs: InlineFlow / InlineFlowLine / InlineFlowRun components
- LinePacker: record per-line positioned runs (commit-on-content seam, same-entity
  coalescing) + bake per-line text-align into inline_start
- collect_inline_items reports RunComplexity for the persist gate
- render: shared emit_text_segment + new emit_inline_flow; gate at emit_inline_run

Tests: 11 layout (single/multi-line/coalesce/abs-coords/center/5 gates/stale-clear)
+ 2 render (consume-per-line / fallback). Full workspace: 10611 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mentation) + cleanups

Pre-push review follow-up to the slice-1 convergence commit.

Code-review caught three layout-IFC-vs-render divergences the slice-1 gate did
not yet exclude — each would mis-render under the converged path:
- bidi (RTL) text: render reorders visually (bidi_visual_order); layout's
  logical-order positions would scramble it. Gate via new
  elidex_bidi::text_has_rtl (allocation-free conservative bidi-class scan).
- text-transform: layout measures/positions untransformed text but render
  transforms before shaping → wrong baked positions.
- fragmentation (paged): flow_lines are not yet sliced per fragment.

Plus: RunComplexity carries the two new gate flags; InlineFlowLine.block_size
doc clarified as persisted-but-not-yet-consumed (forward-looking §10.8.1 leading).

Tests: +3 layout gate cases (rtl/text-transform/fragmented) + 1 elidex-bidi
unit test (text_has_rtl).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…_rtl + cite

Pre-push /simplify + /elidex-review follow-up.

/elidex-review (Axis 2): an absolutely-positioned child interspersed between
inline text under a stacking-context parent was double-painted — render's
stacking-context Layer 5 split the inline run on `is_positioned` while
`paint_non_sc` (and layout's IFC grouping) skip positioned without splitting.
The split-off run keyed on a different entity than the InlineFlow layout
persisted, so the post-abspos run was painted once from the flow and again via
the legacy fallback. Unify Layer 5 to skip positioned children without breaking
the run (One-issue-one-way) — render-only; also fixes a pre-existing overlap.

/simplify: text_has_rtl is now an allocation-free conservative bidi-class scan
(was allocating a BidiInfo for non-ASCII-but-LTR text e.g. CJK/accented Latin).

/elidex-review (Axis 4): fix a §4.5 citation that points to a nonexistent
section → CSS Text 3 §5.6 "Shaping Across Intra-word Breaks".

Tests: +1 render regression (interspersed-abspos no-double-paint). render 126,
shell 105 pass after the Layer-5 change (no golden churn).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 1, 2026 17:23
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Slice 1 of converging the parallel inline-text pipelines: layout's LinePacker now persists per-line collapsed+positioned text runs as an InlineFlow ECS component keyed on the run-start entity, and render consumes it instead of doing an independent (single-line, broken) collect/collapse/measure pass. A RunComplexity gate excludes any run whose layout-IFC membership diverges from render (pseudo/generated, rel/sticky positioned, atomic inline, bidi RTL, text-transform, vertical WM, justify, fragmented); these continue to use the legacy render path. Also unifies Layer 5 with paint_non_sc by skipping positioned children without splitting the inline run, fixing an interspersed-abspos double-paint.

Changes:

  • New InlineFlow/InlineFlowLine/InlineFlowRun components in elidex-ecs; LinePacker records them with text-align baked into each run's inline_start, with explicit insert-or-remove reconciliation per layout pass.
  • elidex-bidi: allocation-free text_has_rtl used as a cheap conservative gate; collect_inline_items now also returns a RunComplexity flag-set; gate consulted in layout_inline_context_fragmented.
  • Render: shared emit_text_segment helper + new emit_inline_flow consume path gated inside emit_inline_run; Layer 5 skips positioned children without flushing the run.

Reviewed changes

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

Show a summary per file
File Description
crates/core/elidex-ecs/src/components.rs Adds InlineFlow/InlineFlowLine/InlineFlowRun component types.
crates/core/elidex-ecs/src/lib.rs Re-exports the new inline-flow components.
crates/text/elidex-bidi/src/lib.rs New text_has_rtl allocation-free RTL gate + test.
crates/text/elidex-text/src/lib.rs Re-exports text_has_rtl.
crates/layout/elidex-layout-block/src/inline/mod.rs Adds RunComplexity, returns it from collect_inline_items, gates persistence and reconciles InlineFlow per pass.
crates/layout/elidex-layout-block/src/inline/pack.rs LinePacker records per-line positioned runs with text-align baked in; coalesces same-entity break pieces.
crates/layout/elidex-layout-block/src/inline/measure.rs Updates call sites for new tuple return.
crates/layout/elidex-layout-block/src/inline/tests/{mod,inline_flow}.rs New persistence/gate/stale-clear tests.
crates/core/elidex-render/src/builder/inline.rs Adds expected_generation param; consumes InlineFlow via new emit_inline_flow; extracts shared emit_text_segment.
crates/core/elidex-render/src/builder/walk.rs Layer 5 skips positioned children without splitting the inline run; threads expected_generation.
crates/core/elidex-render/src/builder/tests/{mod,inline_flow}.rs New consume / fallback / interspersed-abspos tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

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 13 out of 13 changed files in this pull request and generated no new comments.

@send send merged commit e300863 into main Jun 1, 2026
8 checks passed
@send send deleted the feat/inline-pipeline-convergence branch June 1, 2026 23:31
send added a commit that referenced this pull request Jun 2, 2026
…s (slice 2) (#271)

Slice 2 of the render↔layout inline-pipeline convergence (#270 = slice 1). Extends the InlineFlow persist+consume convergence to vertical writing modes, dropping the !is_vertical gate — fixing the silent single-column mis-render of multi-line / multi-run vertical text (render's legacy vertical path did no line-breaking).

Layout: drop !is_vertical from the persist gate; apply the is_vertical projection swap when folding the IFC origin (same rule as static_positions / box assignment), so each InlineFlow scalar holds the absolute physical coordinate for its axis (no vertical-rl block reversal — matches the box convention). FlowAlign's resolution was already inline-axis-agnostic.

Render: unified emit_inline_flow branches horizontal vs vertical at the glyph emit; vertical derives the column center_x from block_start/block_size and pen y from inline_start. Shared emit_vertical_text_segment / vertical_text_orientation extracted from the legacy path. Dispatch reads only writing_mode+text_orientation (Copy) — no ComputedStyle clone on the hot path; styleless parent → horizontal default (mirrors layout's unwrap_or_default). Writing-Modes citations normalized to Level 4; §4.1→§3.1 content-mismatch fixed.

Tests: +6 layout (vertical-rl/lr persist, axis-swap proof, multi-column, vertical+justify-still-gated) + 1 render (multi-column vertical consume); removed the obsolete slice-1 gate_excludes_vertical_writing_mode.

Cap-neutral. Deferred (plan §10): vertical-rl block-axis reversal, writing-mode-aware decoration sidedness, shared logical→physical helper, bidi in converged vertical → slice 4.

Pre-push gate (fmt + mise run ci 10634 tests + /code-review + /simplify + /review + /elidex-review 0C/0I) all green. Copilot 3R TERMINAL (R2+R3 0C/0I).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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