diff --git a/Cargo.lock b/Cargo.lock index f8bcb26dd..ddcd812f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2966,9 +2966,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=95a9ff1479533a03775b8e53587495dd41a01b74#95a9ff1479533a03775b8e53587495dd41a01b74" dependencies = [ "hashbrown 0.17.1", "linebender_resource_handle", @@ -5528,36 +5527,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=95a9ff1479533a03775b8e53587495dd41a01b74#95a9ff1479533a03775b8e53587495dd41a01b74" [[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=95a9ff1479533a03775b8e53587495dd41a01b74#95a9ff1479533a03775b8e53587495dd41a01b74" 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=95a9ff1479533a03775b8e53587495dd41a01b74#95a9ff1479533a03775b8e53587495dd41a01b74" dependencies = [ "icu_properties", ] +[[package]] +name = "parley_engine" +version = "0.11.0" +source = "git+https://github.com/DioxusLabs/parley?rev=95a9ff1479533a03775b8e53587495dd41a01b74#95a9ff1479533a03775b8e53587495dd41a01b74" +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" @@ -7398,7 +7408,7 @@ dependencies = [ [[package]] name = "taffy" version = "0.13.0" -source = "git+https://github.com/DioxusLabs/taffy?rev=0e33e961c4ecd36a38d5103f57f305fbe37d749f#0e33e961c4ecd36a38d5103f57f305fbe37d749f" +source = "git+https://github.com/DioxusLabs/taffy?rev=a0471381c63cd84622295f93a13b306034e392f4#a0471381c63cd84622295f93a13b306034e392f4" dependencies = [ "arrayvec", "serde", diff --git a/Cargo.toml b/Cargo.toml index 3dbdb4558..787cddf8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,7 +96,7 @@ dioxus-cli-config = { version = "0.7.3" } dioxus-core-macro = { version = "0.7.3" } # Taffy + Parley + Fontations -taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "0e33e961c4ecd36a38d5103f57f305fbe37d749f", default-features = false, features = [ +taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "a0471381c63cd84622295f93a13b306034e392f4", default-features = false, features = [ "std", "flexbox", "grid", @@ -105,7 +105,7 @@ taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "0e33e961c4ecd36a38 "calc", "detailed_layout_info", ] } -parley = { version = "0.11.1", default-features = false, features = ["std"] } +parley = { git = "https://github.com/DioxusLabs/parley", rev = "95a9ff1479533a03775b8e53587495dd41a01b74", 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 e4d00d2ae..dac0e95dc 100644 --- a/packages/blitz-dom/src/layout/construct.rs +++ b/packages/blitz-dom/src/layout/construct.rs @@ -4,15 +4,16 @@ use std::sync::Arc; use markup5ever::{QualName, local_name, ns}; use parley::{ - FontContext, InlineBox, InlineBoxKind, LayoutContext, StyleProperty, TreeBuilder, - WhiteSpaceCollapse, + FontContext, InlineBox, InlineBoxKind, InlineBoxVerticalAlign, LayoutContext, StyleProperty, + TreeBuilder, WhiteSpaceCollapse, }; use style::{ computed_values::position::T as PositionProperty, data::ElementData as StyloElementData, shared_lock::StylesheetGuards, values::{ - computed::{Content, ContentItem, Display, Float, TextTransform}, + computed::{BaselineShift, Content, ContentItem, Display, Float, TextTransform}, + generics::box_::BaselineShiftKeyword, specified::box_::{DisplayInside, DisplayOutside}, }, }; @@ -1031,6 +1032,7 @@ pub(crate) fn build_inline_layout_into( // Create a parley tree builder let mut builder = layout_ctx.tree_builder(font_ctx, scale, true, &parley_style); + builder.set_compute_strut(true); // Set whitespace collapsing mode let collapse_mode = root_node_style @@ -1190,6 +1192,10 @@ pub(crate) fn build_inline_layout_into( // Width and height are set during layout width: 0.0, height: 0.0, + baseline: None, + vertical_align: inline_box_vertical_align( + style.map(|s| s.clone_baseline_shift()), + ), }); } else if *tag_name == local_name!("br") { // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX); @@ -1270,6 +1276,10 @@ pub(crate) fn build_inline_layout_into( // Width and height are set during layout width: 0.0, height: 0.0, + baseline: None, + vertical_align: inline_box_vertical_align( + style.map(|s| s.clone_baseline_shift()), + ), }); } }; @@ -1298,3 +1308,15 @@ pub(crate) fn build_inline_layout_into( } } } + +/// Map the computed `baseline-shift` (the longhand behind `vertical-align: top`/`bottom`) +/// to parley's inline-box vertical alignment. +fn inline_box_vertical_align(baseline_shift: Option) -> InlineBoxVerticalAlign { + match baseline_shift { + Some(BaselineShift::Keyword(BaselineShiftKeyword::Top)) => InlineBoxVerticalAlign::Top, + Some(BaselineShift::Keyword(BaselineShiftKeyword::Bottom)) => { + InlineBoxVerticalAlign::Bottom + } + _ => InlineBoxVerticalAlign::Baseline, + } +} diff --git a/packages/blitz-dom/src/layout/inline.rs b/packages/blitz-dom/src/layout/inline.rs index 9449d211f..b0be5700b 100644 --- a/packages/blitz-dom/src/layout/inline.rs +++ b/packages/blitz-dom/src/layout/inline.rs @@ -1,12 +1,15 @@ 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::{ + computed::{CSSPixelLength, Contain}, + generics::text::GenericTextIndent, +}; use taffy::{ AvailableSpace, BlockContext, BlockFormattingContext, BoxSizing, CollapsibleMarginSet, - CoreStyle as _, Direction, LayoutInput, LayoutOutput, LayoutPartialTree as _, MaybeMath as _, - MaybeResolve as _, Overflow, Point, Position, RequestedAxis, ResolveOrZero as _, RunMode, Size, - SizingMode, + CoreStyle as _, Direction, Display, LayoutInput, LayoutOutput, LayoutPartialTree as _, + MaybeMath as _, MaybeResolve as _, Overflow, Point, Position, RequestedAxis, + ResolveOrZero as _, RunMode, Size, SizingMode, }; #[cfg(feature = "floats")] @@ -304,11 +307,54 @@ impl BaseDocument { ibox.width = 0.0; ibox.height = 0.0; } else { + let is_scroll_container = style.overflow.x.is_scroll_container() + || style.overflow.y.is_scroll_container(); + let display = style.display; + // Layout containment suppresses the box's baseline (css-contain §3) + let has_layout_containment = self.nodes[NodeId::from_u64(ibox.id)] + .primary_styles() + .is_some_and(|s| s.clone_contain().contains(Contain::LAYOUT)); let output = self.compute_child_layout(taffy::NodeId::from(ibox.id), child_inputs); + // Per CSS, an in-flow inline-block is baseline-aligned to the baseline of its + // last in-flow line box, while inline flex/grid containers use their first + // baseline. A box with no natural baseline uses its bottom margin edge + // (Parley's fallback when `baseline` is `None`, since `ibox.height` + // includes margins). A scroll container also uses its bottom margin edge + // if it is an inline-block, but flex/grid scroll containers still take + // their baseline from their content, clamped to the border box + // (css-align §9.1). + let baseline = match display { + Display::Flex | Display::Grid => output.baselines.first, + _ => output.baselines.last, + }; + let baseline = if has_layout_containment { + None + } else if is_scroll_container { + match display { + Display::Flex | Display::Grid => { + baseline.map(|b| b.min(output.size.height).max(0.0)) + } + _ => None, + } + } else { + baseline + }; + // Round to physical pixels so that content within the box (which is + // positioned relative to the box's top edge and pixel-snapped there) + // stays pixel-aligned after the box is shifted to sit on the + // (pixel-snapped) line baseline. + ibox.baseline = baseline.map(|baseline| ((margin.top + baseline) * scale).round()); 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; + // Vertical margins adjust the space the box reserves in the line. With an + // explicit baseline the ascent/descent contributions may legitimately be + // negative, but a bottom-aligned box (no baseline) contributes its height + // as ascent, and the space it reserves cannot be negative. + let height = margin.top + margin.bottom + output.size.height; + ibox.height = if ibox.baseline.is_some() { + height * scale + } else { + height.max(0.0) * scale + }; } } @@ -583,7 +629,7 @@ impl BaseDocument { // dbg!(&layout.size); // dbg!(&layout.location); - state.append_inline_box_to_line(box_break_data.advance, 0.0); + state.append_inline_box_to_line(box_break_data.advance, None, false); // if float.is_floated() { // println!("INLINE FLOATED BOX ({}) {:?}", ibox.id, float); @@ -792,11 +838,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 - // line but does not move the box itself, which stays anchored to the - // bottom of the reserved space. + // For a baseline-aligned box, `ibox.y` is the top of the margin box, + // so the border box sits `margin.top` below it. For a bottom-aligned + // box (no 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. + let margin_top_offset = if ibox.baseline.is_some() { + margin.top + } else { + margin.top.max(0.0) + }; layout.location.y = (ibox.y / scale) - + margin.top.max(0.0) + + margin_top_offset + container_pb.top + inset_offset.y; layout.padding = padding; //.map(|p| p / scale); @@ -817,6 +870,11 @@ impl BaseDocument { .lines() .next() .map(|line| (line.metrics().baseline / scale) + container_pb.top); + let last_baseline = inline_layout + .layout + .lines() + .next_back() + .map(|line| (line.metrics().baseline / scale) + container_pb.top); // Put layout back self.nodes[node_id] @@ -839,7 +897,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 cbe4a87de..d249f1eea 100644 --- a/packages/blitz-dom/src/node/node.rs +++ b/packages/blitz-dom/src/node/node.rs @@ -1260,8 +1260,8 @@ 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 node_id = layout.styles()[style_index].brush.id; + let style_index = cluster.style_index(); + let node_id = layout.styles()[usize::from(style_index)].brush.id; let text_pointer_events_none = self .with(node_id) .primary_styles() diff --git a/packages/blitz-paint/src/text.rs b/packages/blitz-paint/src/text.rs index 358c55881..781b8d557 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; @@ -576,7 +576,7 @@ pub(crate) fn stroke_text<'a>( let run = glyph_run.run(); let font = run.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 +620,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.font, font_size, !FONT_EMBOLDEN_ENABLED, // hint - run.normalized_coords(), + &normalized_coords, embolden, Fill::NonZero, &anyrender::Paint::from(text_color), @@ -649,7 +655,7 @@ pub(crate) fn stroke_text<'a>( underline_offset: metrics.underline_offset, underline_size: metrics.underline_size, strikethrough_size: metrics.strikethrough_size, - font: font.clone(), + font: font.font.clone(), font_size, css_font_size, }; diff --git a/packages/stylo_taffy/src/convert.rs b/packages/stylo_taffy/src/convert.rs index 048b1fd48..b4de7558f 100644 --- a/packages/stylo_taffy/src/convert.rs +++ b/packages/stylo_taffy/src/convert.rs @@ -414,9 +414,7 @@ pub fn item_alignment(input: stylo::AlignFlags, is_horiz_rtl: bool) -> Option Some(taffy::AlignItems::END), stylo::AlignFlags::CENTER => Some(taffy::AlignItems::CENTER), stylo::AlignFlags::BASELINE => Some(taffy::AlignItems::BASELINE), - // Taffy does not support last-baseline alignment, so map it to its - // fallback alignment of `self-end` (https://www.w3.org/TR/css-align-3/#baseline-values) - stylo::AlignFlags::LAST_BASELINE => Some(taffy::AlignItems::END), + stylo::AlignFlags::LAST_BASELINE => Some(taffy::AlignItems::LAST_BASELINE), // Should never be hit. But no real reason to panic here. _ => None, }?;