From 1c74ccf64ea41516c74842e4fed0c646191f3672 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 3 Sep 2026 03:01:33 +0000 Subject: [PATCH 1/2] Fix atomic inline baseline export, empty line boxes and decoration baseline - 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. --- Cargo.lock | 10 +-- Cargo.toml | 2 +- packages/blitz-dom/src/layout/inline.rs | 85 +++++++++++++++++++------ packages/blitz-paint/src/text.rs | 31 +++++++-- 4 files changed, 99 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c8959861..a458e0bd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3274,7 +3274,7 @@ dependencies = [ [[package]] name = "fontique" version = "0.11.0" -source = "git+https://github.com/DioxusLabs/parley?rev=ff838004f1912240ec1bfe488e106bd2cb8a9c70#ff838004f1912240ec1bfe488e106bd2cb8a9c70" +source = "git+https://github.com/DioxusLabs/parley?rev=ac4c61c33c5bbc63303570b336b4e27cfa0cdd83#ac4c61c33c5bbc63303570b336b4e27cfa0cdd83" dependencies = [ "hashbrown 0.17.1", "linebender_resource_handle", @@ -5932,12 +5932,12 @@ dependencies = [ [[package]] name = "parlance" version = "0.1.0" -source = "git+https://github.com/DioxusLabs/parley?rev=ff838004f1912240ec1bfe488e106bd2cb8a9c70#ff838004f1912240ec1bfe488e106bd2cb8a9c70" +source = "git+https://github.com/DioxusLabs/parley?rev=ac4c61c33c5bbc63303570b336b4e27cfa0cdd83#ac4c61c33c5bbc63303570b336b4e27cfa0cdd83" [[package]] name = "parley" version = "0.11.0" -source = "git+https://github.com/DioxusLabs/parley?rev=ff838004f1912240ec1bfe488e106bd2cb8a9c70#ff838004f1912240ec1bfe488e106bd2cb8a9c70" +source = "git+https://github.com/DioxusLabs/parley?rev=ac4c61c33c5bbc63303570b336b4e27cfa0cdd83#ac4c61c33c5bbc63303570b336b4e27cfa0cdd83" dependencies = [ "fontique", "hashbrown 0.17.1", @@ -5951,7 +5951,7 @@ dependencies = [ [[package]] name = "parley_data" version = "0.11.0" -source = "git+https://github.com/DioxusLabs/parley?rev=ff838004f1912240ec1bfe488e106bd2cb8a9c70#ff838004f1912240ec1bfe488e106bd2cb8a9c70" +source = "git+https://github.com/DioxusLabs/parley?rev=ac4c61c33c5bbc63303570b336b4e27cfa0cdd83#ac4c61c33c5bbc63303570b336b4e27cfa0cdd83" dependencies = [ "icu_properties", ] @@ -5959,7 +5959,7 @@ dependencies = [ [[package]] name = "parley_engine" version = "0.11.0" -source = "git+https://github.com/DioxusLabs/parley?rev=ff838004f1912240ec1bfe488e106bd2cb8a9c70#ff838004f1912240ec1bfe488e106bd2cb8a9c70" +source = "git+https://github.com/DioxusLabs/parley?rev=ac4c61c33c5bbc63303570b336b4e27cfa0cdd83#ac4c61c33c5bbc63303570b336b4e27cfa0cdd83" dependencies = [ "fontique", "harfrust", diff --git a/Cargo.toml b/Cargo.toml index 1a23b8828..42b8257c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,7 +107,7 @@ taffy = { version = "0.14.0", default-features = false, features = [ "calc", "detailed_layout_info", ] } -parley = { git = "https://github.com/DioxusLabs/parley", rev = "ff838004f1912240ec1bfe488e106bd2cb8a9c70", default-features = false, features = ["std"] } +parley = { git = "https://github.com/DioxusLabs/parley", rev = "ac4c61c33c5bbc63303570b336b4e27cfa0cdd83", default-features = false, features = ["std"] } skrifa = { version = "0.44", default-features = false, features = [ "std", ] } # Should match parley and vello versions diff --git a/packages/blitz-dom/src/layout/inline.rs b/packages/blitz-dom/src/layout/inline.rs index 6402b6016..87352442e 100644 --- a/packages/blitz-dom/src/layout/inline.rs +++ b/packages/blitz-dom/src/layout/inline.rs @@ -1,7 +1,10 @@ use blitz_traits::node_id::NodeId; use parley::{AlignmentOptions, IndentOptions}; -use style::values::specified::box_::DisplayOutside; -use style::values::{computed::CSSPixelLength, generics::text::GenericTextIndent}; +use style::values::specified::box_::{DisplayInside, DisplayOutside}; +use style::values::{ + computed::{CSSPixelLength, Contain}, + generics::text::GenericTextIndent, +}; use taffy::{ AvailableSpace, BlockContext, BlockFormattingContext, BoxSizing, CollapsibleMarginSet, CoreStyle as _, Direction, LayoutInput, LayoutOutput, LayoutPartialTree as _, MaybeMath as _, @@ -306,11 +309,22 @@ impl BaseDocument { let is_absolute = style.position() == Position::Absolute; // The baseline of an inline-block is the baseline of its last in-flow line box, - // unless it has no line boxes or its `overflow` is not `visible`, in which case - // it is the bottom margin edge (CSS 2 §10.8.1). + // unless it has no line boxes or it is a block-axis scroll container, in which + // case it is the bottom margin edge (CSS 2 §10.8.1, css-align-3 §9.1 + // `baseline-source: auto`; `overflow: clip` is not a scroll container). Other + // atomic inlines (flex, grid, table) export a baseline regardless of `overflow`, + // clamped to their border box if they are scroll containers (css-align-3 §9.1). + // A layout-contained box is treated as having no baseline (css-contain-1 §3.3). let overflow = style.overflow(); - let has_visible_overflow = - overflow.x == Overflow::Visible && overflow.y == Overflow::Visible; + let box_style = style.style.get_box(); + let is_flow = matches!( + box_style.display.inside(), + DisplayInside::Flow | DisplayInside::FlowRoot + ); + let is_scroll_container = !matches!(overflow.y, Overflow::Visible | Overflow::Clip); + let is_block_axis_scroll_container = is_flow && is_scroll_container; + let contain_layout = box_style.clone_contain().contains(Contain::LAYOUT); + let exports_baseline = !is_block_axis_scroll_container && !contain_layout; drop(style); if is_absolute || is_floated { @@ -320,18 +334,32 @@ impl BaseDocument { } else { let output = self.compute_child_layout(taffy::NodeId::from(ibox.id), child_inputs); ibox.width = (margin.left + margin.right + output.size.width) * scale; - // Vertical margins adjust the space the box reserves in the line, but the - // reserved space cannot be negative. - ibox.height = (margin.top + margin.bottom + output.size.height).max(0.0) * scale; - ibox.baseline = if has_visible_overflow { + ibox.baseline = if exports_baseline { output .baselines .last .or(output.baselines.first) - .map(|baseline| (margin.top + baseline) * scale) + .map(|baseline| { + let baseline = if is_scroll_container { + baseline.clamp(0.0, output.size.height) + } else { + baseline + }; + (margin.top + baseline) * scale + }) } else { None }; + // Vertical margins adjust the space the box reserves in the line. A box with a + // baseline splits that space into ascent (`margin.top + baseline`) and descent + // (`margin.bottom + height - baseline`), either of which may be negative. A box + // without a baseline sits on the baseline and cannot reserve negative space. + let margin_box_height = margin.top + margin.bottom + output.size.height; + ibox.height = if ibox.baseline.is_some() { + margin_box_height * scale + } else { + margin_box_height.max(0.0) * scale + }; } } @@ -668,8 +696,18 @@ impl BaseDocument { }, ); + // Parley lays out empty text as a single strut-height line (text-editor semantics), + // but a line box containing no text, inline boxes or other in-flow content is a + // zero-height line box in CSS (CSS2 §9.4.2). + let has_inline_content = + !inline_layout.text.is_empty() || !inline_layout.layout.inline_boxes().is_empty(); + #[allow(unused_mut)] - let mut height = inline_layout.layout.height(); + let mut height = if has_inline_content { + inline_layout.layout.height() + } else { + 0.0 + }; // HACK. TODO: fix in Parley. // @@ -829,13 +867,18 @@ impl BaseDocument { layout.size = size; layout.location.x = (ibox.x / scale) + margin.left + container_pb.left + inset_offset.x; - // A negative `margin-top` shrinks the space the box reserves in the + // A box with a baseline is positioned by it, so its border box always + // sits `margin.top` below the margin box (`ibox.y`). Without a baseline + // a negative `margin-top` shrinks the space the box reserves in the // line but does not move the box itself, which stays anchored to the // bottom of the reserved space. - layout.location.y = (ibox.y / scale) - + margin.top.max(0.0) - + container_pb.top - + inset_offset.y; + let margin_top = if ibox.baseline.is_some() { + margin.top + } else { + margin.top.max(0.0) + }; + layout.location.y = + (ibox.y / scale) + margin_top + container_pb.top + inset_offset.y; layout.padding = padding; //.map(|p| p / scale); layout.border = border; //.map(|p| p / scale); } @@ -851,8 +894,12 @@ impl BaseDocument { let line_baseline = |line: parley::Line<'_, _>| (line.metrics().baseline / scale) + container_pb.top; - let first_baseline = inline_layout.layout.lines().next().map(line_baseline); - let last_baseline = inline_layout.layout.lines().last().map(line_baseline); + let first_baseline = has_inline_content + .then(|| inline_layout.layout.lines().next().map(line_baseline)) + .flatten(); + let last_baseline = has_inline_content + .then(|| inline_layout.layout.lines().last().map(line_baseline)) + .flatten(); // Put layout back self.nodes[node_id] diff --git a/packages/blitz-paint/src/text.rs b/packages/blitz-paint/src/text.rs index fa4476f22..3d7cb230a 100644 --- a/packages/blitz-paint/src/text.rs +++ b/packages/blitz-paint/src/text.rs @@ -206,7 +206,8 @@ fn resolve_decoration_entry(doc: &BaseDocument, node_id: NodeId) -> DecorationSt /// decorating box (its own text), falling back to the first run it covers. #[derive(Clone)] struct DecorationRunGeometry { - /// The line's baseline (shared by every run on the line). + /// The baseline the decoration is positioned from (the run's own, `vertical-align` + /// shifted, baseline). baseline: f32, ascent: f32, descent: f32, @@ -391,15 +392,29 @@ fn flush_line_decorations( scale: f64, deco_boxes: &[LineDecoration], win_ascent_ratios: &mut WinAscentCache, + inline_root_id: NodeId, + line_baseline: f32, ) { // Draw innermost boxes first so ancestors' decorations paint on top, matching the // per-run drawing order this replaced (`stack.iter().rev()`). for acc in deco_boxes.iter().rev() { let deco = &acc.deco; // Prefer the decorating box's own font; fall back to the first run it covers. - let Some(geom) = acc.own.as_ref().or(acc.first.as_ref()) else { - continue; + let geom = match (&acc.own, &acc.first) { + (Some(own), _) => own.clone(), + (None, Some(first)) => { + let mut geom = first.clone(); + // A descendant run's baseline may be shifted by `vertical-align`, but the + // decoration is positioned from the decorating box's own baseline. For the + // inline root that is the line's baseline. + if acc.node_id == inline_root_id { + geom.baseline = line_baseline; + } + geom + } + (None, None) => continue, }; + let geom = &geom; let width = acc.max_x - acc.min_x; if width <= 0.0 { continue; @@ -696,7 +711,15 @@ pub(crate) fn stroke_text<'a>( } } - flush_line_decorations(scene, transform, scale, deco_boxes, win_ascent_ratios); + flush_line_decorations( + scene, + transform, + scale, + deco_boxes, + win_ascent_ratios, + inline_root_id, + line.metrics().baseline, + ); } } From 6a80c29bed999e892e03a8713ba536bf4d061d9e Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 3 Sep 2026 03:25:20 +0000 Subject: [PATCH 2/2] Repin parley to 62c3598 (inline box quantization fix) --- Cargo.lock | 10 +++++----- Cargo.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a458e0bd8..804cfbde3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3274,7 +3274,7 @@ dependencies = [ [[package]] name = "fontique" version = "0.11.0" -source = "git+https://github.com/DioxusLabs/parley?rev=ac4c61c33c5bbc63303570b336b4e27cfa0cdd83#ac4c61c33c5bbc63303570b336b4e27cfa0cdd83" +source = "git+https://github.com/DioxusLabs/parley?rev=62c35980d58afa6e13fc1caa148fa346c086f530#62c35980d58afa6e13fc1caa148fa346c086f530" dependencies = [ "hashbrown 0.17.1", "linebender_resource_handle", @@ -5932,12 +5932,12 @@ dependencies = [ [[package]] name = "parlance" version = "0.1.0" -source = "git+https://github.com/DioxusLabs/parley?rev=ac4c61c33c5bbc63303570b336b4e27cfa0cdd83#ac4c61c33c5bbc63303570b336b4e27cfa0cdd83" +source = "git+https://github.com/DioxusLabs/parley?rev=62c35980d58afa6e13fc1caa148fa346c086f530#62c35980d58afa6e13fc1caa148fa346c086f530" [[package]] name = "parley" version = "0.11.0" -source = "git+https://github.com/DioxusLabs/parley?rev=ac4c61c33c5bbc63303570b336b4e27cfa0cdd83#ac4c61c33c5bbc63303570b336b4e27cfa0cdd83" +source = "git+https://github.com/DioxusLabs/parley?rev=62c35980d58afa6e13fc1caa148fa346c086f530#62c35980d58afa6e13fc1caa148fa346c086f530" dependencies = [ "fontique", "hashbrown 0.17.1", @@ -5951,7 +5951,7 @@ dependencies = [ [[package]] name = "parley_data" version = "0.11.0" -source = "git+https://github.com/DioxusLabs/parley?rev=ac4c61c33c5bbc63303570b336b4e27cfa0cdd83#ac4c61c33c5bbc63303570b336b4e27cfa0cdd83" +source = "git+https://github.com/DioxusLabs/parley?rev=62c35980d58afa6e13fc1caa148fa346c086f530#62c35980d58afa6e13fc1caa148fa346c086f530" dependencies = [ "icu_properties", ] @@ -5959,7 +5959,7 @@ dependencies = [ [[package]] name = "parley_engine" version = "0.11.0" -source = "git+https://github.com/DioxusLabs/parley?rev=ac4c61c33c5bbc63303570b336b4e27cfa0cdd83#ac4c61c33c5bbc63303570b336b4e27cfa0cdd83" +source = "git+https://github.com/DioxusLabs/parley?rev=62c35980d58afa6e13fc1caa148fa346c086f530#62c35980d58afa6e13fc1caa148fa346c086f530" dependencies = [ "fontique", "harfrust", diff --git a/Cargo.toml b/Cargo.toml index 42b8257c8..90bbcedce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,7 +107,7 @@ taffy = { version = "0.14.0", default-features = false, features = [ "calc", "detailed_layout_info", ] } -parley = { git = "https://github.com/DioxusLabs/parley", rev = "ac4c61c33c5bbc63303570b336b4e27cfa0cdd83", default-features = false, features = ["std"] } +parley = { git = "https://github.com/DioxusLabs/parley", rev = "62c35980d58afa6e13fc1caa148fa346c086f530", default-features = false, features = ["std"] } skrifa = { version = "0.44", default-features = false, features = [ "std", ] } # Should match parley and vello versions