feat(layout,render): converge inline pipeline — layout persists InlineFlow, render consumes (slice 1)#270
Conversation
…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>
There was a problem hiding this comment.
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/InlineFlowRuncomponents inelidex-ecs;LinePackerrecords them withtext-alignbaked into each run'sinline_start, with explicit insert-or-remove reconciliation per layout pass. elidex-bidi: allocation-freetext_has_rtlused as a cheap conservative gate;collect_inline_itemsnow also returns aRunComplexityflag-set; gate consulted inlayout_inline_context_fragmented.- Render: shared
emit_text_segmenthelper + newemit_inline_flowconsume path gated insideemit_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.
…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>
Why
elidex-rendercarried a parallel inline-text pipeline that re-collected, re-collapsed, re-measured and re-positioned DOM text independently ofelidex-layout-block. Crucially, render's pipeline did no line-breaking — a single fixed baseline (emit_styled_segmentsis literally "a single linear pass") — so wrapped multi-line text, multi-run blocks, and per-linetext-alignrendered 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
LinePackernow persists collapsed + positioned line runs as anInlineFlowECS component (inelidex-ecs, whereEntitylives) keyed on the run-start entity. Render consumes it (emit_inline_flow, gated insideemit_inline_run) instead of re-walking the DOM — honouring layout's per-line line-breaking and per-linetext-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 pass —
layout_generationis constant0off the paged path, so it cannot signal staleness.Also unifies render's two inline-run groupings:
paint_stacking_context_layersLayer 5 now skips positioned children without splitting the run (matchingpaint_non_sc+ layout), fixing an interspersed-abspos double-paint and a pre-existing overlap bug.Key files
elidex-ecs:InlineFlow/InlineFlowLine/InlineFlowRuncomponentselidex-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-linetext-alignintoinline_startcollect_inline_items→RunComplexitygate flagsemit_text_segment+ newemit_inline_flow; gate atemit_inline_runTests
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 cigreen;elidex-shellfull-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-freetext_has_rtl),/review,/elidex-review(Layer-5 unification for interspersed-abspos + a §-citation fix). Plan + 2-round/elidex-plan-reviewpreceded implementation.🤖 Generated with Claude Code