diff --git a/Cargo.lock b/Cargo.lock index f84ebd124e..768f9ff50f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4842,7 +4842,7 @@ dependencies = [ [[package]] name = "taffy" version = "0.13.0" -source = "git+https://github.com/ldm0/taffy?rev=d9915f3dbfa6dae40afd9e97d214b4220fd90359#d9915f3dbfa6dae40afd9e97d214b4220fd90359" +source = "git+https://github.com/ldm0/taffy?rev=b5a55e2b3b7aa3b71ae9f8dfb124fb018c4a2cf2#b5a55e2b3b7aa3b71ae9f8dfb124fb018c4a2cf2" dependencies = [ "arrayvec", "serde", diff --git a/Cargo.toml b/Cargo.toml index 562e52a600..9bb265fa61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,7 +102,7 @@ stylo_atoms = { git = "https://github.com/ldm0/stylo", rev = "cfa170da90c467e937 stylo_dom = { git = "https://github.com/ldm0/stylo", rev = "cfa170da90c467e93786ec0fa5526bd83f201933" } stylo_static_prefs = { git = "https://github.com/ldm0/stylo", rev = "cfa170da90c467e93786ec0fa5526bd83f201933" } stylo_traits = { git = "https://github.com/ldm0/stylo", rev = "cfa170da90c467e93786ec0fa5526bd83f201933" } -taffy = { git = "https://github.com/ldm0/taffy", rev = "d9915f3dbfa6dae40afd9e97d214b4220fd90359" } +taffy = { git = "https://github.com/ldm0/taffy", rev = "b5a55e2b3b7aa3b71ae9f8dfb124fb018c4a2cf2" } [workspace.lints.clippy] disallowed_methods = "deny" diff --git a/moli-layout/src/intrinsic.rs b/moli-layout/src/intrinsic.rs index c06adbcf09..f02d61e33d 100644 --- a/moli-layout/src/intrinsic.rs +++ b/moli-layout/src/intrinsic.rs @@ -18,26 +18,6 @@ impl LayoutWorld where N: Copy + Debug + Eq + Hash, { - /// Measure the border-box width selected by CSS fit-content sizing. - /// - /// Taffy's block and flex absolute-layout paths use the same two intrinsic - /// measurements internally. Moli also needs the operation at the - /// IFC seam, where an out-of-flow placeholder is owned by Parley and the - /// positioned box cannot remain a normal child of Taffy's numeric tree. - pub(crate) fn measure_fit_content_width( - &mut self, - child: LayoutBoxId, - inputs: LayoutInput, - available_width: f32, - ) -> f32 { - self.measure_fit_content_inline_size( - child, - inputs, - WritingMode::HorizontalTb, - available_width, - ) - } - pub(crate) fn measure_fit_content_inline_size( &mut self, child: LayoutBoxId, diff --git a/moli-layout/src/positioned.rs b/moli-layout/src/positioned.rs index ccc21407e2..55433a394e 100644 --- a/moli-layout/src/positioned.rs +++ b/moli-layout/src/positioned.rs @@ -89,13 +89,18 @@ impl PhysicalStaticPosition { } } - /// Fit-content width available on the side(s) selected by the static - /// position. A center edge constrains both sides, unlike a start point. - pub(crate) fn available_width(self, containing_width: f32) -> f32 { - let bounds = self - .horizontal_axis() - .inset_modified_bounds(containing_width); - bounds.end - bounds.start + /// Fit-content space on both physical axes selected by the static edges. + /// The child chooses its inline axis from its own writing mode; a center + /// edge constrains both sides, unlike a start point. + pub(crate) fn available_size(self, containing_size: Size) -> Size { + let extent = |axis: StaticPositionAxis, size| { + let bounds = axis.inset_modified_bounds(size); + bounds.end - bounds.start + }; + Size { + width: extent(self.horizontal_axis(), containing_size.width), + height: extent(self.vertical_axis(), containing_size.height), + } } fn horizontal_axis(self) -> StaticPositionAxis { @@ -110,6 +115,18 @@ impl PhysicalStaticPosition { } } + fn vertical_axis(self) -> StaticPositionAxis { + StaticPositionAxis { + offset: self.point.y, + edge: match self.vertical_edge { + VerticalStaticEdge::Top => PhysicalAxisStaticEdge::Min, + VerticalStaticEdge::Center => PhysicalAxisStaticEdge::Center, + VerticalStaticEdge::Bottom => PhysicalAxisStaticEdge::Max, + }, + safety: self.safety.height, + } + } + /// Resolve the border-box origin from the static margin-box edge and used /// margins. Auto margins on a static-position axis have already become zero /// during absolute sizing; this step must not distribute them a second time. @@ -122,15 +139,7 @@ impl PhysicalStaticPosition { containing_direction: Direction, ) -> Point { let horizontal = self.horizontal_axis(); - let vertical = StaticPositionAxis { - offset: self.point.y, - edge: match self.vertical_edge { - VerticalStaticEdge::Top => PhysicalAxisStaticEdge::Min, - VerticalStaticEdge::Center => PhysicalAxisStaticEdge::Center, - VerticalStaticEdge::Bottom => PhysicalAxisStaticEdge::Max, - }, - safety: self.safety.height, - }; + let vertical = self.vertical_axis(); Point { x: horizontal.border_box_start( containing_size.width, @@ -463,6 +472,25 @@ mod tests { direction: Direction::Ltr, }; + #[test] + fn static_position_constrains_both_physical_available_axes() { + let position = PhysicalStaticPosition::new( + Point { x: 40.0, y: 70.0 }, + HorizontalStaticEdge::Center, + VerticalStaticEdge::Bottom, + ); + assert_eq!( + position.available_size(Size { + width: 200.0, + height: 100.0 + }), + Size { + width: 80.0, + height: 70.0 + }, + ); + } + #[test] fn centered_static_position_centers_the_margin_box() { let position = PhysicalStaticPosition::new( diff --git a/moli-layout/src/taffy_tree.rs b/moli-layout/src/taffy_tree.rs index 19dc3a41a7..99fa5dcece 100644 --- a/moli-layout/src/taffy_tree.rs +++ b/moli-layout/src/taffy_tree.rs @@ -7,9 +7,9 @@ use inline_containing_block::InlineContainingBlocks; use parley::{AlignmentOptions, PositionedLayoutItem}; use style::Atom; use taffy::{ - AbsoluteAxis, AlignContent, AlignContentKeyword, AlignmentSafety, AutoSizeBehavior, - AvailableSpace, BlockContext, BlockFormattingContext, BoxSizing, CacheTree, DetailedGridInfo, - Dimension, Display, FlexDirection, Layout, LayoutBlockContainer, LayoutFlexboxContainer, + AlignContent, AlignContentKeyword, AlignmentSafety, AutoSizeBehavior, AvailableSpace, + BlockContext, BlockFormattingContext, BoxSizing, CacheTree, DetailedGridInfo, Dimension, + Display, FlexDirection, Layout, LayoutBlockContainer, LayoutFlexboxContainer, LayoutGridContainer, LayoutInput, LayoutOutput, LayoutPartialTree, LeafLayoutContext, Line, MaybeMath, MaybeResolve, NodeId, Point, ResolveOrZero, RoundTree, RunMode, Size, SizingMode, SizingPurpose, Style, TraversePartialTree, TraverseTree, compute_block_layout, @@ -1202,221 +1202,71 @@ fn layout_deferred_absolute_child( ) where N: Copy + Debug + Eq + Hash, { - let child_writing_mode = world.boxes[child.index()].style.writing_mode(); - let style = world.boxes[child.index()].style.taffy.clone(); + let style = &world.boxes[child.index()].style.taffy; if style.display == Display::None || style.position != taffy::Position::Absolute { return; } - let area_width = area.size.width; - let area_height = area.size.height; - let percentage_basis = area.writing_mode.to_logical(area.size).inline_size; - let aspect_ratio = style.aspect_ratio; - let margin = style - .margin - .map(|value| value.maybe_resolve(percentage_basis, resolve_stylo_calc_value)); - let padding = style - .padding - .resolve_or_zero(Some(percentage_basis), resolve_stylo_calc_value); - let border = style - .border - .resolve_or_zero(Some(percentage_basis), resolve_stylo_calc_value); - let padding_border_sum = (padding + border).sum_axes(); - let box_sizing_adjustment = if style.box_sizing == BoxSizing::ContentBox { - padding_border_sum - } else { - Size::ZERO - }; - let left = style - .inset - .left - .maybe_resolve(area_width, resolve_stylo_calc_value); - let right = style - .inset - .right - .maybe_resolve(area_width, resolve_stylo_calc_value); - let top = style - .inset - .top - .maybe_resolve(area_height, resolve_stylo_calc_value); - let bottom = style - .inset - .bottom - .maybe_resolve(area_height, resolve_stylo_calc_value); - let block_auto_behavior = match child_writing_mode.block_axis() { - AbsoluteAxis::Horizontal if left.is_some() && right.is_some() => { - AutoSizeBehavior::StretchExplicit - } - AbsoluteAxis::Vertical if top.is_some() && bottom.is_some() => { - AutoSizeBehavior::StretchExplicit - } - _ => AutoSizeBehavior::FitContent, - }; - let style_size = style - .size - .maybe_resolve(area.size, resolve_stylo_calc_value) - .maybe_apply_aspect_ratio(aspect_ratio) - .maybe_add(box_sizing_adjustment); - let min_size = style - .min_size - .maybe_resolve(area.size, resolve_stylo_calc_value) - .maybe_apply_aspect_ratio(aspect_ratio) - .maybe_add(box_sizing_adjustment) - .or(padding_border_sum.map(Some)) - .maybe_max(padding_border_sum); - let max_size = style - .max_size - .maybe_resolve(area.size, resolve_stylo_calc_value) - .maybe_apply_aspect_ratio(aspect_ratio) - .maybe_add(box_sizing_adjustment); - let mut known_dimensions = style_size.maybe_clamp(min_size, max_size); - - if let (None, Some(left), Some(right)) = (known_dimensions.width, left, right) { - known_dimensions.width = Some( - (area_width.maybe_sub(margin.left).maybe_sub(margin.right) - left - right).max(0.0), - ); - known_dimensions = known_dimensions - .maybe_apply_aspect_ratio(aspect_ratio) - .maybe_clamp(min_size, max_size); - } - if let (None, Some(top), Some(bottom)) = (known_dimensions.height, top, bottom) { - known_dimensions.height = Some( - (area_height.maybe_sub(margin.top).maybe_sub(margin.bottom) - top - bottom).max(0.0), - ); - known_dimensions = known_dimensions - .maybe_apply_aspect_ratio(aspect_ratio) - .maybe_clamp(min_size, max_size); - } - - let available_space = Size { - width: AvailableSpace::Definite(area_width.maybe_clamp(min_size.width, max_size.width)), - height: AvailableSpace::Definite(area_height.maybe_clamp(min_size.height, max_size.height)), - }; - if known_dimensions.width.is_none() { - // CSS 2.2 ยง10.3.7 resolves an auto-width absolute box with at least - // one auto horizontal inset as fit-content. Taffy's block/flex paths - // already implement this contract, but an IFC's Parley placeholder - // requires Moli to perform the same sizing at this custom seam. - let non_auto_margin_width = margin.left.unwrap_or(0.0) + margin.right.unwrap_or(0.0); - let available_width = match (left, right) { - (Some(left), None) => area_width - left, - (None, Some(right)) => area_width - right, - (None, None) => static_position.available_width(area_width), - (Some(_), Some(_)) => unreachable!("both insets already resolve auto width"), - } - non_auto_margin_width; - known_dimensions.width = Some(world.measure_fit_content_width( - child, - LayoutInput { - known_dimensions, - definite_dimensions: known_dimensions, - parent_size: area.size.map(Some), - parent_writing_mode: area.writing_mode, - available_space, - sizing_mode: SizingMode::ContentSize, - sizing_purpose: SizingPurpose::IntrinsicContribution, - run_mode: RunMode::ComputeSize, - axis: taffy::RequestedAxis::Horizontal, - block_auto_behavior, - block_margins_are_collapsible: Line::FALSE, - }, - available_width, - )); - known_dimensions = known_dimensions - .maybe_apply_aspect_ratio(aspect_ratio) - .maybe_clamp(min_size, max_size); - } - let measured_size = world - .compute_child_layout( - child.to_taffy(), - LayoutInput { - known_dimensions, - definite_dimensions: known_dimensions, - parent_size: area.size.map(Some), - parent_writing_mode: area.writing_mode, - available_space, - sizing_mode: SizingMode::ContentSize, - sizing_purpose: SizingPurpose::Layout, - run_mode: RunMode::ComputeSize, - axis: taffy::RequestedAxis::Both, - block_auto_behavior, - block_margins_are_collapsible: Line::FALSE, - }, - ) - .size; - let final_size = known_dimensions - .unwrap_or(measured_size) - .maybe_clamp(min_size, max_size); - let output = world.compute_child_layout( + // Inline-containing-block geometry becomes available after the numeric + // traversal, but sizing uses the same absolute contract as block/flex/grid. + let computed = taffy::compute::compute_absolute_layout( + world, child.to_taffy(), - LayoutInput { - known_dimensions: final_size.map(Some), - definite_dimensions: known_dimensions, - parent_size: area.size.map(Some), - parent_writing_mode: area.writing_mode, - available_space, - sizing_mode: SizingMode::ContentSize, - sizing_purpose: SizingPurpose::Layout, - run_mode: RunMode::PerformLayout, - axis: taffy::RequestedAxis::Both, - block_auto_behavior, - block_margins_are_collapsible: Line::FALSE, - }, - ); - - let resolved_margin = taffy::compute::resolve_absolute_margins( - margin, - taffy::Rect { - left, - right, - top, - bottom, - }, - area.size, - final_size, - taffy::WritingDirection { - mode: area.writing_mode, - direction: area.direction, + taffy::compute::AbsoluteConstraintSpace { + containing_block_size: area.size, + writing_direction: taffy::WritingDirection { + mode: area.writing_mode, + direction: area.direction, + }, + static_available_space: static_position.available_size(area.size), }, ); + let output = computed.output; + let size = output.size; + let margin = computed.margin; + let inset = computed.inset; let static_origin = static_position.border_box_origin( - final_size, - resolved_margin, + size, + margin, area.size, area.writing_mode, area.direction, ); - let x = match (left, right) { + let x = match (inset.left, inset.right) { (Some(left), Some(right)) => { if area.direction == taffy::Direction::Rtl { - area_width - final_size.width - right - resolved_margin.right + area.size.width - size.width - right - margin.right } else { - left + resolved_margin.left + left + margin.left } } - (Some(left), None) => left + resolved_margin.left, - (None, Some(right)) => area_width - final_size.width - right - resolved_margin.right, + (Some(left), None) => left + margin.left, + (None, Some(right)) => area.size.width - size.width - right - margin.right, (None, None) => static_origin.x, }; - let y = top - .map(|top| top + resolved_margin.top) + let y = inset + .top + .map(|top| top + margin.top) .or_else(|| { - bottom.map(|bottom| area_height - final_size.height - bottom - resolved_margin.bottom) + inset + .bottom + .map(|bottom| area.size.height - size.height - bottom - margin.bottom) }) .unwrap_or(static_origin.y); let scrollbar_size = world.get_scrollbar_insets(child.to_taffy()).sum_axes(); world.boxes[child.index()].unrounded_layout = Layout { order: 0, in_flow: None, - size: final_size, + size, content_size: output.content_size, scrollbar_size, location: Point { x: area.origin.x + x - numeric_parent_origin.x, y: area.origin.y + y - numeric_parent_origin.y, }, - padding, - border, - margin: resolved_margin, + padding: computed.padding, + border: computed.border, + margin, }; } diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/preferred_aspect_ratio.rs b/moli-renderer-v8/src/runtime/page_vm/tests/preferred_aspect_ratio.rs index 583bad9645..031327dc52 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/preferred_aspect_ratio.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/preferred_aspect_ratio.rs @@ -1,5 +1,50 @@ use super::*; +/// WPT abspos-005/012/013 with authored stretch, limits and percentage-basis +/// controls across all formatting owners, including inline containing blocks. +#[tokio::test(flavor = "current_thread")] +async fn screenshot_resolves_absolute_automatic_sizes_in_logical_order() { + run_page_vm_async_test(async move { + let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default())?; + let mut page = test_page_vm_with_loader_and_document_url( + &loader, + Vec::new(), + Url::parse("https://example.com/absolute-automatic-sizing.html")?, + ); + page.vm_mut() + .set_layout_policy(moli_page_types::LayoutPolicy::OnDemand); + let fixture = include_str!("../../../../tests/fixtures/absolute-automatic-sizing.html"); + page.vm_mut().eval(&format!( + "document.open();document.write({});document.close()", + serde_json::to_string(fixture)?, + ))?; + page.vm_mut() + .prime_document_lifecycle_processing_and_record_stylesheet_network_results(); + // Synchronous geometry reads intentionally use the last snapshot. + page.vm_mut() + .screenshot_layout_snapshot(moli_layout::PaintViewport::new(1280, 1600, 1.0))? + .expect("absolute sizing fixture must retain a layout root"); + let checks: serde_json::Value = serde_json::from_str( + &page + .vm_mut() + .eval("JSON.stringify(collectAbsoluteAutoSizingChecks())")?, + )?; + let checks = checks.as_array().expect("absolute sizing checks"); + assert_eq!(checks.len(), 384); + let failures: Vec<_> = checks + .iter() + .filter(|check| check["actual"] != check["expected"]) + .collect(); + assert!( + failures.is_empty(), + "absolute sizing mismatches: {failures:#?}" + ); + Ok::<_, anyhow::Error>(()) + }) + .await + .expect("absolute automatic sizing fixture should run"); +} + /// WPT block-aspect-ratio-015/017/043/056, with explicit limits, overflow, sizing-box /// semantics, and logical-axis controls shared with the Chromium comparison. #[tokio::test(flavor = "current_thread")] diff --git a/moli-renderer-v8/tests/fixtures/absolute-automatic-sizing.html b/moli-renderer-v8/tests/fixtures/absolute-automatic-sizing.html new file mode 100644 index 0000000000..ed07879e26 --- /dev/null +++ b/moli-renderer-v8/tests/fixtures/absolute-automatic-sizing.html @@ -0,0 +1,115 @@ + + +Absolute automatic sizing across formatting and containing blocks + + + +