diff --git a/Cargo.lock b/Cargo.lock index fed7872c4..9d2deb933 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3274,9 +3274,8 @@ dependencies = [ [[package]] name = "fontique" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6688bc1294fe7117d788937b6c53480169b29c566954af490830d4c09da9516a" +version = "0.11.0" +source = "git+https://github.com/DioxusLabs/parley?rev=0384e4d22e34fb9603b3eadb97236cc286e4f391#0384e4d22e34fb9603b3eadb97236cc286e4f391" dependencies = [ "hashbrown 0.17.1", "linebender_resource_handle", @@ -5943,36 +5942,47 @@ dependencies = [ [[package]] name = "parlance" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6937eda350acc1a5d05872c3cbf99fe78619c269096e2be3d4a350058639d5" +source = "git+https://github.com/DioxusLabs/parley?rev=0384e4d22e34fb9603b3eadb97236cc286e4f391#0384e4d22e34fb9603b3eadb97236cc286e4f391" [[package]] name = "parley" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22d2ff88bd3f7d68d1d9b09c7e6209f9a8e8c05088295140a2bcf2e9b17038c5" +version = "0.11.0" +source = "git+https://github.com/DioxusLabs/parley?rev=0384e4d22e34fb9603b3eadb97236cc286e4f391#0384e4d22e34fb9603b3eadb97236cc286e4f391" dependencies = [ "fontique", - "harfrust", "hashbrown 0.17.1", - "icu_normalizer", - "icu_properties", - "icu_segmenter", "linebender_resource_handle", "parlance", - "parley_data", + "parley_engine", "skrifa", + "smallvec", ] [[package]] name = "parley_data" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1567535334d6ba2d3cde19221ba9a7bd0fabb3cbd99046ddfb10ae061cfcc889" +version = "0.11.0" +source = "git+https://github.com/DioxusLabs/parley?rev=0384e4d22e34fb9603b3eadb97236cc286e4f391#0384e4d22e34fb9603b3eadb97236cc286e4f391" dependencies = [ "icu_properties", ] +[[package]] +name = "parley_engine" +version = "0.11.0" +source = "git+https://github.com/DioxusLabs/parley?rev=0384e4d22e34fb9603b3eadb97236cc286e4f391#0384e4d22e34fb9603b3eadb97236cc286e4f391" +dependencies = [ + "fontique", + "harfrust", + "hashbrown 0.17.1", + "icu_normalizer", + "icu_properties", + "icu_segmenter", + "linebender_resource_handle", + "parlance", + "parley_data", + "skrifa", +] + [[package]] name = "paste" version = "1.0.15" diff --git a/Cargo.toml b/Cargo.toml index 45bddd3e4..b9121994b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,7 +107,7 @@ taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "1b918bafcab101dd23 "calc", "detailed_layout_info", ] } -parley = { version = "0.11.1", default-features = false, features = ["std"] } +parley = { git = "https://github.com/DioxusLabs/parley", rev = "0384e4d22e34fb9603b3eadb97236cc286e4f391", 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/construct.rs b/packages/blitz-dom/src/layout/construct.rs index e1131e082..d23c7b39c 100644 --- a/packages/blitz-dom/src/layout/construct.rs +++ b/packages/blitz-dom/src/layout/construct.rs @@ -286,15 +286,6 @@ fn push_non_whitespace_children_and_pseudos(layout_children: &mut ThinVec f32 { - match line_height { - parley::LineHeight::FontSizeRelative(relative) => relative * font_size, - parley::LineHeight::Absolute(absolute) => absolute, - parley::LineHeight::MetricsRelative(relative) => relative * font_size, //unreachable!(), - } -} - /// Result of classifying the in-flow children of a flow container as /// all-block, all-inline and/or all-out-of-flow. struct FlowClassification { @@ -1036,8 +1027,6 @@ pub(crate) fn build_inline_layout_into( .map(|s| stylo_to_parley::style(inline_context_root_node_id, s)) .unwrap_or_default(); - let root_line_height = resolve_line_height(parley_style.line_height, parley_style.font_size); - // Create a parley tree builder let mut builder = layout_ctx.tree_builder(font_ctx, scale, true, &parley_style); @@ -1086,7 +1075,6 @@ pub(crate) fn build_inline_layout_into( before_id, collapse_mode, text_transform, - root_line_height, ); } for child_id in root_node.children.iter().copied() { @@ -1097,7 +1085,6 @@ pub(crate) fn build_inline_layout_into( child_id, collapse_mode, text_transform, - root_line_height, ); } if let Some(after_id) = root_node.after() { @@ -1108,7 +1095,6 @@ pub(crate) fn build_inline_layout_into( after_id, collapse_mode, text_transform, - root_line_height, ); } @@ -1122,7 +1108,6 @@ pub(crate) fn build_inline_layout_into( node_id: NodeId, collapse_mode: WhiteSpaceCollapse, parent_text_transform: TextTransform, - root_line_height: f32, ) { let node = &nodes[node_id]; @@ -1179,7 +1164,6 @@ pub(crate) fn build_inline_layout_into( child_id, collapse_mode, text_transform, - root_line_height, ); } } @@ -1199,6 +1183,11 @@ pub(crate) fn build_inline_layout_into( // Width and height are set during layout width: 0.0, height: 0.0, + baseline: None, + vertical_align: node + .primary_styles() + .map(|s| stylo_to_parley::vertical_align(&s)) + .unwrap_or_default(), }); } else if *tag_name == local_name!("br") { // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX); @@ -1210,25 +1199,11 @@ pub(crate) fn build_inline_layout_into( builder.set_white_space_mode(collapse_mode); } else { // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX); - let mut style = node + let style = node .primary_styles() .map(|s| stylo_to_parley::style(node.id, &s)) .unwrap_or_default(); - // dbg!(&style); - - let font_size = style.font_size; - - // Floor the line-height of the span by the line-height of the inline context - // See https://www.w3.org/TR/CSS21/visudet.html#line-height - style.line_height = parley::LineHeight::Absolute( - resolve_line_height(style.line_height, font_size) - .max(root_line_height), - ); - - // dbg!(node_id); - // dbg!(&style); - builder.push_style_span(style); if let Some(before_id) = node.before() { @@ -1239,7 +1214,6 @@ pub(crate) fn build_inline_layout_into( before_id, collapse_mode, text_transform, - root_line_height, ); } @@ -1251,7 +1225,6 @@ pub(crate) fn build_inline_layout_into( child_id, collapse_mode, text_transform, - root_line_height, ); } if let Some(after_id) = node.after() { @@ -1262,7 +1235,6 @@ pub(crate) fn build_inline_layout_into( after_id, collapse_mode, text_transform, - root_line_height, ); } @@ -1279,6 +1251,11 @@ pub(crate) fn build_inline_layout_into( // Width and height are set during layout width: 0.0, height: 0.0, + baseline: None, + vertical_align: node + .primary_styles() + .map(|s| stylo_to_parley::vertical_align(&s)) + .unwrap_or_default(), }); } }; diff --git a/packages/blitz-dom/src/layout/inline.rs b/packages/blitz-dom/src/layout/inline.rs index 14362bc3a..b4f50d447 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 _, @@ -307,17 +310,58 @@ impl BaseDocument { let is_floated = false; 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 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 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 { ibox.width = 0.0; ibox.height = 0.0; + ibox.baseline = None; } 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 exports_baseline { + output + .baselines + .last + .or(output.baselines.first) + .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 + }; } } @@ -606,7 +650,13 @@ impl BaseDocument { // dbg!(&layout.size); // dbg!(&layout.location); - state.append_inline_box_to_line(box_break_data.advance, 0.0); + // Floats are out-of-flow and must not contribute to the line's height. + state.append_inline_box_to_line( + box_break_data.advance, + f32::NEG_INFINITY, + f32::NEG_INFINITY, + true, + ); // if float.is_floated() { // println!("INLINE FLOATED BOX ({}) {:?}", ibox.id, float); @@ -648,8 +698,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. // @@ -809,13 +869,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); } @@ -829,11 +894,14 @@ impl BaseDocument { // println!("known_dimensions: w: {:?} h: {:?}", inputs.known_dimensions.width, inputs.known_dimensions.height); // println!("\n"); - let first_baseline = inline_layout - .layout - .lines() - .next() - .map(|line| (line.metrics().baseline / scale) + container_pb.top); + let line_baseline = + |line: parley::Line<'_, _>| (line.metrics().baseline / scale) + container_pb.top; + 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] @@ -856,7 +924,10 @@ impl BaseDocument { bottom: content_extent.height, } }, - baselines: taffy::Baselines::from_first(first_baseline), + baselines: taffy::Baselines { + first: first_baseline, + last: last_baseline, + }, top_margin: CollapsibleMarginSet::ZERO, bottom_margin: CollapsibleMarginSet::ZERO, margins_can_collapse_through: !has_styles_preventing_being_collapsed_through diff --git a/packages/blitz-dom/src/node/node.rs b/packages/blitz-dom/src/node/node.rs index 3717cba6e..3fd81423d 100644 --- a/packages/blitz-dom/src/node/node.rs +++ b/packages/blitz-dom/src/node/node.rs @@ -1434,7 +1434,7 @@ impl Node { if let Some((cluster, _side)) = Cluster::from_point_exact(layout, x * scale, y * scale) { - let style_index = cluster.glyphs().next()?.style_index(); + let style_index = usize::from(cluster.style_index()); let node_id = layout.styles()[style_index].brush.id; let text_pointer_events_none = self .with(node_id) diff --git a/packages/blitz-dom/src/stylo_to_parley.rs b/packages/blitz-dom/src/stylo_to_parley.rs index e8da9ba13..6a5e60a2e 100644 --- a/packages/blitz-dom/src/stylo_to_parley.rs +++ b/packages/blitz-dom/src/stylo_to_parley.rs @@ -14,6 +14,8 @@ pub(crate) mod stylo { pub(crate) use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse; pub(crate) use style::properties::ComputedValues; pub(crate) use style::properties::style_structs::Font; + pub(crate) use style::values::computed::AlignmentBaseline; + pub(crate) use style::values::computed::BaselineShift; pub(crate) use style::values::computed::OverflowWrap; pub(crate) use style::values::computed::WordBreak; pub(crate) use style::values::computed::font::FontFeatureSettings; @@ -27,6 +29,7 @@ pub(crate) mod stylo { pub(crate) use style::values::computed::font::GenericFontFamily; pub(crate) use style::values::computed::font::LineHeight; pub(crate) use style::values::computed::font::SingleFontFamily; + pub(crate) use style::values::generics::box_::BaselineShiftKeyword; } pub(crate) mod parley { @@ -289,6 +292,57 @@ pub(crate) fn white_space_collapse(input: stylo::WhiteSpaceCollapse) -> parley:: } } +/// Map the css-inline-3 `alignment-baseline` and `baseline-shift` longhands (which Stylo +/// stores in place of the `vertical-align` shorthand) to Parley's `VerticalAlign`. +/// +/// Percentages are resolved against the element's own `line-height`. +pub(crate) fn vertical_align(style: &stylo::ComputedValues) -> parley::VerticalAlign { + let box_styles = style.get_box(); + let alignment = match box_styles.clone_alignment_baseline() { + stylo::AlignmentBaseline::Baseline => parley::AlignmentBaseline::Baseline, + stylo::AlignmentBaseline::TextTop => parley::AlignmentBaseline::TextTop, + stylo::AlignmentBaseline::TextBottom => parley::AlignmentBaseline::TextBottom, + stylo::AlignmentBaseline::Middle => parley::AlignmentBaseline::Middle, + }; + let shift = match box_styles.clone_baseline_shift() { + stylo::BaselineShift::Keyword(stylo::BaselineShiftKeyword::Sub) => { + parley::BaselineShift::Sub + } + stylo::BaselineShift::Keyword(stylo::BaselineShiftKeyword::Super) => { + parley::BaselineShift::Super + } + stylo::BaselineShift::Keyword(stylo::BaselineShiftKeyword::Top) => { + parley::BaselineShift::Top + } + stylo::BaselineShift::Keyword(stylo::BaselineShiftKeyword::Bottom) => { + parley::BaselineShift::Bottom + } + // TODO: `center` (align the aligned subtree's centre with the line box's centre) is not + // representable in Parley yet. Approximate it with `middle`. + stylo::BaselineShift::Keyword(stylo::BaselineShiftKeyword::Center) => { + return parley::VerticalAlign::MIDDLE; + } + stylo::BaselineShift::Length(lp) => { + let shift = if lp.has_percentage() { + let font_styles = style.get_font(); + let font_size = font_styles.font_size.used_size.0.px(); + let line_height = match font_styles.line_height { + // TODO: `normal` resolves against the first available font's metrics, which + // aren't available here; 1.2 is the usual approximation. + stylo::LineHeight::Normal => font_size * 1.2, + stylo::LineHeight::Number(num) => font_size * num.0, + stylo::LineHeight::Length(value) => value.0.px(), + }; + lp.resolve(Length::new(line_height)).px() + } else { + lp.resolve(Length::new(0.0)).px() + }; + parley::BaselineShift::Length(shift) + } + }; + parley::VerticalAlign::new(alignment, shift) +} + pub(crate) fn style( span_id: NodeId, style: &stylo::ComputedValues, @@ -299,10 +353,11 @@ pub(crate) fn style( // Convert font size and line height let font_size = font_styles.font_size.used_size.0.px(); let line_height = match font_styles.line_height { - stylo::LineHeight::Normal => parley::LineHeight::FontSizeRelative(1.2), + stylo::LineHeight::Normal => parley::LineHeight::NORMAL, stylo::LineHeight::Number(num) => parley::LineHeight::FontSizeRelative(num.0), stylo::LineHeight::Length(value) => parley::LineHeight::Absolute(value.0.px()), }; + let vertical_align = self::vertical_align(style); let letter_spacing = itext_styles .letter_spacing @@ -383,6 +438,7 @@ pub(crate) fn style( font_features: parley::FontFeatures::List(Cow::Owned(font_features)), locale: Default::default(), line_height, + vertical_align, word_spacing, letter_spacing, text_wrap_mode, diff --git a/packages/blitz-paint/src/text.rs b/packages/blitz-paint/src/text.rs index 358c55881..3d7cb230a 100644 --- a/packages/blitz-paint/src/text.rs +++ b/packages/blitz-paint/src/text.rs @@ -54,7 +54,7 @@ pub(crate) fn draw_inline_backgrounds<'a>( continue; } - let metrics = glyph_run.run().metrics(); + let metrics = glyph_run.run().font_metrics(); let x = glyph_run.offset() as f64; let w = glyph_run.advance() as f64; let baseline = glyph_run.baseline() as f64; @@ -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; @@ -574,9 +589,9 @@ pub(crate) fn stroke_text<'a>( for item in line.items() { if let PositionedLayoutItem::GlyphRun(glyph_run) = item { let run = glyph_run.run(); - let font = run.font(); + let font = &run.font().font; let font_size = run.font_size(); - let metrics = run.metrics(); + let metrics = run.font_metrics(); let style = glyph_run.style(); let synthesis = run.synthesis(); let glyph_xform = synthesis @@ -620,11 +635,17 @@ pub(crate) fn stroke_text<'a>( kurbo::Vec2::default() }; + let normalized_coords: Vec = run + .normalized_coords() + .iter() + .map(|coord| coord.to_bits()) + .collect(); + scene.draw_glyphs( font, font_size, !FONT_EMBOLDEN_ENABLED, // hint - run.normalized_coords(), + &normalized_coords, embolden, Fill::NonZero, &anyrender::Paint::from(text_color), @@ -690,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, + ); } }