Implement the vertical-align style - #766
Conversation
The root style is always materialised at index 0 and every other style records the index of its enclosing span's style. The tree builder now materialises ancestor spans that contain no direct text so that they can contribute their inline box to line metrics.
`VerticalAlign` mirrors css-inline-3 as a compound of `AlignmentBaseline` (baseline, text-top, text-bottom, middle) and `BaselineShift` (length, sub, super, top, bottom), so callers holding the longhands (e.g. Blitz over Stylo) can pass them through losslessly and values such as `vertical-align: text-top 2px` compose. The CSS 2 keywords are available as associated constants (`VerticalAlign::SUPER` etc.) plus `VerticalAlign::length`. The property is plumbed through `StyleProperty`, `TextStyle`, both builders and `ResolvedStyle`; atomic inline boxes carry their own `vertical_align` and optional `baseline`. No layout behaviour changes yet.
Every style table entry gets `StyleMetrics`: primary-font ascent/descent/ x-height (honouring font variations), the line-height expanded `over`/`under` box with CSS 2 §10.8.1 half-leading, and its baseline offset relative to the root of its aligned subtree. Offsets accumulate parent-first through the style's parent chain (`alignment-baseline` offset plus `baseline-shift`); `top`/`bottom` start a new line-relative aligned subtree. `sub`/`super` use the WebKit/Blink constants (font-size / 5 and / 3). Per-character style indices are reset for each layout so an empty layout's substitute space uses the root style rather than a stale index.
… glyphs/inline boxes Line breaking now seeds every line with the root (strut) inline box, adds each run's style box plus its not-yet-contributed ancestors, and tracks vertical-align: top/bottom subtrees separately. Glyph runs and inline boxes are positioned at their style's shifted baseline. Lines containing only inline boxes now get strut height (snapshots updated). Details folded in from review: - Lines containing only empty or out-of-flow inline boxes have zero height; negative-height in-flow boxes still count as content and keep the strut. - The trailing line after a final newline is sized by the newline's style chain rather than collapsing to the strut. - With `quantize`, the exact baseline offset is accumulated down the style chain and rounded once per style (and once per top/bottom subtree), so glyph baselines are whole pixels without per-level rounding drift, and inline box ascent/descent are not rounded separately. - Font queries and metrics are shared between styles with identical font selection inputs, run box metrics are computed once per run, and the shaped run's metrics are reused for its style; aligned-subtree offsets are stored per layout instead of per line.
There was a problem hiding this comment.
The approach seems correct overall. I just reviewed your commits.
There appears to be some performance cost to the per-style font querying, and the new SmallVecs on LineBoxMetrics:
Default Style - arabic 20 characters [ 9.7 us ... 9.9 us ] +2.13%*
Default Style - latin 20 characters [ 4.4 us ... 4.7 us ] +8.13%*
Default Style - japanese 20 characters [ 8.2 us ... 8.7 us ] +5.73%*
Default Style - arabic 1 paragraph [ 52.6 us ... 53.0 us ] +0.72%
Default Style - latin 1 paragraph [ 18.3 us ... 19.5 us ] +6.72%*
Default Style - japanese 1 paragraph [ 68.6 us ... 69.9 us ] +1.94%*
Default Style - arabic 4 paragraph [ 220.6 us ... 218.8 us ] -0.82%
Default Style - latin 4 paragraph [ 72.2 us ... 75.4 us ] +4.56%*
Default Style - japanese 4 paragraph [ 97.6 us ... 100.3 us ] +2.79%*
Styled - arabic 20 characters [ 10.7 us ... 11.3 us ] +5.74%*
Styled - latin 20 characters [ 5.7 us ... 6.2 us ] +8.49%*
Styled - japanese 20 characters [ 8.8 us ... 9.7 us ] +9.55%*
Styled - arabic 1 paragraph [ 55.3 us ... 56.4 us ] +2.10%*
Styled - latin 1 paragraph [ 22.4 us ... 23.7 us ] +6.03%*
Styled - japanese 1 paragraph [ 74.9 us ... 79.0 us ] +5.46%*
Styled - arabic 4 paragraph [ 239.9 us ... 244.1 us ] +1.73%*
Styled - latin 4 paragraph [ 90.2 us ... 93.6 us ] +3.79%*
Styled - japanese 4 paragraph [ 108.5 us ... 114.4 us ] +5.42%*
Word + Letter Spacing - arabic 20 characters [ 9.5 us ... 9.8 us ] +3.45%*
Word + Letter Spacing - latin 20 characters [ 4.4 us ... 4.8 us ] +7.65%*
Word + Letter Spacing - japanese 20 characters [ 8.3 us ... 8.7 us ] +4.98%*
Word + Letter Spacing - arabic 1 paragraph [ 53.2 us ... 53.9 us ] +1.19%*
Word + Letter Spacing - latin 1 paragraph [ 18.7 us ... 19.8 us ] +5.89%*
Word + Letter Spacing - japanese 1 paragraph [ 69.3 us ... 70.4 us ] +1.60%*
Word + Letter Spacing - arabic 4 paragraph [ 223.7 us ... 223.6 us ] -0.04%
Word + Letter Spacing - latin 4 paragraph [ 71.7 us ... 75.9 us ] +5.89%*
Word + Letter Spacing - japanese 4 paragraph [ 99.7 us ... 101.1 us ] +1.45%
Repeated Justification - latin 4 paragraph [ 1.3 us ... 1.3 us ] +0.04%
Fontique - system fonts init (CoreText) [ 41.4 ms ... 42.0 ms ] +1.35%*
| /// Vertical alignment of the box within its line, relative to the enclosing style span. | ||
| pub vertical_align: VerticalAlign, |
There was a problem hiding this comment.
This is a breaking change, let's add a changelog notice?
| /// The extents from the line box's baseline. | ||
| /// Extents of each aligned subtree with content on this line. The first entry is always the | ||
| /// root subtree (root style index `0`). | ||
| subtrees: SmallVec<[SubtreeExtents; 2]>, |
There was a problem hiding this comment.
(Fable helped find this) By Default, this is empty. reset establishes 0 as the root (the comment on subtrees says the first entry is always the root subtree: 0), but this isn't true if a public consumer uses BreakLines::revert_to(BreakerState::default()). We'd then OOB at &self.subtrees[0] through LineBoxMetrics::root next time we break lines.
How about just pushing 0 to subtrees in the default impl, and having reset build on that, to avoid drift? The manual default should include setting last_text to the (MAX, 0) sentinel, too.
The follow-up PR linked in the description mostly eliminates this for the font querying. The |
Implements vertical alignment.
LLM Contributions: Generated with Fable 5.1 Low. Reviewed by GPT 5.6 Sol XHigh. This has also been manually reviewed, and gone through several rounds of iteration. I probably still need to do another round on the details of the the actual alignment, but I think this is architecturally in a good place.
vertical-alignvia Parley DioxusLabs/blitz#832This PR is designed to be reviewed commit-by-commit (4 commits):
Introduce a minimal tree structure:
Adds
parent: u16to every style, which allows for upwards tree-traversal. TheTreeBuilderencodes it's tree with this new field. TheRangedBuilderbuilds a trivial tree with one root, and where all style spans are direct children of that root. This builds on the existing convention that the 0th style is the root/paragraph level style.Adds
VerticalAlignstyle.VerticalAlignis struct which consists ofBaselineShift, andAlignmentBaselinefields, and can be set both as a span style and on inline boxes.Introduces the concept of first available font and resolves a font + metrics for every style. We thus end up with a tree of spans each with associated line metrics before we even begin layout. We do not use the actual resolved fonts for any run/atom/cluster for vertical alignment at all (this matches browsers, but there are other reasonable choices here, and we could bring back using run/cluster/atom metrics for this as an option if desired). Some level of alignment relative to the parent is already pre-computed at this stage.
(Note: these style lookups are kinda expensive - I have a follow-up PR that eliminates that overhead with a global cache on the
FontContext- Cache primary font queries and font metrics across layouts in FontContext DioxusLabs/parley#19 for those who want a preview)Implements the actual vertical alignment logic by building a list of "aligned subtrees" for each line, and computing the offsets of each span in each aligned subtree relative to each other. Most lines actually only have one "aligned subtree", so it is alignment within a tree doing most of the work.
vertical-align: topandbottomintroduce new "aligned subtrees" which are initially aligned independently of the other "aligned subtrees" on the line. There is then a final alignment step when completing each line that aligns the "aligned subtrees" to each other.Some notes:
RangedBuilderlayouts fallback gracefully to "strut metric"-style layout (spans in ranged layouts only have one ancestor: the root).Changelog