feat(style,layout,render): converge inline pipeline — counters + pseudo/generated content pre-layout (slice 3)#273
Merged
Merged
Conversation
…do/generated content pre-layout (slice 3) Move the CSS counter state machine + pseudo/marker generated-content resolution out of render's paint walk into a new pre-layout pass (the final phase of style resolution). Layout now measures resolved generated text (was a `[counter:X]` placeholder) so InlineFlow persists pseudo runs; render reads resolved pseudo TextContent + a ListItemMarker component instead of running a counter machine for document content. - elidex-ecs: + ListItemMarker(String) component (single source of marker text). - elidex-style: new generated_content pass — a document-order CounterState walk writing pseudo `content` -> TextContent and a reconciled ListItemMarker; wired as the final phase of resolve_styles_with_compat. generate_pseudo_entity no longer pre-resolves content (the pass is the single resolver; the `[counter:X]` placeholder is gone). - elidex-layout-block: drop the has_pseudo persist_flow gate; the pseudo branch now applies the bidi/text-transform gates on the resolved text. - elidex-render: delete resolve_pseudo_text + resolve_content_items_with_counters; maybe_emit_list_marker reads ListItemMarker; the walk counter machine is retained (gated paged-only) solely for paged-media margin boxes. Fold-in spec fixes: CSS Lists 3 §4.5 (display:none does not affect counters), attr() resolves against the originating element, and the §5.x->§4.x counter citation drift in counter.rs/walk.rs. Slice 3 of the render<->layout inline-pipeline convergence (One-issue-one-way, cap-neutral). Plan: render-layout-inline-convergence-slice3-plan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r helper + clone-guard the pass /code-review + /simplify follow-ups (no behavior change): - counter.rs: extract `apply_implicit_list_counters_from_dom` (DOM tag/attrs/li-count marshalling) shared by the cascade walk, the generated-content pass, and render's paged counter walk — was triplicated. - generated_content.rs: probe the Copy marker fields + a has-counter-props flag from a scoped borrow and clone `ComputedStyle` only when the element carries counter influence (list tag or explicit counter-*), avoiding a full-style clone per common element on the restyle hot path; `resolve_pseudo_content` reads `content` internally. - doc: note that the pass's implicit-counter application is load-bearing (the parallel cascade path does not bake implicit counters); tidy a test binding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Moves CSS counters + generated content resolution (pseudo content, list markers) out of render’s paint walk into a new pre-layout style pass, so layout measures resolved text and render consumes persisted ECS state.
Changes:
- Add
generated_contentpass inelidex-style, invoked as the final phase ofresolve_styles_with_compat. - Introduce
ListItemMarker(String)ECS component as the single source of list marker text; render reads it instead of evaluating counters. - Remove the inline-flow “has_pseudo” gate so pseudo text can persist in
InlineFlowonce resolved pre-layout.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/layout/elidex-layout-block/src/inline/tests/inline_flow.rs | Updates gating test to assert pseudo flows now persist with resolved text. |
| crates/layout/elidex-layout-block/src/inline/mod.rs | Removes has_pseudo gate; adds bidi/text-transform gating for pseudo text runs. |
| crates/css/elidex-style/src/walk.rs | Centralizes implicit list counter application via apply_implicit_list_counters_from_dom. |
| crates/css/elidex-style/src/resolve/box_model/mod.rs | Spec citation update for reversed counters. |
| crates/css/elidex-style/src/pseudo.rs | Stops pre-resolving pseudo text; creates pseudo entities with empty TextContent. |
| crates/css/elidex-style/src/lib.rs | Wires resolve_generated_content as final style-resolution phase; exports it. |
| crates/css/elidex-style/src/generated_content.rs | New pass: document-order counter machine + pseudo/list marker resolution into ECS. |
| crates/css/elidex-style/src/counter.rs | Spec citation sweep + new apply_implicit_list_counters_from_dom helper. |
| crates/core/elidex-render/src/builder/walk.rs | Reads ListItemMarker; gates counter mutation to paged builds only. |
| crates/core/elidex-render/src/builder/tests/display_types.rs | Runs generated-content pass before display list build in marker tests. |
| crates/core/elidex-render/src/builder/tests/counter.rs | Runs generated-content pass before display list build; updates display:none expectations. |
| crates/core/elidex-render/src/builder/mod.rs | Adds paged flag to PaintContext setup. |
| crates/core/elidex-render/src/builder/inline.rs | Removes render-time pseudo counter evaluation; reads resolved TextContent only. |
| crates/core/elidex-ecs/src/lib.rs | Re-exports new ListItemMarker component. |
| crates/core/elidex-ecs/src/components.rs | Defines ListItemMarker(String) component and its semantics. |
Comments suppressed due to low confidence (1)
crates/core/elidex-render/src/builder/walk.rs:575
maybe_emit_list_markerclonesListItemMarker’sStringfor every list item even though the marker is immediately passed by reference. This adds avoidable allocation/copy overhead on large lists; you can borrow the stored marker text directly from the ECS ref.
let marker_text = match ctx.dom.world().get::<&ListItemMarker>(child) {
Ok(m) => m.0.clone(),
Err(_) => return,
};
if let Ok(child_lb) = ctx.dom.world().get::<&LayoutBox>(child) {
emit_list_marker_with_counter(
&child_lb,
&child_style,
&marker_text,
ctx.font_db,
ctx.font_cache,
ctx.dl,
);
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- generated_content.rs: align the recursion cap with the other document-order walks (`> MAX_ANCESTOR_DEPTH`, not `>=`) so generated-content resolution covers exactly the depths layout/render process, not one level shallower. - counter.rs: `apply_implicit_list_counters_from_dom` bails for non-list tags before cloning Attributes (runs on every element on every restyle via the cascade walk) and only clones for <ol>/<li> (the tags that read attributes). Deferred (pre-existing, not introduced by this PR): the implicit list-item increment keys on the <li> tag rather than `display: list-item` (CSS Lists 3 §4.6) — a separate spec-conformance follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
generated_content pre-layout pass: - Hoist the list-item marker reconcile-remove BEFORE the display:none / display:contents early-returns, so the "insert-or-remove every pass" invariant holds on all exit paths (a list-item → display:none/contents transition no longer leaves a stale ListItemMarker). The insert half stays after counter processing for visible-type list-items. - Use a fresh CounterState per root (find_roots may return multiple disconnected trees = independent documents; counter scope must not leak across them). Regression tests: marker_removed_when_element_becomes_display_none (none + contents), counters_independent_across_roots. 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.
Summary
Slice 3 of the render↔layout inline-pipeline convergence (One-issue-one-way). Moves the CSS counter state machine + pseudo/marker generated-content resolution out of render's paint walk into a new pre-layout pass — the final phase of style resolution. The convergence target: generated-content text (counter values, pseudo
content, list markers) is resolved once, in document order, before layout, and persisted as ECS state that both layout and render read.Prior slices: #270 (horizontal LTR) / #271 (vertical WM).
Why it's a correctness fix, not just dedup
Pseudo entities were spawned at style time with
counter()content left as placeholder text ([counter:list-item]). Layout measured that placeholder → wrong line-break/width. Render papered over it by re-resolving counters at paint time — so it painted the right glyphs at positions computed from the wrong text. Resolving once, pre-layout, fixes the measurement at the source and lets the single inline pipeline own pseudo runs too (InlineFlownow persists them).Changes
+ ListItemMarker(String)component — the single source of list-item marker text.generated_contentpass — a document-orderCounterStatewalk that writes resolved pseudocontent→ the pseudo'sTextContentand a reconciledListItemMarker(insert-or-remove every pass, mirroring slice-1'sInlineFlowexplicit-clear discipline). Wired as the final phase ofresolve_styles_with_compat(one insertion point covers every style→layout caller).generate_pseudo_entityno longer pre-resolves content — the pass is the single resolver (the[counter:X]placeholder is gone).has_pseudopersist_flowgate; the pseudo branch now applies thehas_bidi/has_text_transformgates on the resolved text (so RTL / text-transformed pseudo content stays gated for later slices).resolve_pseudo_text+resolve_content_items_with_counters;maybe_emit_list_markerreadsListItemMarker; the walk's counter machine is retained but gated paged-only — its sole remaining consumer is paged-media margin boxes (per-page running-header counters require post-pagination page assignment and cannot be precomputed pre-layout; analogous to bidi staying in render).Folded-in spec fixes
display:noneelement does not set/reset/increment a counter (the old paint walk processed counters before the display:none check). Applied in the pass and mirrored in the retained paged walk.attr()in pseudocontentresolves against the originating element (was resolving against the pseudo entity → empty).counter.rs/walk.rscited "CSS Lists L3 §5.x" — counters live under §4 (§5 has no headings). Swept to §4.1/§4.2/§4.3/§4.6/§4.7; counter-value formatting → CSS Counter Styles 3 §6.Edge matrix covered
Counter ops × scope (reset/increment/set, reversed, nested
counters()); implicit list-item (ol/ul/li/start/value/reversed/nested); 12list-style-type× marker formatting; pseudocontentshapes (string/attr/counter/counters/mixed); §4.5 display:none-skip and visibility:hidden-still-counts; ::before-vs-::after counter timing; marker gate-OUT staleness (reconcile removal); re-resolution idempotency; marker × paged.Tests
6 new
generated_contentpass tests (markers in document order, §4.5 display:none skip with value check, reconcile removal on gate-out, nested-ol reset, counter() pseudo content, attr-against-originating). Layoutgate_excludes_pseudo_element→persists_pseudo_element_flow(inverted). Render marker tests run the pass before building the display list. All 1053 tests across the touched crates + elidex-shell pass (no golden churn — output-preserving for existing cases).Cap posture
Cap-neutral, no new
#11-*slot — One-issue-one-way relocation + conformance/correctness, not a new web-platform feature (same posture as slices 1–2).Deferred (own homes)
::marker as a real entity + layout marker-box generation (list-style-position); paged-media margin-box counter convergence; quotes/url/leader
contentvalues; visibility:collapse §4.5 edge.🤖 Generated with Claude Code