Implement the vertical-align style - #766
Conversation
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 follow-up PR linked in the description mostly eliminates this for the font querying. The |
9c229fb to
a33e57c
Compare
DJMcNab
left a comment
There was a problem hiding this comment.
I haven't quite finished reviewing all of line_break.rs, sorry!
In general, this looks great, and is a really valuable feature. The implementation also seems reasonably sensible. I've reviewed commit-by-commit as suggested, and so some of the comments might be a bit duplicative.
My biggest blockers are:
- The usage of the term inline box consistently being confusing/inconsistent.
- The wrong definition of
first_available_font
Feel free to aggressively scope other comments as follow-ups; I think even opening an issue for that would be good (so you can mark them as resolved once added). GitHub's UI is pretty bad at making comments browsable.
I will still need to review any new changes, so if you can avoid squashing them into the existing commits, that would be a great help.
| for style in styles.drain(..) { | ||
| let style_index = if style.style == style_table[0] { | ||
| 0 | ||
| } else { | ||
| assert!(style_table.len() <= u16::MAX as usize, "too many styles"); | ||
| style_table.push(style.style); | ||
| (style_table.len() - 1) as u16 | ||
| }; |
There was a problem hiding this comment.
This doesn't feel right to me - we already have a RangedStyle for the root style, which was pushed first. That is, in what scenario does this code actually change the result?
| styles.truncate(styles.len() - merged_count); | ||
|
|
||
| style_table.reserve(styles.len()); | ||
| // The root style is always at index 0 and is the parent of every other style. |
There was a problem hiding this comment.
Maybe "every other style has their parent set to zero; ensure that is actually the root style"
| // The style tree (`parent`) is intentionally not part of the comparison: the tree builder | ||
| // records span nesting that the flat builders cannot express, so only the visual style | ||
| // properties are compared. |
There was a problem hiding this comment.
Nit: This comment smells LLM-y. I agree that this is probably the right approach; a comment is also probably needed, so I guess it's fine?
Although also I am worried about how this would handle vertical alignment properties...
| /// box (the enclosing style span, or the root style for top-level content). Mirrors the CSS | ||
| /// `alignment-baseline` property. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] | ||
| pub enum AlignmentBaseline { |
There was a problem hiding this comment.
Obviously the CSS spec has several additional values for this specified. Can you speak to why this subset was chosen?
See https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/alignment-baseline
There was a problem hiding this comment.
Basically because these are the only ones anybody ever uses (they are the "CSS2" values that have existed forever). The other values (alphabetic, ideographic, hanging, mathematical, central, etc) are newer and refer to specific baselines from the opentype spec, and I would consider them advanced functionality. They also require actually reading those baselines from the font.
We should probably implement these eventually, but I would consider them low priority.
| pub(crate) fn quantize_offset(offset: f32, quantize: bool) -> f32 { | ||
| if quantize { offset.round() } else { offset } | ||
| } |
There was a problem hiding this comment.
Nit: I'd probably just inline this function? At the very least, the current doc is bad.
| fn line_height(self) -> f32 { | ||
| let line_box = self.line_box.or_zero(); | ||
| line_box.over + line_box.under | ||
| fn reset(&mut self, strut: Option<&StyleMetrics>) { |
| if root == 0 { | ||
| return &mut self.subtrees[0]; | ||
| } | ||
| let index = match self.subtrees.iter().position(|s| s.root == root) { |
There was a problem hiding this comment.
Nit: I'd probably use .iter_mut().find(), then in the None branch use push then last_mut (ideally, we'd use push_mut, but that would need a new 'MSRV-bump' reminder warning)
| subtree | ||
| .line_box | ||
| .add(baseline_offset, run_box.over, run_box.under); | ||
| subtree | ||
| .content_box | ||
| .add(baseline_offset, run_box.ascent, run_box.descent); |
There was a problem hiding this comment.
This is the code which means we, in Canva, currently can't use this - I believe that the relevant spec line is:
ignoring glyphs from other fonts.
CSS Inline Layout Module Level 3
Landing this PR without addressing that would be fine; I suspect the majority of content Blitz sees has line-height: normal, for example.
There was a problem hiding this comment.
This should be fixed in DioxusLabs@e08c7f5
We now only apply these Metrics for LineHeight::MetricsRelative.
| // Inline box extents are exact box sizes supplied by the caller, not font metrics; | ||
| // rounding them would change the space the box reserves relative to its height. |
There was a problem hiding this comment.
This comment has the wrong focus - the interesting question is how line-box quantisation is preserved despite this happening.
Why do we even have this parameter?
a33e57c to
e08c7f5
Compare
32c3b48 to
79a458e
Compare
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.
…+0020 coverage check CSS Fonts 4 defines the first available font via unicode-range coverage of U+0020, not glyph coverage. Parley has no unicode-range, so every available face qualifies and the first face of the stack is used.
…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.
2286707 to
7a48a76
Compare
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