Implement vertical-align via Parley - #832
Conversation
Map Stylo's alignment-baseline/baseline-shift longhands to Parley's new VerticalAlign property on text styles and atomic inline boxes, report the inline-block baseline (last line box, unless overflow is not visible) to Parley, and drop the root line-height floor workaround now that Parley adds the strut and ancestor inline boxes itself.
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Blitz real-page benchmark: Barack Obama Wikipedia article, 1280 pxHeadless parse + style + layout of Variants
Identical features for all (default features of the root Methodmkdir -p ~/bench/obama && cd ~/bench/obama
curl -L -o obama.html https://en.wikipedia.org/wiki/Barack_Obama
# the two load.php stylesheets referenced by <link rel=stylesheet> were fetched to style0.css / style1.css
# and the <link href> attributes rewritten to those local names. No other network input.
# one git worktree per variant, scratch example copied in (not committed anywhere):
cp obama_bench.rs ~/repos/blitz-{A,B,C,D}/examples/
(cd ~/repos/blitz-$V && cargo build --release --example obama_bench)
# 3 interleaved rounds A B C D, then medians of the per-run medians:
for round in 1 2 3; do for v in A B C D; do
/usr/bin/time -v ~/repos/blitz-$v/target/release/examples/obama_bench ~/bench/obama/obama.html 20 3
done; done
Sanity check that all variants do the same work: an instrumented build counted the items Blitz walks in Results (median of 3 interleaved rounds; run 2, which includes D)
Per-round medians (first / relayout, ms) — run 2: A 282/158, 269/146, 265/147 · B 848/741, 847/735, 845/734 · C 1070/959, 1080/950, 1068/961 · D 1045/925, 1050/931, 1051/941. Run 1 (A B C only) gave the same picture: A 267/161, B 853/742, C 1088/952. Run-to-run noise is ~±3%. Where the time goes (perf,
|
| A | B | C | D | |
|---|---|---|---|---|
| total | 1 670 | 5 906 | 7 206 | 7 188 |
parley::layout::line::GlyphRunIter::next (called from Blitz compute_inline_layout_inner's for item in line.items() loop) |
216 | 3 826 | 4 929 | 4 980 |
of which libc memcpy (self) |
— | 2 093 | 3 286 | 3 427 |
build_inline_layout_into / parley::builder::build_into_layout (tree build + shaping + style metrics) |
448 | 524 | 484 | 517 |
fontique::Query::matches_with (all callers) |
104 | 102 | 21 | 97 |
parley::layout::style_metrics::resolve_style_metrics |
— | 16.5 | 6.5 | — |
harfrust … hb_font_t::shape (sanity, should be constant) |
112 | 111 | 103 | 110 |
Interpretation
-
The 3–4× regression from A to B/C/D is not Add patch for naga to fix macOS support #18 and not the cache; it is Parley
mainvs the0.11.1release, and it is entirely inGlyphRunIter::next. D (Blitzmaincode, only the Parley pin moved to Parleymain) is as slow as C. Blitz'scompute_inline_layout_inneriteratesline.items()for every line to position inline boxes;GlyphRunIter::nextre-walksrun.visual_clusters().flat_map(|c| c.glyphs()).skip(glyph_start)from the start of the run for every glyph run (already O(glyphs × glyph-runs) in 0.11.1), and on Parleymaineach step now copies a largeCluster(Copystruct holding anAtom/ShapedSlice) — therep movsbmemcpy is 35–46% of the whole run. Same item/run/glyph counts in A and B, ~18× more time per item. This is worth fixing upstream (keep a cursor inGlyphRunIterinstead ofskip, and/or slimCluster); it dwarfs everything else on this page. -
Is Add patch for naga to fix macOS support #18's regression visible on a real page? Barely. Comparing B with D (same Parley base, ± Add patch for naga to fix macOS support #18): the parley construction stage (
build_inline_layout_into) is 524 vs 517 ms over 8 layouts (~+1%),resolve_style_metricscosts 16.5 ms per 8 layouts (~2 ms per full page), and heap grows 84.6 → 91.1 MB (+6.5 MB live, plausibly the per-layoutstyle_metrics/ per-runbox_metricsdata kept for every inline context) — that last part is the most tangible cost of Add patch for naga to fix macOS support #18 here. Wall-time-wise B is actually faster than D (847 vs 1050 ms), but see 4. -
Does the cache recover it? Yes for the part it targets. In C,
fontique::Query::matches_withdrops from 102 ms to 21 ms (−80%) andresolve_style_metricsfrom 16.5 to 6.5 ms per 8 layouts; total construction time 524 → 484 ms (−8%, i.e. ~5 ms per full-page layout). Only 6 primary-font entries and 84 metrics entries are needed for the whole article, so the cache's own footprint is negligible: heap after layout is identical to B to within 10 kB (91.08 vs 91.07 MB), peak RSS 199.5 vs 199.4 MB. Relative to A's absolute numbers (~270 ms/page) those 5 ms are ~2%; relative to the Tango micro-benchmarks they line up with the 5–13% Latin wins there. -
Unexplained: B is ~20% faster end-to-end than both C and D, and perf puts all of that difference in
GlyphRunIter::next/memcpy (3 826 vs 4 929/4 980 ms), a code path neither add support for font-size #19 nor Blitz Implementvertical-alignvia Parley #832 touches (line.rs is byte-identical between B and C; Cluster/Run types are unchanged). Shaping and query time in C are lower than B, so this is not the cache being slow. The most likely explanation is codegen (inlining / copy-elision) differences in theflat_map().skip()chain between the three builds; I did not chase it further because the fix in point 1 makes the whole path irrelevant. Treat the B-vs-C wall-clock gap as noise from that path, and the stage-level numbers above as the signal.
Memory summary
| A | B | C | D | |
|---|---|---|---|---|
| peak RSS | 172.8 MB | 199.4 MB | 199.5 MB | 186.9 MB |
| live heap after layout | 77.5 MB | 91.3 MB | 91.3 MB | 84.7 MB |
Parley main vs 0.11.1: +7 MB; #18: +6.5 MB; #19 cache: +~10 kB (6 + 84 entries).
Caveats
- Images / web fonts are not loaded (only the HTML and its two stylesheets), so text is laid out in the system fallback fonts available on the VM; font-face-heavy pages would exercise the metrics cache more (here only 84 (font, size) pairs).
- The relayout path re-shapes all text (Blitz has no cheaper incremental relayout that keeps shaped text), so "steady-state relayout" ≈ "layout without parsing".
- D required 5 one-line API shims in Blitz
mainto compile against Parleymain(InlineBox::baseline,append_inline_box_to_linearity,Cluster::style_index, plus Implementvertical-alignvia Parley #832'sblitz-paint/src/text.rsdiff); none are on a timed path. - Nothing from this benchmark was committed to any PR branch; the worktrees/scratch example live only on the benchmark VM.
obama_bench.rs (scratch, not committed)
//! Scratch benchmark: parse + style + layout of a local HTML file with Blitz's headless path.
//!
//! Usage: obama_bench <path/to/page.html> [iterations] [warmups]
//! Outputs one JSON line with the timings.
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Instant;
use blitz_dom::DocumentConfig;
use blitz_html::HtmlDocument;
use blitz_traits::net::{Bytes, NetHandler, NetProvider, Request};
use blitz_traits::shell::{ColorScheme, Viewport};
struct Counting;
static ALLOCATED: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let p = unsafe { System.alloc(layout) };
if !p.is_null() {
let cur = ALLOCATED.fetch_add(layout.size(), Ordering::Relaxed) + layout.size();
PEAK.fetch_max(cur, Ordering::Relaxed);
}
p
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
ALLOCATED.fetch_sub(layout.size(), Ordering::Relaxed);
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let p = unsafe { System.realloc(ptr, layout, new_size) };
if !p.is_null() {
if new_size >= layout.size() {
let cur =
ALLOCATED.fetch_add(new_size - layout.size(), Ordering::Relaxed) + new_size
- layout.size();
PEAK.fetch_max(cur, Ordering::Relaxed);
} else {
ALLOCATED.fetch_sub(layout.size() - new_size, Ordering::Relaxed);
}
}
p
}
}
#[global_allocator]
static GLOBAL: Counting = Counting;
/// Serves `file://` URLs synchronously from disk; ignores everything else (no network).
struct LocalFiles;
impl NetProvider for LocalFiles {
fn fetch(&self, _doc_id: usize, request: Request, handler: Box<dyn NetHandler>) {
if request.url.scheme() != "file" {
return;
}
let Ok(path) = request.url.to_file_path() else {
return;
};
let Ok(bytes) = std::fs::read(&path) else {
eprintln!("missing local file: {}", path.display());
return;
};
handler.bytes(request.url.to_string(), Bytes::from(bytes));
}
}
const WIDTH: u32 = 1280;
const HEIGHT: u32 = 800;
fn viewport(scale: f32) -> Viewport {
Viewport::new(WIDTH, HEIGHT, scale, ColorScheme::Light)
}
fn build(html: &str, base_url: &str, net: &Arc<LocalFiles>) -> HtmlDocument {
let mut doc = HtmlDocument::from_html(
html,
DocumentConfig {
base_url: Some(base_url.to_string()),
net_provider: Some(Arc::clone(net) as _),
viewport: Some(viewport(1.0)),
..Default::default()
},
);
// Stylesheets are delivered synchronously by `LocalFiles`; two resolves make sure
// any nested `@import`s delivered during the first are applied too.
doc.resolve(0.0);
doc.resolve(0.0);
doc
}
fn stats(mut v: Vec<f64>) -> (f64, f64) {
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
let median = v[v.len() / 2];
let p90 = v[((v.len() as f64 * 0.9).ceil() as usize).min(v.len()) - 1];
(median, p90)
}
fn main() {
let path = std::env::args().nth(1).expect("html path");
let iterations: usize = std::env::args()
.nth(2)
.and_then(|s| s.parse().ok())
.unwrap_or(20);
let warmups: usize = std::env::args()
.nth(3)
.and_then(|s| s.parse().ok())
.unwrap_or(3);
let path = std::fs::canonicalize(&path).unwrap();
let html = std::fs::read_to_string(&path).unwrap();
let base_url = format!("file://{}", path.display());
let net = Arc::new(LocalFiles);
let heap_before = ALLOCATED.load(Ordering::Relaxed);
// (i) full parse + style + layout, repeated from scratch
let mut full = Vec::new();
let mut doc = None;
let mut heap_after_first_layout = 0;
let mut root_height = 0.0;
for i in 0..warmups + iterations {
let start = Instant::now();
let d = build(&html, &base_url, &net);
let elapsed = start.elapsed().as_secs_f64() * 1e3;
if i == 0 {
heap_after_first_layout = ALLOCATED.load(Ordering::Relaxed) - heap_before;
root_height = d.as_ref().root_element().final_layout().size.height;
}
if i >= warmups {
full.push(elapsed);
}
doc = Some(d);
}
let mut doc = doc.unwrap();
let heap_after_layout = ALLOCATED.load(Ordering::Relaxed) - heap_before;
// (ii) steady-state relayout: toggling the scale invalidates every inline (text)
// layout, rebuilds the stylist device (restyle) and re-runs layout.
let scales = [1.0_f32, 1.00001_f32];
let mut relayout = Vec::new();
for i in 0..warmups + iterations {
doc.as_mut().set_viewport(viewport(scales[i % 2]));
let start = Instant::now();
doc.resolve(0.0);
let elapsed = start.elapsed().as_secs_f64() * 1e3;
if i >= warmups {
relayout.push(elapsed);
}
}
let (full_med, full_p90) = stats(full);
let (re_med, re_p90) = stats(relayout);
#[allow(unused_mut)]
let mut cache = String::from("null");
#[cfg(font_cache)]
{
let (p, m) = doc.as_ref().font_ctx_cache_len();
cache = format!("[{p}, {m}]");
}
println!(
"{{\"first_layout_ms\": {{\"median\": {full_med:.2}, \"p90\": {full_p90:.2}}}, \
\"relayout_ms\": {{\"median\": {re_med:.2}, \"p90\": {re_p90:.2}}}, \
\"heap_after_first_layout_mb\": {:.2}, \"heap_after_layout_mb\": {:.2}, \
\"heap_peak_mb\": {:.2}, \"root_height\": {root_height:.1}, \"cache_entries\": {cache}}}",
heap_after_first_layout as f64 / 1e6,
heap_after_layout as f64 / 1e6,
PEAK.load(Ordering::Relaxed) as f64 / 1e6,
);
}
WPT follow-up for Blitz #832 / Parley #18 (
|
| Repo | PR | Base | Content |
|---|---|---|---|
| parley | DioxusLabs/parley#20 (head 1fd63fba72b38df9ec14892bd6f20e96fe2c1176) |
devin/1788383056-vertical-align |
trailing/consecutive hanging spaces no longer start an extra line; Style::parent() + Line::style_baseline() made public; 2 regression tests + 5 snapshots |
| blitz | #833 | devin/1788386630-vertical-align |
decorations painted at the decorating box's baseline (blitz-paint/src/text.rs::decorating_box_baseline); parley repinned to 1fd63fba72b38df9ec14892bd6f20e96fe2c1176 |
Verification: parley cargo fmt --all / cargo test --workspace (172 parley_tests + others, all ok) / cargo clippy --workspace --all-targets -D warnings clean. Blitz cargo fmt --check, cargo check --workspace --all-targets, cargo clippy -p blitz-dom -D warnings clean. (cargo clippy -p blitz-paint -D warnings fails on a pre-existing needless_return at packages/blitz-dom/src/mutator.rs:1250, untouched by these PRs and also present on the #832 head.)
Per-directory pass counts
| Directory | main | #832 | #832 + fixes (#833) |
|---|---|---|---|
css/CSS2/linebox |
137/201 | 169/201 | 169/201 |
css/CSS2/text |
268/410 | 270/410 | 271/410 |
css/css-text |
982/1872 | 1006/1872 | 1021/1872 |
css/css-inline |
47/234 | 49/234 | 49/234 |
css/CSS2/visudet |
15/39 | 17/39 | 17/39 |
css/CSS2/normal-flow |
648/772 | 648/772 | 648/772 |
css/CSS2/css1 |
94/164 | 116/164 | 116/164 |
css/css-values |
229/496 | 232/496 | 232/496 |
| total | 2420/4188 | 2507/4188 | 2523/4188 |
#832 → #832+fixes: 19 tests FAIL→PASS, 3 PASS→FAIL (all three are white-space: break-spaces tests, see rows below — they passed on #832 only because of the wrong break-after-first-hanging-space behaviour, since Blitz maps break-spaces to pre-wrap; stylo_to_parley.rs:291).
Newly passing: CSS2/text/text-decoration-va-length-001.xht, css-text/white-space/pre-wrap-00{1..7}.html, pre-wrap-leading-spaces-015/017, white-space-pre-wrap-trailing-spaces-005/007/008/011/021, hanging-whitespace-004, trailing-space-align-start.tentative, line-break-anywhere-and-white-space-006, hyphens-auto-002.
Root causes fixed
- (a) Parley – hanging spaces (
parley/src/layout/line_break.rs:1060-1085, theis_space && Wrapoverflow branch). After appending an overflowing space the breaker always calledstart_new_line. Effects:"AAA "at end of layout → a second, empty line (every block ending in a space got a phantom line box;layout.height()doubled);"XXXX XX"in 4ch broke after the first space instead of hanging both. Fix:continuewhen the next atom is another U+0020, or when this is the last atom of the last item. NBSP deliberately not chained (Whitespace::Spaceonly; a broader variant chaining every space-class regressedwhite-space-intrinsic-size-001). Tests:trailing_whitespace_does_not_add_line,vertical_align_style_baseline. - (b) Blitz – decoration baseline (
packages/blitz-paint/src/text.rs,stroke_text). Decorations were positioned at each glyph run's shifted baseline; CSS 2 §16.3.1 positions them from the decorating box. Fix walksStyle::parent()from the run's style to the style whose brush is the decorating node and usesLine::style_baseline. Fixestext-decoration-va-length-001.
Investigated failing tests
Status columns: main → #832 → #832+fixes.
| Test | Class | Diagnosis | Fix / reason not fixed | Status |
|---|---|---|---|---|
css/CSS2/text/text-decoration-va-length-001.xht |
b | underline of <u> followed the vertical-align: <length> child's shifted glyph baseline (blitz-paint/src/text.rs, DecorationRunGeometry.baseline = glyph_run.baseline()) |
blitz#833 | F→F→P |
css/css-text/white-space/pre-wrap-001…007.html, white-space-pre-wrap-trailing-spaces-005/007/008/011/021, pre-wrap-leading-spaces-015/017, hanging-whitespace-004, trailing-space-align-start.tentative, CSS2/text/white-space-processing-002/003/008/009/010 (the last five only pass with line-height: normal → metrics, see question 1) |
a | second and following hanging spaces were pushed to a new line; trailing space at end of content created an empty line box (line_break.rs:1060) |
parley#20 | F→F→P (the white-space-processing-* five: F→F→F) |
css/css-text/line-break/line-break-anywhere-and-white-space-006.html, css/css-text/hyphens/hyphens-auto-002.html |
a | same phantom trailing line | parley#20 | F→F→P |
css/css-text/white-space/break-spaces-001.html, break-spaces-010.html, white-space-intrinsic-size-001.html |
c | white-space: break-spaces is mapped to Preserve/pre-wrap (stylo_to_parley.rs:289-291 TODO) so spaces hang instead of wrapping and don't count toward min-content; passed on #832 only via the bug fixed above |
needs a break-spaces mode in Parley (WhiteSpaceCollapse::BreakSpaces: spaces are break opportunities and never hang) |
P→P→F |
css/CSS2/visudet/line-height-204.html |
d (+ design question) | Test sets line-height on one div to getComputedStyle(other).height and requires equal baselines. Font Revalia.woff has positive hhea.descender = 382 (non-standard sign), which skrifa/parley read as descent −18.65px at 100px (ascent 104.8). Blitz maps line-height: normal to 1.2 × font-size = 120px (stylo_to_parley.rs:354), so the strut baseline is at 105+floor((120−86)/2)=122; the 20px inline-block sitting on the baseline extends the line box to 122px, the script then sets line-height: 122px → half-leading 18 → baseline 123 → 1px shift. Browsers compute normal from font metrics (ascent+descent+lineGap = 86px here), which the test relies on. |
not fixed: changing normal → LineHeight::MetricsRelative(1.0) was tried; net +2 (+8/−6: fixes white-space-processing-002/003/008/009/010, pre-wrap-008/009, block-in-inline-first-line-001; regresses inline-formatting-context-012, block-formatting-contexts-011, inline-block-width-002a/b, overflow-wrap-cluster-001/002) and doesn't fix this test either; it is a design decision for #832 owner (question 1) |
P→F→F |
css/CSS2/linebox/baseline-block-with-overflow-001.html |
b (undiagnosed detail) | inline-block whose only child is an `overflow: hidden | scroll | autoblock. Blitz uses taffy'sbaselines.last.or(first) (layout/inline.rs:~660); taffy 0.14 compute_block_layoutsynthesizes a baseline from a scroll-container child's border-box bottom, so the inline-block gets a baseline where CSS 2 §10.8.1 says "bottom margin edge" *only for the inline-block itself*. Pixel diff: first XX groups aligned, later groups off. Suppressing baselines ofoverflow != visibleblock containers inlayout/mod.rs` compiled but did not make the test pass, so the wrong detail is not yet isolated (could also be the inline-block's own margin handling). |
css/CSS2/linebox/vertical-align-negative-leading-001.html |
a (undiagnosed detail) | negative half-leading (line-height < content area); line-box blue spans matched the ref at sampled columns but glyph/inline-box y differs, so the strut is right and the shift/quantisation of the child is off. Not narrowed to a line. |
not fixed | F→F→F |
css/CSS2/linebox/empty-inline-002/003.xht, inline-box-001/002.xht, border-padding-bleed-001…003.xht, split-inline-borders.html, inline-formatting-context-002…007/022/023.xht |
c | need inline border/padding/margin boxes on non-atomic inlines (border: 25px solid on <span>, etc.); Blitz has no inline fragment boxes |
missing feature | F→F→F |
css/CSS2/linebox/vertical-align-baseline-003.xht, -004a.xht, -005a.xht, -006.xht |
b (not individually diagnosed) | all are inline-block baseline = last line box, with margins (-004a: margin-bottom: 99px, margin: 8px); the inline-block baseline path is inline.rs ibox.baseline = (margin.top + baseline) * scale; source read, screenshots not compared |
not fixed | F→F→F |
css/CSS2/linebox/vertical-align-baseline-009.xht, vertical-align-applies-to-007/015.xht, line-height-applies-to-015.xht |
c | display: table-caption / table-cell vertical-align and table layout |
tables unsupported | F→F→F |
css/CSS2/linebox/vertical-align-117a/118a.xht, -121.xht |
b/a (not individually diagnosed) | text-bottom with margin: auto -1em on inline-blocks (117a/118a); top/bottom inline-blocks with border-bottom (121); source read only |
not fixed | F→F→F |
css/CSS2/linebox/line-height-125.xht |
c | padding-top on an inline <fontsize> element (inline padding box) |
missing feature | F→F→F |
css/CSS2/linebox/inline-negative-margin-001.html |
b (not diagnosed) | inline-block with margin-left: 10ch |
not fixed | F→F→F |
css/CSS2/visudet/inline-block-baseline-001…006/011/014/016 |
b (not individually diagnosed) | inline-block baseline family; also fail on main | not fixed | F→F→F |
css/CSS2/visudet/line-height-205.html |
d | same Revalia / line-height: normal issue as -204 |
see question 1 | F→F→F |
css/CSS2/visudet/content-height-004/005.html, replaced-elements-*.html |
c | replaced-element sizing, not line-box geometry | out of scope | F→F→F |
css/css-inline/alignment-baseline/alignment-baseline-horizontal-002/003.html, alignment-baseline-nested-001/002.html |
c | need central/ideographic/mathematical/alphabetic/hanging baseline tables (BASE table / synthesized) — only `baseline |
middle | text-top |
css/css-inline/alignment-baseline/*-vertical-rl-*, dominant-baseline/dominant-baseline-mixed-writing-modes-002.html |
c | vertical writing modes | out of scope (the latter's main PASS was a false positive: unsupported writing-mode rendering happened to match) | P→F→F / F→F→F |
css/css-inline/baseline-shift/baseline-shift-sub-super.html, baseline-shift-length-percentage.html |
d / c | sub/super use WebKit's font-size/5, /3; the ref is built with explicit lengths matching the reference browser's constants. Percentage test also uses baseline-source. A spec-neutral alternative would be the font's OS/2 subscriptYOffset/superscriptYOffset, which is what css-inline-3 suggests when available |
not changed (constants are a design choice, see question 2) | F→F→F |
css/css-inline/baseline-shift/*-svg.html, baseline-shift-top/bottom/center-svg |
c | SVG text | out of scope | F→F→F |
css/css-inline/empty-span-size-001/002.html |
c | empty <span> with padding/border (inline padding box); 002's main PASS was accidental (no vertical shift applied to the empty box) |
missing feature | F→F→F / P→F→F |
css/css-values/lh-rlh-on-root-001.html, lh-unit-003/004.html |
c | lh/rlh units with line-height: normal need the metrics-based normal (Stylo resolves lh from the computed value; normal has none) |
see question 1 | F→F→F |
css/CSS2/normal-flow/inline-block-zorder-002/004.xht |
b (undiagnosed) | overlapping red/green inline-blocks; looked at paint order, no explanation found | not fixed | F→F→F |
Not investigated: failures whose feature flags/contents are floats, direction/writing-mode, calc, script-driven, text-box-trim, ruby, initial-letter, hanging-punctuation, text-wrap balance/pretty, hyphenation, text-autospace, letter-spacing, text-align/justify.
Questions for the #832 design
line-height: normal= 1.2 × font-size (stylo_to_parley.rs:330,354, kept from main). Browsers use ascent+descent+lineGap of the primary font. Switching toparley::LineHeight::MetricsRelative(1.0)is net +2 on the eight directories (list above) and is whatline-height-204/205,lh-unit-003/004,white-space-processing-*assume, but it changes the height of every block of text in Blitz. Not done here; flagged.sub/superconstants (style_metrics.rs:255-258): WebKit/Blink constants chosen; Firefox uses the font's OS/2 sub/superscript offsets.baseline-shift-sub-super.htmlcannot pass with either unless the reference browser's choice is matched.text-top/text-bottomuse the parent's font ascent/descent (content area) — matches spec; no test was found failing because of this.
Not fixed but in scope (honest gaps)
baseline-block-with-overflow-001, vertical-align-negative-leading-001, the inline-block baseline families (vertical-align-baseline-003…006, visudet/inline-block-baseline-*, vertical-align-117a/118a/121, inline-negative-margin-001), and inline-block-zorder-002/004 were opened and screenshot-compared (or source-read as marked) but the wrong line was not isolated within this session; they were not changed.
Blitz #832 (
|
| build | what | purpose |
|---|---|---|
main |
Blitz main, parley 0.11.1 (crates.io, 92b63a2) |
baseline |
main+parley-main |
Blitz main + parley main (83acf8f, the merge-base of parley#18) with API-only shims |
isolates the dependency bump from the vertical-align logic |
#832 |
Blitz #832 head a875dd47 |
reproduce |
All 51 regressions reproduce locally on #832 vs main (statuses in the table below are local; the "fixed" column is the head of blitz#834 + parley#22).
Key finding: 29 of the 51 also fail on main+parley-main, i.e. they are caused by the 70-commit parley bump (0.11.1 → 83acf8f: #734 NBSP preservation, #738 spacing model, #697 CSS line-box sizing, #743 negative half-leading, …) and not by parley#18 / blitz#832's vertical-align code. The remaining 22 are attributable to the vertical-align branches; of those, 9 were genuine bugs and are fixed, 13 are pre-existing gaps that the old always-tall strut / ignored-inline-box-baseline behaviour masked.
PRs opened
- Parley: Fix line-box content detection for negative-height boxes, NBSP width handling and inline box quantization parley#22 (
devin/1788404063-vertical-align-fixes→devin/1788383056-vertical-align, head62c35980d58afa6e13fc1caa148fa346c086f530), 3 regression tests inparley/src/tests/test_style_metrics.rs. - Blitz: Fix atomic-inline baseline export, empty line boxes and root decoration baseline #834 (
devin/1788404083-vertical-align-fixes→devin/1788386630-vertical-align), repins parley to62c35980….
Validation: parley cargo fmt/test/clippy -D warnings clean; blitz cargo check --workspace --all-targets, cargo clippy -p blitz-dom -D warnings, cargo fmt --check clean. Full css/ WPT run of the fixed head vs pristine #832: 18 FAIL→PASS, 1 PASS→FAIL (css/css-contain/contain-size-064.html, see "Exposed gap" below), no subtest regressions. The 8 non-listed improvements (absolute-non-replaced-max-height-002, multi-line-row-flex-fragmentation-081a–d-print, svg-g-no-size-container, flex-direction-row-reverse, flexbox-align-self-vert-003) all come from the negative-margin / quantization fixes to add_inline_box.
Shared root causes (clusters)
| cluster | shared cause | class |
|---|---|---|
| NBSP (zorder-002/004/005, inline-table-zorder-001, t41-html4-keywords-a, contain-animation-001, highlight-001/002, empty-span-size-002) | parley #734 stopped collapsing U+00A0 but is_space_or_nbsp() still classified it as trailing/hangable space in calculate_content_widths (parley/src/layout/data.rs:373), so -only shrink-to-fit boxes had 0 min/max-content width. On main old parley collapsed NBSP to empty text, which sent Blitz down the collapse-through path (blitz-dom/src/layout/inline.rs:192-240) producing 0×0 boxes — the "passing" tests were test==ref both blank. |
a (width) / c (what the width now exposes) |
| Atomic-inline baseline export (baseline-of-scrollable-1a/1b, single-axis, contain-layout-baseline-001/005, grid-baseline-, grid-inline-, list-and-block-in-inline, font-family-applies-to-005, flexbox-baseline-multi-line-vert-002) | #832 started honouring InlineBox::baseline from taffy. Blitz's own mapping (inline.rs:300-360) was wrong for scroll containers / contain:layout (fixed, b). The rest are taffy 0.14 limits: block containers only export a first baseline (taffy/src/compute/block.rs:712 Baselines::from_first) and child scroll-container baselines are clamped, not synthesized (block.rs:1501); grid only measures item baselines when ≥2 items baseline-align (grid/track_sizing.rs:496-530). On main the baseline was ignored (box bottom sat on the baseline), which happened to coincide with the expected result. |
b + c |
| Root strut now always present (contain-size-flexbox-002, intrinsic-percent-replaced-008/dynamic-008, abs-pos-with-replaced-child, ruby-overhang-spaces-022) | Old inline layout dropped the strut on lines containing only inline boxes; #18 applies CSS2 §10.8.1 strut (style-table entry 0) correctly. Tests that relied on the missing strut (vertical writing modes, scrollbars, ruby) now expose the actual unsupported feature. | c |
| Ruby (ruby-003, br-clear-all-000, empty-ruby-, ruby-base-container-) | display: ruby* is not implemented — construct.rs has no DisplayInside::Ruby* arm so <ruby>/<rt> fall through to inline flow (construct.rs:599), with rt { font-size:50%; line-height:1 } from default.css:1009-1015. Bump-attributable: parley #697 CSS line-box sizing made <rt> text contribute its own inline box metrics; on main per-inline metrics were ignored. |
c |
| White-space / hanging (hanging-punctuation-first-002, full-width-leading-spaces-004, hanging-whitespace-002, trailing-ideographic-space-013/014, text-decoration-skip-spaces-001) | U+3000 and other Zs are Whitespace::None (parley_engine/src/shape/data.rs:239-250); hanging glyphs / hanging-punctuation are unimplemented. Bump-attributable (#738 spacing model stops mutating advances; these previously passed through accidental 0-advance handling). |
c |
| Floats (floats-141 fixed; floats-149) | floats-141: add_inline_box treated a finite negative-height box (negative margins) as no-content (a). floats-149: parley #697 line box sizing + float placement of an empty float in an inline with margin-left; not VA. |
a / c |
| lh/rlh, transforms, small-caps, line-height-204 | bump-attributable, see rows. | c |
Per-test table
Status columns: main / #832 (a875dd47) / fixed (blitz#834 head); x/y = passing subtests.
| # | test | class | root cause | fix | main → #832 → fixed |
|---|---|---|---|---|---|
| 1 | css/CSS2/floats-clear/floats-141.xht | a | Two parley#18 bugs in LineBoxMetrics::add_inline_box (parley/src/layout/line_break.rs:296): (1) a box whose ascent+descent <= 0 (negative margin-top atomic inline) was treated as no content → strut dropped, line collapsed; (2) with quantize, ascent and descent were rounded separately, reserving 1px more than the box's own (integral) height when the baseline split it into .5 halves. |
parley#22 | PASS → FAIL → PASS |
| 2 | css/CSS2/floats-clear/floats-149.xht | c | Fails on main+parley-main (not VA). Empty float:left inside an inline with margin-left:40px gets placed on line 1 after parley #697 line-box sizing changed where the float anchor is measured (inline.rs float placement via break_remaining). Old behaviour passed by accident. |
won't fix: parley bump / float placement, unrelated to vertical-align | PASS → FAIL → FAIL |
| 3 | css/CSS2/fonts/font-family-applies-to-005.xht | c | Inline-block with 3 block lines: taffy 0.14 block.rs:1493-1513 only computes Baselines::first, last is None, so inline.rs:337-350 falls back to the first-line baseline (14px) instead of the last (52.4px, CSS2 §10.8.1) → line grows to 59px and box shifts 1–2px. Main ignored the baseline (bottom edge on baseline = same as last-line for this test). Verified with dbg: first=Some(14), last=None. Not a font-fallback issue. |
won't fix: needs taffy last-baseline for block containers | PASS → FAIL → FAIL |
| 4 | css/CSS2/linebox/baseline-block-with-overflow-001.html | c | Confirmed #832's explanation: inline-block with overflow:hidden on an inner block child; taffy block.rs:1501 clamps the child scroll-container baseline instead of synthesizing from its bottom margin edge (css-align-3 §9.1). Main passed because the inline-block's baseline was ignored. |
won't fix: taffy | PASS → FAIL → FAIL |
| 5 | css/CSS2/normal-flow/inline-block-zorder-002.xht | c | NBSP cluster. -only positioned block was 0×0 on main (collapse-through, inline.rs:192-240) so test and ref were both blank. Now it has size and paints in tree order after the inline-block: blitz-paint/src/render.rs:965 draw_children has no CSS2 Appendix E phase separation (in-flow block backgrounds must paint before atomic-inline content). |
won't fix: pre-existing paint-order gap | PASS → FAIL → FAIL |
| 6 | css/CSS2/normal-flow/inline-block-zorder-004.xht | a | NBSP cluster: calculate_content_widths (data.rs:373) trimmed NBSP as trailing whitespace → inline-block collapsed to 0 width. |
parley#22 | PASS → FAIL → PASS |
| 7 | css/CSS2/normal-flow/inline-block-zorder-005.xht | a | same as #6 | parley#22 | PASS → FAIL → PASS |
| 8 | css/CSS2/normal-flow/inline-table-zorder-001.xht | c | same mechanism as #5 (inline-table instead of inline-block); Appendix E ordering. | won't fix: paint order | PASS → FAIL → FAIL |
| 9 | css/CSS2/text/text-decoration-va-length-001.xht | b | blitz-paint/src/text.rs flush_line_decorations (≈:395): a decorating inline root with no run of its own fell back to the first descendant run's geometry, including its vertical-align-shifted baseline, so the underline moved with the shifted child. Now uses the line baseline for the root. (#832 called this pre-existing; it is a fixable painting bug.) |
blitz#834 | PASS → FAIL → PASS |
| 10 | css/CSS2/visudet/line-height-204.html | c | Fails on main+parley-main. Revalia.woff has a positive hhea.descender; parley_engine FontMetrics::from_skrifa negates it → descent −18.65. parley #743 (preserve negative half-leading) makes the strut over=122/under=−2, so used height ≠ line-height: normal; the test's explicit 122px then puts the baseline 1px lower. Not #18. |
won't fix: font-metric sign handling in parley_engine (parley bump) | PASS → FAIL → FAIL |
| 11 | css/css-align/baseline-of-scrollable-1a.html | b | inline.rs:300-350 exported the content baseline of an overflow:hidden inline-block. css-align-3 §9.1: a block-axis scroll container's baseline is its bottom margin edge. Now is_block_axis_scroll_container ⇒ exports_baseline = false (bottom-edge fallback). |
blitz#834 | PASS → FAIL → PASS |
| 12 | css/css-align/baseline-of-scrollable-1b.html | c | Scroll container is a child block of the inline-block; taffy block.rs:1501 clamps its baseline to the border box instead of synthesizing from the bottom margin edge. Main passed because inline-block baseline was ignored. |
won't fix: taffy | PASS → FAIL → FAIL |
| 13 | css/css-align/baseline-of-single-axis-scroll-container.html | c | overflow-y: clip; overflow-x: scroll is fixed up by stylo to overflow-y: hidden (css-overflow-3 §3.1 computed-value rule), so inline.rs:325 sees a two-axis scroll container and synthesizes the bottom-edge baseline; Chrome treats the clip axis as non-scrolling. Needs the pre-fixup value from stylo. |
won't fix: needs stylo-level info | 1/2 → 0/2 → 0/2 |
| 14 | css/css-break/ruby-003.html | c | Ruby cluster; fails on main+parley-main. <rt> laid out as inline text with own metrics (parley #697) makes the ruby line 2 lines tall; multicol + ruby unsupported. |
won't fix: ruby unimplemented | PASS → FAIL → FAIL |
| 15 | css/css-color/t41-html4-keywords-a.xht | a | NBSP cluster: swatches are -only inline-blocks that collapsed to 0 width. Geometry, not colour parsing. |
parley#22 | PASS → FAIL → PASS |
| 16 | css/css-contain/contain-animation-001.html | c | contain: size unimplemented (no Contain::SIZE handling in blitz-dom/taffy). Test passed on main only because the child was 0×0, giving the 100×100 the ref expects by accident. |
won't fix: contain:size unsupported | PASS → FAIL → FAIL |
| 17 | css/css-contain/contain-layout-baseline-001.html | b | contain: layout suppresses baseline export (css-contain-1 §3.2); inline.rs exported the content baseline. Now contain_layout ⇒ exports_baseline = false. |
blitz#834 | PASS → FAIL → PASS |
| 18 | css/css-contain/contain-layout-baseline-005.html | b | same as #17 (fieldset variant). | blitz#834 | PASS → FAIL → PASS |
| 19 | css/css-contain/contain-size-flexbox-002.html | d/c | Chrome/Firefox pass only because overflow:scroll adds a ≈15px scrollbar making both the flex item and the inline-block ≥16px tall. Blitz has 0-width scrollbars, so flex row = 2px vs inline-block row = 16px (strut). Passed on main because the old inline layout omitted the strut on inline-box-only lines (a bug). |
won't fix: depends on scrollbar geometry | PASS → FAIL → FAIL |
| 20 | css/css-flexbox/flexbox-baseline-multi-line-vert-002.html | c | Test side is correct (first baseline 13). The ref is misrendered: .flexContainer > * { display:inline-block; width/height:20px } matches <br>, and construct.rs:981/1192 only special-cases <br> when its display is inline, so the <br> becomes a 20×20 atomic inline and a 2nd line. Masked on main because inline-box baselines were ignored. |
won't fix: <br> with non-inline display (pre-existing) |
PASS → FAIL → FAIL |
| 21 | css/css-flexbox/flexbox_inline.html | b | Atomic inline with negative vertical margins: inline.rs:357 clamped the reserved height to ≥0 and inline.rs:875 positioned with margin.top.max(0), so the box no longer sat on the baseline that parley#18 now honours. Baseline-bearing boxes now use the (possibly negative) margin box. |
blitz#834 | PASS → FAIL → PASS |
| 22 | css/css-fonts/small-caps-letter-spacing-002.html | c | Fails on main+parley-main; unrelated to vertical-align. Most likely parley #738 (spacing model) / #731 (ligature suppression with letter-spacing) changed spacing of synthesized small-caps; not narrowed further. |
won't fix: parley bump, not investigated beyond attribution | PASS → FAIL → FAIL |
| 23 | css/css-grid/alignment/grid-baseline-001.html | c | Baseline cluster: taffy 0.14 grid only measures item baselines when ≥2 items participate in baseline alignment (grid/track_sizing.rs:496-530), else container baseline = first item's bottom (grid/mod.rs:853). Now that the inline-grid's baseline is used by parley, the wrong one shows. |
won't fix: taffy | PASS → FAIL → FAIL |
| 24 | css/css-grid/alignment/grid-baseline-002.html | c | same as #23 | won't fix: taffy | PASS → FAIL → FAIL |
| 25 | css/css-grid/alignment/grid-inline-baseline.html | c | same as #23 | won't fix: taffy | PASS → FAIL → FAIL |
| 26 | css/css-grid/grid-items/grid-inline-items-002.html | c | same as #23 | won't fix: taffy | PASS → FAIL → FAIL |
| 27 | css/css-highlight-api/painting/custom-highlight-painting-inheritance-001.html | c | Highlight API painting unimplemented. Test & ref both contain -only spans which were 0-width on main, so neither showed green → accidental match. |
won't fix: Highlight API unsupported | PASS → FAIL → FAIL |
| 28 | css/css-highlight-api/painting/custom-highlight-painting-inheritance-002.html | c | same as #27 | won't fix | PASS → FAIL → FAIL |
| 29 | css/css-inline/dominant-baseline/dominant-baseline-mixed-writing-modes-002.html | c | Confirmed #832's explanation: ref uses alignment-baseline: text-top (now mapped in stylo_to_parley.rs:301-303) while the test uses dominant-baseline (unsupported) plus vertical writing modes (unsupported). |
won't fix | PASS → FAIL → FAIL |
| 30 | css/css-inline/empty-span-size-002.html | b | Parley (since the bump, incl. #18) lays out empty text as one strut-height line; Blitz only short-circuited empty content when the block had no border/padding (inline.rs:192-240), so a bordered block with only empty spans got a 19px line. Fix: has_inline_content == false ⇒ height 0, no baselines (CSS2 §9.4.2). (#832 called this pre-existing; it is fixable in Blitz.) |
blitz#834 | PASS → FAIL → PASS |
| 31 | css/css-lists/list-and-block-in-inline.html | c | Same as #3: taffy block containers export only the first baseline (block.rs:712); an inline-block's baseline is its last line box. Main matched by accident (bottom edge). |
won't fix: taffy last-baseline | PASS → FAIL → FAIL |
| 32 | css/css-ruby/br-clear-all-000.html | c | Ruby cluster (bump). <rt> as inline text with own line-height contributes extra height; clear + ruby annotation overflow into padding not modelled. |
won't fix: ruby unimplemented | 1/1 → 0/1 → 0/1 |
| 33 | css/css-ruby/empty-ruby-base-container.html | c | Ruby cluster (bump): a<ruby><rt>b</rt></ruby>c with line-height:3 — <rt> (50% font, line-height:1) is an inline with its own metrics, ref has no annotation. |
won't fix | PASS → FAIL → FAIL |
| 34 | css/css-ruby/empty-ruby-text-container-abs.html | c | Ruby cluster (bump), as #33. | won't fix | PASS → FAIL → FAIL |
| 35 | css/css-ruby/empty-ruby-text-container-float.html | c | Ruby cluster (bump), as #33. | won't fix | PASS → FAIL → FAIL |
| 36 | css/css-ruby/ruby-base-container-abs.html | c | Ruby cluster (bump), as #33 (+ NBSP now has width). | won't fix | PASS → FAIL → FAIL |
| 37 | css/css-ruby/ruby-base-container-float.html | c | Ruby cluster (bump), as #33. | won't fix | PASS → FAIL → FAIL |
| 38 | css/css-ruby/ruby-overhang-spaces-022.html | c | Ruby unsupported: <rt> laid out as inline text contributes its own inline-box metrics to the line (25px vs 24px in ref) now that #18 honours per-inline metrics. |
won't fix | PASS → FAIL → FAIL |
| 39 | css/css-sizing/intrinsic-percent-replaced-008.html | c | Vertical writing modes unsupported. Abs-pos block containing only a canvas: with a proper root strut the line is canvas + strut descent (104px) instead of 100. Chrome gets 100 because in vertical-lr the strut extends the inline size. Main passed because the strut was dropped on inline-box-only lines. |
won't fix: vertical writing modes | PASS → FAIL → FAIL |
| 40 | css/css-sizing/intrinsic-percent-replaced-dynamic-008.html | c | same as #39 | won't fix | PASS → FAIL → FAIL |
| 41 | css/css-text-decor/text-decoration-skip-spaces-001.html | c | White-space cluster (bump): U+2000–U+200A/U+205F/U+3000/U+1680 are Whitespace::None (parley_engine/src/shape/data.rs:239-250), so they neither hang nor are skipped by the underline; text-decoration-skip not implemented. |
won't fix | PASS → FAIL → FAIL |
| 42 | css/css-text/hanging-punctuation/hanging-punctuation-first-002.html | c | hanging-punctuation unimplemented; U+3000 is Whitespace::None. Bump-attributable. |
won't fix | PASS → FAIL → FAIL |
| 43 | css/css-text/white-space/full-width-leading-spaces-004.html | c | U+3000 not treated as hangable space for min-content sizing (data.rs:373 only knows Space/NBSP). Bump-attributable. |
won't fix | PASS → FAIL → FAIL |
| 44 | css/css-text/white-space/hanging-whitespace-002.tentative.html | c | pre-wrap spaces followed by an unconditionally-hanging U+3000 must hang; U+3000 isn't a hanging glyph in parley. Bump-attributable. | won't fix | PASS → FAIL → FAIL |
| 45 | css/css-text/white-space/trailing-ideographic-space-013.html | c | Trailing U+3000 must hang; Whitespace::None. Bump-attributable. |
won't fix | PASS → FAIL → FAIL |
| 46 | css/css-text/white-space/trailing-ideographic-space-014.html | c | same as #45 | won't fix | PASS → FAIL → FAIL |
| 47 | css/css-text/white-space/white-space-zero-fontsize-001.html | c | Parley emits a phantom line for a block-final \n (line_break.rs pushes an empty run for the trailing newline; the height correction never fires because item_range is non-empty — same on parley main). Browsers create no line box for a block-final forced break. On main both test (2 lines) and ref (3 lines) were 45px due to the old copy-previous-line hack; #18 (1c93d69) correctly sizes the phantom line by the newline's font-size:0 style so the test is 31px (= browsers) but the ref is still 45px. |
won't fix here: parley block-final-newline line box (pre-existing) | PASS → FAIL → FAIL |
| 48 | css/css-transforms/3d-point-mapping-2-transforminterop.html | c | 5/12 → 2/12 on main+parley-main too: elementFromPoint through 3D transforms; the 3 lost subtests depend on text line geometry changed by parley #697/#743, not on vertical-align (stale-baseline hit-testing ruled out: identical on #832 and fixed head). |
won't fix: transform hit-testing (bump) | 5/12 → 2/12 → 2/12 |
| 49 | css/css-values/lh-rlh-on-root-001.html | c | 4/8 → 2/8 on main+parley-main too. lh/rlh on the root resolve against the root's line-height: normal, whose value changed with parley #743 (negative half-leading preserved). Not #18. |
won't fix: parley bump / root lh resolution | 4/8 → 2/8 → 2/8 |
| 50 | css/css-viewport/zoom/vertical-align.html | c | stylo 0.20: inherit on a reset zoom-dependent property copies the parent's computed value verbatim (properties.mako.rs inherit_*; cascade.rs:1134-1141 only rescales inherited props). Firefox fails this test too (wpt.fyi). Masked on main because vertical-align lengths weren't implemented at all. |
won't fix: stylo | PASS → FAIL → FAIL |
| 51 | css/css-writing-modes/abs-pos-with-replaced-child.html | c | same mechanism as #39 (vertical writing mode; root strut adds descent under the replaced element). | won't fix | PASS → FAIL → FAIL |
Totals: a 4 (floats-141, zorder-004, zorder-005, t41) · b 6 (text-decoration-va-length-001, baseline-of-scrollable-1a, contain-layout-baseline-001/005, flexbox_inline, empty-span-size-002) · c 40 · d 1 (contain-size-flexbox-002, also c). 10 of 51 fixed; 29 of the remaining 41 are the parley dependency bump rather than #832/#18.
Exposed gap introduced by the fixes
css/css-contain/contain-size-064.html PASS → FAIL on the fixed head. contain: size is unimplemented; the test passed only because its -only grid cells had zero min-content width on both test and ref. With the (spec-correct) NBSP width fix the ref shows the green cells the test can't produce. Same mechanism as #16 (contain-animation-001). Flagging rather than reverting: the NBSP fix repairs 3 listed tests + 8 others.
Things deliberately not changed / questions
- Line-break-time NBSP handling (
parley/src/layout/line_break.rs:944 is_space_or_nbsp()) was left as-is. Changing it to== Spacemakes 8 more tests pass (letter-spacing-justify-001,text-justify-and-trailing-spaces-001…004,white-space-nowrap-011,word-break-break-all-018/021) but regressesline-break-anywhere-overrides-uax-behavior-004/006. Worth a separate parley PR with an investigation of theline-break: anywhereinteraction. - No vertical-align semantics were changed (root strut = style entry 0, parent-relative alignment, compound
VerticalAlign { alignment, shift }all intact). Thequantizeparameter ofLineBoxMetrics::add_inline_boxis now unused (kept to avoid changingappend_inline_box_to_line's public signature, whichparley_tests/tests/floats.rs:183calls) — question: should the publicquantizearg be dropped in a follow-up? - Implement
vertical-alignvia Parley #832's description explanations:baseline-block-with-overflow-001anddominant-baseline-mixed-writing-modes-002are confirmed pre-existing gaps;text-decoration-va-length-001andempty-span-size-002were actually fixable in Blitz and are fixed in Fix atomic-inline baseline export, empty line boxes and root decoration baseline #834.
…seline - Inline-blocks that are block-axis scroll containers export no baseline (bottom margin edge is used); other atomic inlines (flex/grid/table) keep their content baseline, clamped to the border box when scrollable. contain: layout boxes export no baseline. - Atomic inlines with a baseline may reserve negative space in the line (negative vertical margins) and are positioned from that baseline. - An inline root with no text and no inline boxes is a zero-height line box and exports no baselines. - Text decorations on the inline root fall back to the line baseline rather than the (vertical-align shifted) first run's baseline. - Repin parley to ac4c61c33c5bbc63303570b336b4e27cfa0cdd83.
… NBSP width fixes)
…faces without U+0020)
Summary
Companion to DioxusLabs/parley#18 (Parley now builds the CSS inline alignment tree itself: root strut, ancestor inline boxes, parent-relative baseline shifts,
top/bottomaligned subtrees).Cargo.tomlpins Parley to that branch's head (6268e53); this should be repointed once #18 lands.stylo_to_parley::vertical_align: Stylo storesvertical-alignas the css-inline-3 longhands, and Parley'sVerticalAlign { alignment, shift }is the same compound, so both are mapped losslessly:alignment-baseline(baseline|text-top|text-bottom|middle) →parley::AlignmentBaseline,baseline-shift(sub|super|top|bottom|<length-percentage>) →parley::BaselineShift. Percentages resolve against the element's own line-height.baseline-shift: centerhas no Parley equivalent yet and is approximated asmiddle;baseline-sourceis ignored.TextStyle::vertical_alignon every span andInlineBox::vertical_alignon atomic inlines.InlineBox::baselineis now populated for inline-blocks from taffy'sbaselines.last.or(first)(+ top margin), orNone(bottom margin edge) whenoverflowisn't visible / there are no line boxes (CSS 2 §10.8.1). Inline layout now also reportsbaselines.lastupward.root_line_heightfloor on each span'sline-height— Parley adds the strut and every ancestor span's own box to the line, so the floor was double counting and broke shifted spans.append_inline_box_to_line(.., NEG_INFINITY, NEG_INFINITY, ..)so they contribute no height.Cluster::style_index()replacesGlyph::style_index();Run::font_metrics(),run.font().font,NormalizedCoord::to_bits().WPT (
css/CSS2/linebox css/css-inline css/CSS2/text)454 → 490 tests passing, 1198 → 1299 subtests, 0 crashes. Four previously-passing tests now fail; all are pre-existing gaps that the old always-tall strut happened to hide:
text-decoration-va-length-001.xht— Blitz paints decorations per glyph run at the run's (now shifted) baseline; they should sit at the decorating ancestor's baseline. Needs decoration propagation in blitz-paint.linebox/baseline-block-with-overflow-001.html— taffy block layout propagates a child's baseline through anoverflow: hiddenblock; should synthesize from the bottom margin edge (taffy issue).css-inline/empty-span-size-002.html— Blitz doesn't model inline border/padding boxes, so an empty bordered span doesn't make the line visible.dominant-baseline-mixed-writing-modes-002.html— vertical writing modes /dominant-baselineunsupported.WPT results
Subtests: 321 newly passing, 48 newly failing (net +273).
Full diff (272 changed tests)
Generated by the WPT workflow.
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