Skip to content

feat(layout): converge inline pipeline — multicol whole-in-column InlineFlow (slice 4 / I-multicol)#291

Merged
send merged 3 commits into
mainfrom
feat/inline-convergence-slice4-multicol
Jun 6, 2026
Merged

feat(layout): converge inline pipeline — multicol whole-in-column InlineFlow (slice 4 / I-multicol)#291
send merged 3 commits into
mainfrom
feat/inline-convergence-slice4-multicol

Conversation

@send

@send send commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Summary

Slice 4 / I-multicol of the render↔layout inline-pipeline convergence (follows #282 I-paged). A multi-column (Column-fragmented) run that is whole in its column now persists an InlineFlow, fixing multi-line inline text in multicol (legacy's single-linear-pass mis-rendered it), and the column shift now moves the persisted flow — not just the LayoutBox.

Two production edits:

  1. Gate (elidex-layout-block/src/inline/mod.rs) — split into a pre-pack persist_candidate (drives flow_align, optimistically includes Column) and a post-pack persist_flow refined to whole-in-column: column_is_whole = skip_lines == 0 && break_after_line.is_none(). The optimistic candidate is safe because flow_align gates only flow_lines population, never entity_bounds/box geometry (the packer commits those unconditionally).
  2. Shifter convergence (elidex-layout-multicol/src/lib.rs) — delete the duplicate shift_entity_and_descendants (+ MAX_DESCENDANT_DEPTH); position_column_fragments calls elidex_layout_block::block::shift_descendants, the canonical project-wide subtree shifter, which already moves a persisted InlineFlow. One-issue-one-way: a single shifter, not a second InlineFlow-aware copy.

Scope (whole-in-column; mid-IFC column break → deferred to Z), forced by G11. A paragraph that spans a column boundary needs ≥2 fragments at ≥2 column offsets, but the column shift moves a run-start's whole subtree by one delta and the containing box is already last-column-wins (fill.rs:117, one box/flow per entity). So mid-IFC column break stays on legacy (no regression — broken today too) and converges with box fragments later, in the standalone fragment tree (Z), rather than shipping an inconsistent text-right/box-wrong partial fix.

No accumulate (whole-in-column ⇒ length-1 InlineFlow.fragments) and no probe-gating flag: with no accumulate, the final balanced-fill pass's insert_one-replace + the unconditional clear_inline_flows deterministically overwrite/clear every probe flow. elidex-ecs + render are unchanged.

Tests (7, cap-neutral)

  • 4 unit (elidex-layout-block): multicol_whole_run_now_persists (+ asserts byte-identical geometry vs the non-fragmented reference), gate_excludes_multicol_continuation, gate_excludes_multicol_truncation, multicol_nested_in_paged_stamps_page_generation.
  • 3 integration (elidex-layout-multicol, real layout_multicol dispatcher): multicol_column_run_shifted_to_column_offset (the shift crux — flow moves with its box to the column offset), multicol_balanced_persists_one_fragment_per_run (probe overwrite-safety), multicol_spanner_ifc_persists_nonfragmented.
  • Vertical-WM column shift is covered transitively (the wiring is WM-agnostic; the projection is shift_descendants' own responsibility, tested in elidex-layout-block; multicol's vertical box shift is tested by vertical_rl_columns/vertical_lr_columns) — see the NOTE in multicol/src/tests.rs.

Verification

cargo fmt + mise run ci green (3-OS check + clippy + nextest + doc + deny) · 303 layout-block + 43 multicol + 131 render (no churn) + 105 shell (no golden churn — the shifter convergence is behavior-neutral for all existing fixtures). Pre-push: /code-review (0 bugs) · /elidex-review 5-axis (0 CRIT / 0 IMP) · /simplify + /review (docstring-drift + persist_flow refinement-form + comment trims applied).

🤖 Generated with Claude Code

send and others added 2 commits June 6, 2026 14:42
…ineFlow (slice 4 / I-multicol)

A multicol (`Column`) run that is whole in its column (skip_lines == 0, no
fragment break) now persists an `InlineFlow`, fixing multi-line inline text in
multi-column layout (legacy's single-linear-pass mis-rendered it). The column
shift converges onto block layout's canonical `shift_descendants`, which already
moves a persisted `InlineFlow` — so the converged column's inline text shifts to
the column offset instead of repainting at column 0.

- Gate (inline/mod.rs): split into a pre-pack `persist_candidate` (drives
  `flow_align`, optimistically includes `Column`) and a post-pack `persist_flow`
  refined to whole-in-column (`column_is_whole = skip_lines == 0 &&
  break_after_line.is_none()`). Mid-IFC column break stays on legacy, deferred to
  the standalone fragment tree (Z) with box fragments (G11: one box/flow per
  entity — the column shift moves a run-start's whole subtree by one delta).
- Multicol (lib.rs): delete the duplicate `shift_entity_and_descendants` (+ its
  `MAX_DESCENDANT_DEPTH`); `position_column_fragments` calls
  `elidex_layout_block::block::shift_descendants` (One-issue-one-way — a single
  subtree shifter, now InlineFlow-aware for columns too).

No accumulate (whole-in-column = length-1 Vec) and no probe-gating flag: with no
accumulate the final fill pass's `insert_one`-replace + unconditional
`clear_inline_flows` deterministically overwrite/clear every balanced-fill probe
flow. ecs + render unchanged.

7 new tests (4 gate unit + 3 multicol integration). cap-neutral.

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

- inline/mod.rs: update the `fragmentation_type` field docstring (was stale at the
  I-paged "paged-scoped, Column gated until I-multicol" wording — this PR is that
  I-multicol; now documents whole-in-column Column persistence + mid-IFC→Z).
- inline/mod.rs: express `persist_flow` as the refinement it is —
  `persist_candidate && (!frag_is_column || column_is_whole)` — instead of repeating
  the full disjunction (logically identical; makes the candidate→refine relation
  visible). Trim the duplicated `flow_align`-safety restatement.
- multicol/lib.rs: trim the shifter comment's re-explanation of `shift_descendants`
  internals (they live at the callee docstring); keep the one-issue-one-way reuse
  rationale + the before/after correctness note.

No behavior change (gate tests + 43 multicol + 303 layout-block all green; clippy clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 6, 2026 05:56

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 for multi-column (Column-fragmented) layout by persisting InlineFlow for whole-in-column inline formatting contexts and ensuring the column offset shift moves the persisted flow (not just LayoutBoxes). This addresses incorrect rendering of multi-line inline text in multicol when using the converged path.

Changes:

  • Refines the InlineFlow persist gate to allow Column fragmentation only when the run is whole in its column (skip_lines == 0 and no fragment break), while keeping mid-IFC column breaks on legacy.
  • Converges multicol column-shifting on the canonical elidex_layout_block::block::shift_descendants, which shifts both LayoutBox and persisted InlineFlow.
  • Adds multicol integration tests covering flow persistence, probe overwrite-safety in balanced fill, and correct flow shifting to column offsets.

Reviewed changes

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

File Description
crates/layout/elidex-layout-multicol/src/tests.rs Adds integration tests validating whole-in-column InlineFlow persistence and correct flow shifting with column offsets.
crates/layout/elidex-layout-multicol/src/lib.rs Replaces the local LayoutBox-only shifter with the canonical shift_descendants to shift both boxes and persisted flows.
crates/layout/elidex-layout-block/src/inline/tests/inline_flow.rs Updates/expands unit tests to cover multicol whole-in-column persistence and exclusion of continuation/truncation cases.
crates/layout/elidex-layout-block/src/inline/mod.rs Splits the persist gate into pre-pack persist_candidate and post-pack refined persist_flow for whole-in-column multicol runs.

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

Comment thread crates/layout/elidex-layout-multicol/src/tests.rs
Comment thread crates/layout/elidex-layout-multicol/src/tests.rs Outdated
- `flow_inline_start` (multicol tests): assert single-line + non-empty runs before
  indexing `lines[0].runs[0]`, so a future regression fails with a clear message
  instead of an unhelpful out-of-bounds panic.
- `multicol_column_run_shifted_to_column_offset`: tighten the column-1 assertion
  from `b_x > 250.0` to `(b_x - 300.0).abs() < 1.0` (and column 0 to `~0`) — the
  exact column offset (width 300, gap 0), so wrong offset math (e.g. 280) can't pass.

Test-only. 43 multicol + clippy green.

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

send commented Jun 6, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@send send merged commit e03525f into main Jun 6, 2026
6 checks passed
@send send deleted the feat/inline-convergence-slice4-multicol branch June 6, 2026 06:21
send added a commit that referenced this pull request Jun 6, 2026
…2), not drop (#292)

Balanced fill no longer drops overflow children: `column-count` is a balance target, not a hard layout cap. The definitive pass is uncapped (`u32::MAX`) so surplus content spills into overflow columns (CSS Multicol L1 §8.2), overwriting each child's column-0 height-probe `InlineFlow` (stranded as a ghost post-#291) with its real per-column flow; the search probe caps at `count + 1` (minimal feasibility cap, fixes the degenerate search). Adds a balance + small max-height regression test asserting flows land at their real column offsets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
send added a commit that referenced this pull request Jun 6, 2026
… lines) (#293)

`tests.rs` grew past the 1000-line convention (1214 lines after the #291
I-multicol whole-in-column tests + the #292 balanced-overflow regression).
Split the single `mod tests` into a `tests/` directory module, matching the
sibling layout crates (flex/grid/table `tests/mod.rs` house style):

- mod.rs        — shared imports + helpers + crate-root `#![allow(unused_must_use)]`
- is_multicol.rs   — `is_multicol` predicate tests (6)
- geometry.rs      — basic layout / geometry / width / gap / child positioning (17)
- spanner.rs       — `column-span` / spanner segmentation (7)
- fill.rs          — `column-fill` (auto/balance) + overflow columns (2)
- inline_flow.rs   — I-multicol `InlineFlow` persistence + column shift (4)

Pure test-organization refactor: test bodies moved byte-for-byte, no production
code change. Shared helpers live in the parent `mod tests`; submodules `use
super::*`. The crate-root `#![allow(unused_must_use)]` cascades to every
submodule. 36 tests preserved (identical names); 44/44 crate tests pass; clippy
clean; `mise run ci` green. Each file is now ≤ 509 lines.

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