Skip to content

Implement CSS vertical-align for inline formatting contexts - #18

Closed
nicoburns wants to merge 35 commits into
mainfrom
devin/1788383056-vertical-align
Closed

nicoburns wants to merge 35 commits into
mainfrom
devin/1788383056-vertical-align

Conversation

@nicoburns

@nicoburns nicoburns commented Sep 2, 2026 •

Copy link
Copy Markdown
Member

LLM Contributions: Written with Devin (Cognition AI); design and review by the author.

Implements the CSS 2.2 §10.8 inline alignment model. Commit-by-commit:

  1. Style tree parents — TreeStyleBuilder now materialises ancestor styles (even ones with no direct text) into the style table, parent-first, and each Style carries parent: u16. Root style is index 0 and is the strut. RangedBuilder styles all parent 0.
  2. VerticalAlign — new StyleProperty::VerticalAlign(VerticalAlign) and InlineBox::vertical_align. Following css-inline-3, VerticalAlign { alignment: AlignmentBaseline, shift: BaselineShift } is a compound of the two longhands (AlignmentBaseline::{Baseline, TextTop, TextBottom, Middle}, BaselineShift::{Length(f32), Sub, Super, Top, Bottom}), so vertical-align: text-top 2px is representable and callers with the longhands (Blitz/Stylo) pass them through losslessly. The CSS 2 keywords are associated constants (VerticalAlign::SUPER, ::TEXT_TOP, …) plus VerticalAlign::length(px). Percentages must be resolved by the caller.
  3. StyleMetrics pass (layout/style_metrics.rs) — once per layout, per style: primary-font ascent/descent/x-height (FontMetrics::from_variations), line-height as half-leading → over/under, then baseline_offset relative to the parent = alignment offset (middle uses the parent x-height and the child's expanded box; text-top/bottom the parent content box) + shift (sub/super use WebKit's fontSize/5, fontSize/3), and aligned_subtree (nearest top/bottom ancestor, else 0).
  4. Line breaking — LineBoxMetrics becomes SmallVec<SubtreeExtents> + a per-line "contributed styles" set. Each line is seeded with the strut; each run/inline box adds its own style box at its baseline offset plus any not-yet-contributed ancestors. top/bottom subtrees are sized separately and, in finish_line, grow the line below/above when taller than the root subtree; their offsets are stored on LineData::aligned_subtree_offsets. Lines containing only empty inline boxes stay zero-height.
  5. Output — GlyphRun::baseline and PositionedInlineBox::y use the style's shifted baseline via Line::style_baseline / Line::inline_box_top. LineMetrics::baseline stays the root baseline; cursor/selection use whole-line block extents and are unaffected.

Behaviour changes for existing users: lines containing only inline boxes (or none of the text's styles) now get strut height, and a span's inline box uses the primary-font metrics of that style (six snapshots updated). New styles/vertical_align snapshot tests cover every keyword, nested super, ancestor-only spans, inline boxes and tall top/bottom subtrees; unit tests cover the metrics pass including composition of alignment + shift.

Blitz follow-up: DioxusLabs/blitz#832.

Changelog

Added

  • StyleProperty::VerticalAlign / VerticalAlign { alignment: AlignmentBaseline, shift: BaselineShift } and InlineBox::vertical_align, implementing CSS vertical-align (parent-relative and line-relative values).

Changed

  • Breaking change: InlineBox has a new vertical_align field.
  • Line boxes now follow the CSS 2.2 inline-box model more closely: every line includes the root style's strut, and nested tree-builder spans contribute their own inline boxes even when they have no direct text.

Link to Devin session: https://dioxus.staging.devinenterprise.com/sessions/da7669e341814cc7976e4ebf32b7eacb
Open in Devin Desktop: https://dioxus.staging.devinenterprise.com/desktop/session/da7669e341814cc7976e4ebf32b7eacb?variant=devin-insiders
Requested by: @nicoburns

@staging-devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

…box"

Parley already uses "inline box" for `InlineBox`, an atomic box embedded in the text, so use "span" / "span box" for what CSS calls an inline box (the box generated by a style table entry on a line). Documents the term in the `style_metrics` module docs.
BreakerState::default() is public and can be passed to BreakLines::revert_to,
but its LineBoxMetrics had no root aligned subtree, so the next line would
index out of bounds. Default now contains the root subtree (without a strut)
and reset builds on it, so the breaker is valid from any BreakerState.
Split the font-derived part into BoxMetrics, whose from_font returns a
complete value and is what run_box_metrics needs; StyleMetrics::from_font
is only meaningful inside resolve_style_metrics, which fills in the
remaining tree-position fields.
Each style now resolves its first available font and metrics directly;
cross-layout caching of both is left to the FontContext font cache.
Every non-root span's parent must precede it in the style table
(debug-asserted); a malformed parent index falls back to the root rather
than producing a self-parented orphan, so ancestor walks always end at 0.
Replaces the parallel inline_boxes / inline_box_styles vectors with a single
Vec<LayoutInlineBox { inline_box, style_index }>, so sorting is a plain
stable sort_by_key and the two can no longer get out of step.

Layout::inline_boxes() / inline_boxes_mut() now return ExactSizeIterators
over &InlineBox / &mut InlineBox instead of slices.
Signed-off-by: Nico Burns <nico@nicoburns.com>
Fonts without OS/2 sxHeight previously fell back to half the ascent.
Gecko (DEFAULT_XHEIGHT_FACTOR) and Blink both estimate 0.56 * ascent
in that case; css-inline-3 Appendix A.2 only suggests 0.5em as a
last resort after glyph measurement.
`LineBoxMetrics::contributed` (style indices already counted on the current line) was a `SmallVec<[u16; 8]>` cloned with `LineState` at every break opportunity and re-extended on every `reset`. It is append-only, so it can live once on `BreakerState` with a saved length in `PrevBoundaryState` and a truncate on revert. `subtrees` stays inline: its entries are mutated in place, so a length is not enough to roll them back (see `lines_revert_restores_line_height`).

Also reuse the `prev_state` allocation in `break_next` via `clone_from`.

Benchmarked on ~250KB of prose: break+align -7.6% (plain) / -6.1% (styled) / -8.2% (styled + vertical-align) vs before; layouts identical.
…ghts

With quantization enabled, `LineHeight::MetricsRelative` now sums the
rounded ascent, descent and leading (as Blink and Gecko do for
`line-height: normal`) so that such lines are a whole number of pixels
tall. Previously the span box ascent/descent were rounded but the line
height was not, leaving fractional line heights (e.g. 18.625px for a
16px Roboto line) that accumulated across lines. Both the run and the
style span box line heights go through the shared `LineHeight::resolve`.
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1788383056-vertical-align branch from 9c10024 to c6abe77 Compare September 26, 2026 02:44
Signed-off-by: Nico Burns <nico@nicoburns.com>
@nicoburns
nicoburns force-pushed the devin/1788383056-vertical-align branch from 12189ce to 888cdcc Compare September 28, 2026 13:08
@nicoburns nicoburns closed this Sep 28, 2026
@staging-devin-ai-integration
staging-devin-ai-integration Bot removed this pull request from stack #24 September 28, 2026 15:41
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