From b09fee1df2bac21369e7ae0f3df7ecb810ee0857 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sat, 29 Aug 2026 01:31:50 +0800 Subject: [PATCH] feat(layout): expose resolved Grid tracks --- Cargo.lock | 2 +- Cargo.toml | 2 +- moli-layout/src/grid.rs | 230 ++++++++++++++++++ moli-layout/src/layout_tree/model.rs | 34 +++ moli-layout/src/layout_tree/query.rs | 12 +- moli-layout/src/layout_tree/source_query.rs | 23 +- moli-layout/src/layout_tree/tree.rs | 36 ++- moli-layout/src/lib.rs | 11 +- moli-layout/src/projection.rs | 23 +- moli-layout/src/taffy_tree.rs | 37 ++- moli-layout/src/world.rs | 12 +- .../src/native_bridge/element/geometry.rs | 2 +- .../element/geometry/mock/queries.rs | 1 + .../element/geometry/provider.rs | 25 +- .../element/styles/declaration/values.rs | 140 ++++++++++- .../tests/grid_resolved_track_values.rs | 117 +++++++++ .../src/runtime/page_vm/tests/mod.rs | 1 + 17 files changed, 668 insertions(+), 40 deletions(-) create mode 100644 moli-layout/src/grid.rs create mode 100644 moli-renderer-v8/src/runtime/page_vm/tests/grid_resolved_track_values.rs diff --git a/Cargo.lock b/Cargo.lock index 124ec9197..767529861 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4823,7 +4823,7 @@ dependencies = [ [[package]] name = "taffy" version = "0.13.0" -source = "git+https://github.com/ldm0/taffy?rev=b7735b0aba77dd0e98015f3cd1dd26a71f90a415#b7735b0aba77dd0e98015f3cd1dd26a71f90a415" +source = "git+https://github.com/ldm0/taffy?rev=847d1ce20c03b0378eafa530fa43f1e92d9292ea#847d1ce20c03b0378eafa530fa43f1e92d9292ea" dependencies = [ "arrayvec", "serde", diff --git a/Cargo.toml b/Cargo.toml index 6f78bd7fa..e22c761ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,7 +98,7 @@ stylo_atoms = { git = "https://github.com/ldm0/stylo", rev = "32c3de3f2aab3340c5 stylo_dom = { git = "https://github.com/ldm0/stylo", rev = "32c3de3f2aab3340c51f03f2a0878d1e4eed7fd6" } stylo_static_prefs = { git = "https://github.com/ldm0/stylo", rev = "32c3de3f2aab3340c51f03f2a0878d1e4eed7fd6" } stylo_traits = { git = "https://github.com/ldm0/stylo", rev = "32c3de3f2aab3340c51f03f2a0878d1e4eed7fd6" } -taffy = { git = "https://github.com/ldm0/taffy", rev = "b7735b0aba77dd0e98015f3cd1dd26a71f90a415" } +taffy = { git = "https://github.com/ldm0/taffy", rev = "847d1ce20c03b0378eafa530fa43f1e92d9292ea" } [workspace.lints.clippy] disallowed_methods = "deny" diff --git a/moli-layout/src/grid.rs b/moli-layout/src/grid.rs new file mode 100644 index 000000000..ea4728b80 --- /dev/null +++ b/moli-layout/src/grid.rs @@ -0,0 +1,230 @@ +//! Browser-owned resolved Grid tracks projected from the numeric backend. + +use style::Atom; + +use crate::{ + LAYOUT_SUBPIXELS_PER_CSS_PIXEL, LayoutResolvedGridTrackList, LayoutResolvedGridTracks, +}; + +pub(crate) fn project_resolved_grid_tracks( + style: &taffy::Style, + detailed: taffy::DetailedGridInfo, +) -> Option { + let row_line_names = expanded_line_names( + &style.grid_template_rows, + &style.grid_template_row_names, + usize::from(detailed.rows.explicit_tracks), + usize::from(detailed.rows.auto_repetitions), + )?; + let column_line_names = expanded_line_names( + &style.grid_template_columns, + &style.grid_template_column_names, + usize::from(detailed.columns.explicit_tracks), + usize::from(detailed.columns.auto_repetitions), + )?; + Some(LayoutResolvedGridTracks { + rows: project_tracks(detailed.rows, row_line_names)?, + columns: project_tracks(detailed.columns, column_line_names)?, + }) +} + +fn project_tracks( + tracks: taffy::DetailedGridTracksInfo, + explicit_line_names: Vec>, +) -> Option { + let track_count = usize::from(tracks.negative_implicit_tracks) + .checked_add(usize::from(tracks.explicit_tracks))? + .checked_add(usize::from(tracks.positive_implicit_tracks))?; + if tracks.sizes.len() != track_count + || tracks.gutters.len() != track_count.saturating_add(1) + || explicit_line_names.len() != usize::from(tracks.explicit_tracks).saturating_add(1) + { + return None; + } + let used_track_sizes = tracks + .sizes + .into_iter() + .map(to_blink_layout_unit) + .collect::>>()?; + Some(LayoutResolvedGridTrackList { + negative_implicit_track_count: usize::from(tracks.negative_implicit_tracks), + explicit_track_count: usize::from(tracks.explicit_tracks), + positive_implicit_track_count: usize::from(tracks.positive_implicit_tracks), + used_track_sizes, + explicit_line_names, + }) +} + +/// Blink performs Grid sizing in a 26.6 fixed-point `LayoutUnit`. Taffy uses +/// floats, so truncate finite non-negative used track sizes at this ownership +/// boundary before they become observable through CSSOM. +fn to_blink_layout_unit(value: f32) -> Option { + if !value.is_finite() || value < 0.0 { + return None; + } + let raw = (f64::from(value) * f64::from(LAYOUT_SUBPIXELS_PER_CSS_PIXEL)) + .trunc() + .min(f64::from(i32::MAX)); + Some(raw as f32 / LAYOUT_SUBPIXELS_PER_CSS_PIXEL) +} + +fn expanded_line_names( + template: &[taffy::GridTemplateComponent], + template_line_names: &[Vec], + explicit_track_count: usize, + auto_repetitions: usize, +) -> Option>> { + if template.is_empty() { + return template_line_names + .is_empty() + .then(|| vec![Vec::new(); explicit_track_count.saturating_add(1)]); + } + if template_line_names.len() != template.len().saturating_add(1) { + return None; + } + + let append_names = |line: &mut Vec, names: &[Atom]| { + line.extend(names.iter().cloned()); + }; + let mut lines = vec![Vec::new()]; + let mut expanded_track_count = 0usize; + let mut saw_auto_repeat = false; + for (index, component) in template.iter().enumerate() { + append_names(lines.last_mut()?, template_line_names.get(index)?); + match component { + taffy::GridTemplateComponent::Single(_) => { + expanded_track_count = expanded_track_count.checked_add(1)?; + lines.push(Vec::new()); + } + taffy::GridTemplateComponent::Repeat(repeat) => { + if repeat.tracks.is_empty() + || repeat.line_names.len() != repeat.tracks.len().saturating_add(1) + { + return None; + } + let repeat_count = match repeat.count { + taffy::RepetitionCount::Count(count) => usize::from(count), + taffy::RepetitionCount::AutoFill | taffy::RepetitionCount::AutoFit => { + if saw_auto_repeat { + return None; + } + saw_auto_repeat = true; + auto_repetitions + } + }; + for _ in 0..repeat_count { + append_names(lines.last_mut()?, repeat.line_names.first()?); + for track_index in 0..repeat.tracks.len() { + expanded_track_count = expanded_track_count.checked_add(1)?; + lines.push(Vec::new()); + append_names(lines.last_mut()?, repeat.line_names.get(track_index + 1)?); + } + } + } + } + } + if !saw_auto_repeat && auto_repetitions != 0 { + return None; + } + append_names(lines.last_mut()?, template_line_names.get(template.len())?); + if expanded_track_count > explicit_track_count { + return None; + } + + // `grid-template-areas` can extend the explicit grid beyond the authored + // track list. Those extra tracks have no authored line names; generated + // area names do not serialize into the resolved track listing. + lines.resize_with(explicit_track_count.saturating_add(1), Vec::new); + Some(lines) +} + +#[cfg(test)] +mod tests { + use super::*; + use taffy::style_helpers::TaffyAuto; + + fn names(values: &[&str]) -> Vec { + values.iter().map(|value| Atom::from(*value)).collect() + } + + fn track() -> taffy::TrackSizingFunction { + taffy::TrackSizingFunction::AUTO + } + + #[test] + fn fixed_repeat_merges_names_at_each_expanded_boundary() { + let template = vec![ + taffy::GridTemplateComponent::Single(track()), + taffy::GridTemplateComponent::Repeat(taffy::GridTemplateRepetition { + count: taffy::RepetitionCount::Count(2), + tracks: vec![track(), track()], + line_names: vec![names(&["c"]), names(&["d"]), names(&["e"])], + }), + taffy::GridTemplateComponent::Single(track()), + ]; + let line_names = vec![names(&["a"]), names(&["b"]), names(&["f"]), names(&["g"])]; + + assert_eq!( + expanded_line_names(&template, &line_names, 6, 0), + Some(vec![ + vec!["a".into()], + vec!["b".into(), "c".into()], + vec!["d".into()], + vec!["e".into(), "c".into()], + vec!["d".into()], + vec!["e".into(), "f".into()], + vec!["g".into()], + ]), + ); + } + + #[test] + fn auto_repeat_uses_the_layout_result_count() { + let template = vec![ + taffy::GridTemplateComponent::Single(track()), + taffy::GridTemplateComponent::Repeat(taffy::GridTemplateRepetition { + count: taffy::RepetitionCount::AutoFill, + tracks: vec![track(), track()], + line_names: vec![names(&["c"]), names(&["d"]), names(&["e"])], + }), + taffy::GridTemplateComponent::Single(track()), + ]; + let line_names = vec![names(&["a"]), names(&["b"]), names(&["f"]), names(&["g"])]; + let expanded = expanded_line_names(&template, &line_names, 12, 5).expect("valid expansion"); + + assert_eq!(expanded.len(), 13); + assert_eq!(expanded[1], names(&["b", "c"])); + assert_eq!(expanded[3], names(&["e", "c"])); + assert_eq!(expanded[11], names(&["e", "f"])); + assert_eq!(expanded[12], names(&["g"])); + } + + #[test] + fn area_expanded_explicit_grid_does_not_change_auto_repeat_count() { + let template = vec![taffy::GridTemplateComponent::Repeat( + taffy::GridTemplateRepetition { + count: taffy::RepetitionCount::AutoFill, + tracks: vec![track()], + line_names: vec![names(&["a"]), names(&["b"])], + }, + )]; + let line_names = vec![Vec::new(), Vec::new()]; + + let expanded = expanded_line_names(&template, &line_names, 8, 5) + .expect("grid-template-areas may extend the explicit grid after auto-repeat"); + + assert_eq!(expanded.len(), 9); + assert_eq!(expanded[0], names(&["a"])); + assert_eq!(expanded[4], names(&["b", "a"])); + assert_eq!(expanded[5], names(&["b"])); + assert!(expanded[6..].iter().all(Vec::is_empty)); + } + + #[test] + fn used_track_sizes_are_truncated_to_blink_layout_units() { + assert_eq!(to_blink_layout_unit(100.0 / 3.0), Some(33.328_125)); + assert_eq!(to_blink_layout_unit(0.015_625), Some(0.015_625)); + assert_eq!(to_blink_layout_unit(-1.0), None); + assert_eq!(to_blink_layout_unit(f32::NAN), None); + } +} diff --git a/moli-layout/src/layout_tree/model.rs b/moli-layout/src/layout_tree/model.rs index 561ef0e5e..07fd2e17d 100644 --- a/moli-layout/src/layout_tree/model.rs +++ b/moli-layout/src/layout_tree/model.rs @@ -2,6 +2,8 @@ use std::ops::Range; +use style::Atom; + use crate::LayoutPosition; /// Viewport inputs shared by layout, geometry queries, and paint projection. @@ -55,6 +57,38 @@ impl LayoutSize { } } +/// Frozen resolved track list for one axis of a CSS Grid formatting context. +/// +/// Track sizes are retained in layout CSS pixels at Blink-compatible 1/64-pixel +/// precision. The authored line-name snapshot is kept in the same layout epoch +/// so a CSSOM read cannot combine old used sizes with names from a newer style +/// mutation. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct LayoutResolvedGridTrackList { + pub negative_implicit_track_count: usize, + pub explicit_track_count: usize, + pub positive_implicit_track_count: usize, + pub used_track_sizes: Vec, + /// Expanded names for each explicit grid line. The length is the explicit + /// track count plus one; names at repeat boundaries are already merged. + pub explicit_line_names: Vec>, +} + +impl LayoutResolvedGridTrackList { + pub fn track_count(&self) -> usize { + self.negative_implicit_track_count + .saturating_add(self.explicit_track_count) + .saturating_add(self.positive_implicit_track_count) + } +} + +/// Frozen resolved row and column track lists for a CSS Grid container. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct LayoutResolvedGridTracks { + pub rows: LayoutResolvedGridTrackList, + pub columns: LayoutResolvedGridTrackList, +} + /// An axis-aligned rectangle in one explicit layout coordinate space. #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct LayoutRect { diff --git a/moli-layout/src/layout_tree/query.rs b/moli-layout/src/layout_tree/query.rs index 596416982..3d297345b 100644 --- a/moli-layout/src/layout_tree/query.rs +++ b/moli-layout/src/layout_tree/query.rs @@ -7,8 +7,8 @@ use crate::LayoutError; use super::{ hit_test::{LayoutCaretPosition, LayoutHit}, model::{ - LayoutBoxModel, LayoutFragmentId, LayoutOutputBoxId, LayoutPoint, LayoutQuad, LayoutSize, - LayoutViewport, + LayoutBoxModel, LayoutFragmentId, LayoutOutputBoxId, LayoutPoint, LayoutQuad, + LayoutResolvedGridTracks, LayoutSize, LayoutViewport, }, pass_result::{LayoutFlushReason, LayoutPassMetrics}, tree::FrozenLayoutTree, @@ -113,6 +113,10 @@ pub enum LayoutQuery { ElementMetrics { source: N, }, + /// Used Grid row and column tracks for a principal Grid container. + UsedGridTracks { + source: N, + }, ScrollIntoViewGeometry { source: N, }, @@ -162,6 +166,7 @@ pub enum LayoutQueryAnswer { ContentQuads(Vec), TextRangeRects(Vec), ElementMetrics(Option>), + UsedGridTracks(Option), ScrollIntoViewGeometry(Option>), IntersectionGeometry(Option), HitTest(Option>), @@ -232,6 +237,9 @@ where LayoutQuery::ElementMetrics { source } => { LayoutQueryAnswer::ElementMetrics(self.element_metrics_for_source(*source)) } + LayoutQuery::UsedGridTracks { source } => { + LayoutQueryAnswer::UsedGridTracks(self.used_grid_tracks_for_source(*source)) + } LayoutQuery::ScrollIntoViewGeometry { source } => { LayoutQueryAnswer::ScrollIntoViewGeometry( self.scroll_into_view_geometry_for_source(*source), diff --git a/moli-layout/src/layout_tree/source_query.rs b/moli-layout/src/layout_tree/source_query.rs index 07733bb4b..58d506fd8 100644 --- a/moli-layout/src/layout_tree/source_query.rs +++ b/moli-layout/src/layout_tree/source_query.rs @@ -7,7 +7,8 @@ use crate::LayoutPosition; use super::{ model::{ LayoutBoxModel, LayoutCoordinateSpaceId, LayoutFragmentBoxModel, LayoutFragmentKind, - LayoutOutputBoxId, LayoutPoint, LayoutQuad, LayoutRect, LayoutSize, LayoutTransform2D, + LayoutOutputBoxId, LayoutPoint, LayoutQuad, LayoutRect, LayoutResolvedGridTracks, + LayoutSize, LayoutTransform2D, }, query::{LayoutElementMetrics, LayoutNodeOutput}, tree::FrozenLayoutTree, @@ -207,6 +208,21 @@ where }) } + /// Returns used Grid tracks from the same frozen epoch as other CSSOM + /// geometry, normalized out of the container's effective CSS zoom. + pub fn used_grid_tracks_for_source(&self, source: N) -> Option { + let output = self.source_output(source)?; + let layout_box = self.boxes.get(output.principal_box?.index())?; + let mut grid = layout_box.resolved_grid_tracks.clone()?; + let unzoom = CssomAbsoluteZoom::new(layout_box.geometry.effective_zoom); + for tracks in [&mut grid.rows, &mut grid.columns] { + for size in &mut tracks.used_track_sizes { + *size = unzoom.scalar(*size); + } + } + Some(grid) + } + /// Resolves a viewport point into the coordinate system Blink uses for /// `MouseEvent.offsetX/Y`: a box target's padding edge, or the shared IFC /// coordinate space for a flattened inline layout object. @@ -475,6 +491,10 @@ impl CssomAbsoluteZoom { LayoutPoint::new(point.x / self.0, point.y / self.0) } + fn scalar(self, value: f32) -> f32 { + value / self.0 + } + fn size(self, size: LayoutSize) -> LayoutSize { LayoutSize::new(size.width / self.0, size.height / self.0) } @@ -763,6 +783,7 @@ mod tests { geometry_source: Some(1), principal_source: Some(1), hit_source: Some(1), + resolved_grid_tracks: None, control_paint_order: None, }], vec![ diff --git a/moli-layout/src/layout_tree/tree.rs b/moli-layout/src/layout_tree/tree.rs index 800454af5..2e3a42ac3 100644 --- a/moli-layout/src/layout_tree/tree.rs +++ b/moli-layout/src/layout_tree/tree.rs @@ -25,6 +25,7 @@ pub struct FrozenLayoutBox { pub geometry_source: Option, pub principal_source: Option, pub hit_source: Option, + pub resolved_grid_tracks: Option, /// Paint ordinal of this box's scrollbar/corner surface, if any. pub(crate) control_paint_order: Option, } @@ -122,9 +123,38 @@ where } let box_allocations = self.boxes.iter().fold(0usize, |bytes, layout_box| { - bytes.saturating_add(allocation::( - layout_box.fragments.capacity(), - )) + let fragment_bytes = allocation::(layout_box.fragments.capacity()); + let grid_bytes = layout_box.resolved_grid_tracks.as_ref().map_or(0, |grid| { + let numeric_bytes = [ + grid.rows.used_track_sizes.capacity(), + grid.columns.used_track_sizes.capacity(), + ] + .into_iter() + .fold(0usize, |bytes, capacity| { + bytes.saturating_add(allocation::(capacity)) + }); + let line_name_bytes = + [&grid.rows, &grid.columns] + .into_iter() + .fold(0usize, |bytes, tracks| { + let outer = + allocation::>(tracks.explicit_line_names.capacity()); + tracks.explicit_line_names.iter().fold( + bytes.saturating_add(outer), + |bytes, names| { + let atoms = allocation::(names.capacity()); + let retained_text = names.iter().fold(0usize, |bytes, name| { + bytes.saturating_add(name.len()) + }); + bytes.saturating_add(atoms).saturating_add(retained_text) + }, + ) + }); + numeric_bytes.saturating_add(line_name_bytes) + }); + bytes + .saturating_add(fragment_bytes) + .saturating_add(grid_bytes) }); let own_estimated_geometry_bytes = std::mem::size_of::() .saturating_add(allocation::>(self.boxes.capacity())) diff --git a/moli-layout/src/lib.rs b/moli-layout/src/lib.rs index e483f8233..e8b9c78d3 100644 --- a/moli-layout/src/lib.rs +++ b/moli-layout/src/lib.rs @@ -7,7 +7,7 @@ /// Number of fixed layout subpixels in one CSS pixel, matching Blink's /// `LayoutUnit` precision. -pub const LAYOUT_SUBPIXELS_PER_CSS_PIXEL: f32 = 64.0; +pub(crate) const LAYOUT_SUBPIXELS_PER_CSS_PIXEL: f32 = 64.0; mod builder; mod capture; @@ -15,6 +15,7 @@ mod containment; mod error; mod form; mod gradient; +mod grid; mod inline; mod intrinsic; mod layout_tree; @@ -53,10 +54,10 @@ pub use layout_tree::{ LayoutFragmentBoxModel, LayoutFragmentId, LayoutFragmentKind, LayoutHit, LayoutIntersectionGeometry, LayoutNodeOutput, LayoutOutputBoxId, LayoutPaintedSurfaceHit, LayoutPassMetrics, LayoutPassResult, LayoutPoint, LayoutQuad, LayoutQuery, LayoutQueryAnswer, - LayoutQueryBatch, LayoutRect, LayoutScrollContainerMetrics, LayoutScrollExtent, - LayoutScrollIntoViewGeometry, LayoutSize, LayoutTransform2D, LayoutTreeRetentionMetrics, - LayoutViewport, MAX_RETAINED_LAYOUT_BOXES, MAX_RETAINED_LAYOUT_FRAGMENTS, - MAX_RETAINED_LAYOUT_TREE_BYTES, + LayoutQueryBatch, LayoutRect, LayoutResolvedGridTrackList, LayoutResolvedGridTracks, + LayoutScrollContainerMetrics, LayoutScrollExtent, LayoutScrollIntoViewGeometry, LayoutSize, + LayoutTransform2D, LayoutTreeRetentionMetrics, LayoutViewport, MAX_RETAINED_LAYOUT_BOXES, + MAX_RETAINED_LAYOUT_FRAGMENTS, MAX_RETAINED_LAYOUT_TREE_BYTES, }; pub use normalize::{NormalizedBoxNode, NormalizedBoxTree, NormalizedFormattingContext}; pub use normalize_source::{ diff --git a/moli-layout/src/projection.rs b/moli-layout/src/projection.rs index 063d3ffcd..465182805 100644 --- a/moli-layout/src/projection.rs +++ b/moli-layout/src/projection.rs @@ -928,6 +928,7 @@ where .next() .expect("a frozen layout tree always owns the viewport coordinate space"), ); + let world = self.world; let boxes = self .boxes .into_iter() @@ -947,14 +948,20 @@ where control_paint_order, ), coordinate_space, - )| FrozenLayoutBox { - geometry, - scroll_extent, - coordinate_space, - geometry_source, - principal_source, - hit_source, - control_paint_order, + )| { + let resolved_grid_tracks = world.boxes[geometry.id.index()] + .resolved_grid_tracks + .clone(); + FrozenLayoutBox { + geometry, + scroll_extent, + coordinate_space, + geometry_source, + principal_source, + hit_source, + resolved_grid_tracks, + control_paint_order, + } }, ) .collect(); diff --git a/moli-layout/src/taffy_tree.rs b/moli-layout/src/taffy_tree.rs index 657b1cc47..7aaae90d2 100644 --- a/moli-layout/src/taffy_tree.rs +++ b/moli-layout/src/taffy_tree.rs @@ -4,17 +4,18 @@ use parley::{AlignmentOptions, PositionedLayoutItem, YieldData}; use style::Atom; use taffy::{ AlignContent, AlignContentKeyword, AlignmentSafety, AvailableSpace, BlockContext, - BlockFormattingContext, BoxSizing, CacheTree, Clear, Dimension, Display, FloatDirection, - Layout, LayoutBlockContainer, LayoutFlexboxContainer, LayoutGridContainer, LayoutInput, - LayoutOutput, LayoutPartialTree, LeafLayoutContext, Line, MaybeMath, MaybeResolve, NodeId, - Point, ResolveOrZero, RoundTree, RunMode, Size, SizingMode, SizingPurpose, Style, + BlockFormattingContext, BoxSizing, CacheTree, Clear, DetailedGridInfo, Dimension, Display, + FloatDirection, 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, compute_cached_layout, compute_flexbox_layout, compute_grid_layout, compute_hidden_layout, compute_leaf_layout_with_context, compute_root_layout, round_layout, }; use crate::{ - LayoutBoxId, LayoutBoxKind, LayoutCapabilityDiagnostic, LayoutWorld, PaintRect, PaintViewport, + LAYOUT_SUBPIXELS_PER_CSS_PIXEL, LayoutBoxId, LayoutBoxKind, LayoutCapabilityDiagnostic, + LayoutWorld, PaintRect, PaintViewport, inline::{ InlineFormattingContext, InlineFragments, InlineLinePlacement, InlineObjectRole, break_inline_lines, build_inline_fragments, build_inline_line_placements, @@ -27,9 +28,6 @@ use crate::{ world::InlineStaticPosition, }; -// Blink stores box geometry in 1/64 CSS-pixel LayoutUnits. -const LAYOUT_SUBPIXELS_PER_CSS_PIXEL: f32 = 64.0; - pub(crate) struct PreparedWorldLayout { positioned_static_placeholders: Vec, numeric_unrounded_layouts: Vec, @@ -133,6 +131,7 @@ where layout_box.layout_children.clear(); layout_box.positioned_containing_block = None; layout_box.inline_static_position = None; + layout_box.resolved_grid_tracks = None; } world.viewport_layout.children.clear(); @@ -1612,6 +1611,28 @@ where fn get_grid_child_style(&self, child_node_id: NodeId) -> Self::GridItemStyle<'_> { self.get_core_container_style(child_node_id) } + + fn set_detailed_grid_info(&mut self, node_id: NodeId, detailed_grid_info: DetailedGridInfo) { + let layout_box = &self.boxes[LayoutBoxId::from_taffy(node_id).index()]; + if layout_box + .capability_diagnostics + .contains(&LayoutCapabilityDiagnostic::GridTemplateModeDeferred) + // The current backend reports detailed Grid tracks in physical + // axes. Publishing them for a vertical container would expose its + // width as the used `grid-template-columns` inline size. Keep the + // computed value until logical Grid constraints land in Taffy. + || !layout_box.style.uses_horizontal_writing_mode() + { + return; + } + let Some(resolved_grid_tracks) = + crate::grid::project_resolved_grid_tracks(&layout_box.style.taffy, detailed_grid_info) + else { + return; + }; + self.boxes[LayoutBoxId::from_taffy(node_id).index()].resolved_grid_tracks = + Some(resolved_grid_tracks); + } } impl RoundTree for LayoutWorld diff --git a/moli-layout/src/world.rs b/moli-layout/src/world.rs index c1ded8dac..fef4eadc2 100644 --- a/moli-layout/src/world.rs +++ b/moli-layout/src/world.rs @@ -5,9 +5,9 @@ use taffy::{Cache, Layout, Point, Style}; use crate::{ LayoutCssImageReference, LayoutElementSemantics, LayoutError, LayoutPoint, LayoutPseudo, - LayoutScrollbarAxis, LayoutScrollbarColors, LayoutScrollbarGutter, LayoutScrollbarWidth, - ResolvedLayoutStyle, inline::InlineFormattingContext, replaced::ReplacedContext, - style::LayoutOverflowMode, + LayoutResolvedGridTracks, LayoutScrollbarAxis, LayoutScrollbarColors, LayoutScrollbarGutter, + LayoutScrollbarWidth, ResolvedLayoutStyle, inline::InlineFormattingContext, + replaced::ReplacedContext, style::LayoutOverflowMode, }; /// Dense identifier scoped to exactly one [`LayoutWorld`]. @@ -252,6 +252,11 @@ pub struct LayoutBox { /// owner's conflict-resolution grid. pub(crate) collapsed_table_border_part: bool, pub(crate) inline_formatting_context: bool, + /// Used Grid tracks produced by the numeric layout pass. + /// + /// This is browser-owned canonical geometry rather than a retained Taffy + /// object. It is frozen with the rest of the layout tree for CSSOM reads. + pub(crate) resolved_grid_tracks: Option, pub(crate) cache: Cache, pub(crate) unrounded_layout: Layout, pub(crate) final_layout: Layout, @@ -1092,6 +1097,7 @@ where collapsed_table_borders: None, collapsed_table_border_part: false, inline_formatting_context: false, + resolved_grid_tracks: None, cache: Cache::new(), unrounded_layout: Layout::with_order(0), final_layout: Layout::with_order(0), diff --git a/moli-renderer-v8/src/native_bridge/element/geometry.rs b/moli-renderer-v8/src/native_bridge/element/geometry.rs index 1fd9be50a..f4b825ba8 100644 --- a/moli-renderer-v8/src/native_bridge/element/geometry.rs +++ b/moli-renderer-v8/src/native_bridge/element/geometry.rs @@ -33,7 +33,7 @@ pub(crate) use provider::{ observable_bounding_client_rect, observable_bounding_client_rects, observable_caret_position, observable_client_rects, observable_element_metrics, observable_event_offset, observable_geometry_batch, observable_hit_test_all, observable_scroll_adjusted_client_rect, - observable_sources_with_fragments, + observable_sources_with_fragments, observable_used_grid_tracks, }; pub(in crate::native_bridge) use rects::{ node_get_bounding_client_rect_callback, node_get_client_rects_callback, diff --git a/moli-renderer-v8/src/native_bridge/element/geometry/mock/queries.rs b/moli-renderer-v8/src/native_bridge/element/geometry/mock/queries.rs index 468ba97e2..47de6e914 100644 --- a/moli-renderer-v8/src/native_bridge/element/geometry/mock/queries.rs +++ b/moli-renderer-v8/src/native_bridge/element/geometry/mock/queries.rs @@ -69,6 +69,7 @@ pub(crate) fn answer_queries( LayoutQuery::ElementMetrics { source } => { LayoutQueryAnswer::ElementMetrics(mock_element_metrics(runtime, *source)) } + LayoutQuery::UsedGridTracks { .. } => LayoutQueryAnswer::UsedGridTracks(None), LayoutQuery::ScrollIntoViewGeometry { source } => { LayoutQueryAnswer::ScrollIntoViewGeometry(mock_scroll_into_view_geometry( runtime, document, *source, diff --git a/moli-renderer-v8/src/native_bridge/element/geometry/provider.rs b/moli-renderer-v8/src/native_bridge/element/geometry/provider.rs index 4041e04ae..41fe6d1d1 100644 --- a/moli-renderer-v8/src/native_bridge/element/geometry/provider.rs +++ b/moli-renderer-v8/src/native_bridge/element/geometry/provider.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use moli_layout::{ LayoutAnswers, LayoutBoxModel, LayoutCaretPosition, LayoutDocumentMetrics, LayoutElementMetrics, LayoutError, LayoutFlushReason, LayoutHit, LayoutPoint, LayoutQuery, - LayoutQueryAnswer, LayoutQueryBatch, LayoutScrollIntoViewGeometry, + LayoutQueryAnswer, LayoutQueryBatch, LayoutResolvedGridTracks, LayoutScrollIntoViewGeometry, }; use super::client_rect::{ClientRect, client_rect_from_quad, union_client_rect, zero_client_rect}; @@ -234,6 +234,29 @@ pub(crate) fn observable_element_metrics( } } +pub(crate) fn observable_used_grid_tracks( + runtime: &JsContextHost, + source: DomHandle, + reason: LayoutFlushReason, +) -> Result, LayoutError> { + if !runtime.dom_host().is_connected(source) { + return Ok(None); + } + let Some(document) = runtime.layout_document_for_source(source) else { + return Ok(None); + }; + let answers = observable_geometry_batch( + runtime, + document, + reason, + &LayoutQueryBatch::new(vec![LayoutQuery::UsedGridTracks { source }]), + )?; + match answers.answers.into_iter().next() { + Some(LayoutQueryAnswer::UsedGridTracks(tracks)) => Ok(tracks), + _ => Err(provider_contract_error("used Grid tracks")), + } +} + pub(crate) fn observable_scroll_into_view_geometry( runtime: &JsContextHost, source: DomHandle, diff --git a/moli-renderer-v8/src/native_bridge/element/styles/declaration/values.rs b/moli-renderer-v8/src/native_bridge/element/styles/declaration/values.rs index 1bde75b21..5aab3382e 100644 --- a/moli-renderer-v8/src/native_bridge/element/styles/declaration/values.rs +++ b/moli-renderer-v8/src/native_bridge/element/styles/declaration/values.rs @@ -13,6 +13,7 @@ use crate::{ document_runtime::DomHandle, native_bridge::element::geometry::{ ClientRect, observable_bounding_client_rect, observable_bounding_client_rects, + observable_used_grid_tracks, }, style_engine::{ ComputedDisplayKind, ComputedRenderedStyleFacts, FullStyleWorldSnapshot, StyleViewport, @@ -5075,6 +5076,11 @@ fn resolve_moli_computed_style_value( if property == "font-family" { return normalize_cssom_font_family_value(value).unwrap_or_else(|| value.to_owned()); } + if matches!(property, "grid-template-columns" | "grid-template-rows") + && let Some(tracks) = resolved_grid_template_tracks(runtime, handle, property) + { + return tracks; + } if property == "width" && let Some(width) = resolve_computed_width_with_inline_fallback(runtime, handle, value, context, resolution) @@ -5121,6 +5127,86 @@ fn resolve_moli_computed_style_value( value.to_owned() } +fn resolved_grid_template_tracks( + runtime: &JsContextHost, + handle: DomHandle, + property: &str, +) -> Option { + if !runtime.layout_policy().uses_real_layout() { + return None; + } + let grid = observable_used_grid_tracks( + runtime, + handle, + moli_layout::LayoutFlushReason::SynchronousGeometry, + ) + .ok()??; + let tracks = match property { + "grid-template-columns" => &grid.columns, + "grid-template-rows" => &grid.rows, + _ => return None, + }; + + serialize_used_grid_track_list(tracks) +} + +fn serialize_used_grid_track_list( + tracks: &moli_layout::LayoutResolvedGridTrackList, +) -> Option { + if tracks.track_count() != tracks.used_track_sizes.len() { + return None; + } + if tracks.used_track_sizes.is_empty() { + return Some("none".to_owned()); + } + if tracks.explicit_line_names.len() != tracks.explicit_track_count.saturating_add(1) { + return None; + } + let mut components = Vec::with_capacity( + tracks.used_track_sizes.len().saturating_add( + tracks + .explicit_line_names + .iter() + .filter(|names| !names.is_empty()) + .count(), + ), + ); + let mut size_index = 0usize; + for _ in 0..tracks.negative_implicit_track_count { + components.push(format_non_negative_used_css_px(f64::from( + *tracks.used_track_sizes.get(size_index)?, + ))); + size_index += 1; + } + for (track_index, names) in tracks.explicit_line_names.iter().enumerate() { + if !names.is_empty() { + let mut serialized = String::from("["); + for (index, name) in names.iter().enumerate() { + if index > 0 { + serialized.push(' '); + } + serialize_identifier(name.as_ref(), &mut serialized) + .expect("serializing an identifier into String cannot fail"); + } + serialized.push(']'); + components.push(serialized); + } + if track_index < tracks.explicit_track_count { + components.push(format_non_negative_used_css_px(f64::from( + *tracks.used_track_sizes.get(size_index)?, + ))); + size_index += 1; + } + } + for _ in 0..tracks.positive_implicit_track_count { + components.push(format_non_negative_used_css_px(f64::from( + *tracks.used_track_sizes.get(size_index)?, + ))); + size_index += 1; + } + (size_index == tracks.used_track_sizes.len()).then(|| components.join(" ")) +} + fn computed_axis_position_shorthand_value( runtime: &JsContextHost, handle: DomHandle, @@ -6538,17 +6624,51 @@ fn parse_css_percent(value: &str) -> Option { } fn format_css_px(value: f64) -> String { - if (value.round() - value).abs() < 0.000_001 { - return format!("{}px", value.round() as i64); + format!("{}px", format_css_numeric_literal(value)) +} + +/// Matches Blink's `CSSNumericLiteralValue` serialization, which uses `%g` +/// with six significant digits for finite non-integer dimensions. +fn format_css_numeric_literal(value: f64) -> String { + if value == 0.0 { + return "0".to_owned(); } - let mut serialized = format!("{value:.6}"); - while serialized.contains('.') && serialized.ends_with('0') { + if !value.is_finite() { + return value.to_string(); + } + + // Formatting in scientific notation first gives us both the six-digit + // rounding and the post-rounding exponent without reimplementing floating + // point decimal conversion. + let scientific = format!("{value:.5e}"); + let (mantissa, exponent) = scientific + .rsplit_once('e') + .expect("finite f64 scientific formatting must contain an exponent"); + let exponent = exponent + .parse::() + .expect("f64 scientific formatting must contain a decimal exponent"); + if (-4..6).contains(&exponent) { + let fractional_digits = usize::try_from((5 - exponent).max(0)).unwrap_or(0); + let mut serialized = format!("{value:.fractional_digits$}"); + trim_decimal_zeros(&mut serialized); + return serialized; + } + + let mut mantissa = mantissa.to_owned(); + trim_decimal_zeros(&mut mantissa); + format!("{mantissa}e{exponent:+03}") +} + +fn trim_decimal_zeros(serialized: &mut String) { + if !serialized.contains('.') { + return; + } + while serialized.ends_with('0') { serialized.pop(); } if serialized.ends_with('.') { serialized.pop(); } - format!("{serialized}px") } fn format_non_negative_used_css_px(value: f64) -> String { @@ -7030,7 +7150,7 @@ mod tests { use super::{ KEYFRAME_NESTING_DEPTH_LIMIT, animation_shorthand_names, box_shorthand_component, collect_custom_functions_from_css, compress_box_shorthand_value, - custom_function_container_rule_texts, format_css_number, + custom_function_container_rule_texts, format_css_number, format_css_px, keyframe_has_supported_animation_values, keyframe_property_values, normalize_computed_color_functions, normalize_css_integer_token, normalize_style_value, simple_var_function_parts, @@ -7064,6 +7184,14 @@ mod tests { assert_eq!(format_css_number(120.00005), "120.00005"); } + #[test] + fn css_pixel_serialization_matches_blink_six_significant_digits() { + assert_eq!(format_css_px(33.328_125), "33.3281px"); + assert_eq!(format_css_px(0.015_625), "0.015625px"); + assert_eq!(format_css_px(999_999.0), "999999px"); + assert_eq!(format_css_px(1_000_000.0), "1e+06px"); + } + #[test] fn connected_shadow_roots_for_document_excludes_child_document_roots() { let mut host = test_host(); diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/grid_resolved_track_values.rs b/moli-renderer-v8/src/runtime/page_vm/tests/grid_resolved_track_values.rs new file mode 100644 index 000000000..4804f8a3b --- /dev/null +++ b/moli-renderer-v8/src/runtime/page_vm/tests/grid_resolved_track_values.rs @@ -0,0 +1,117 @@ +use super::*; + +#[tokio::test(flavor = "current_thread")] +async fn computed_style_serializes_used_grid_tracks_from_the_frozen_layout_tree() { + run_page_vm_async_test(async move { + let loader = + crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); + let mut page_vm = test_page_vm_with_loader_and_document_url( + &loader, + Vec::new(), + Url::parse("https://example.com/grid-used-track-cssom.html")?, + ); + page_vm.vm_mut().eval( + r#" +document.head.innerHTML = ``; +document.body.innerHTML = ` +
+
+
+
+
+
+
+
+
+
+
+
`; +'installed' +"#, + )?; + page_vm.vm_mut().sync_live_document_style_sources(); + page_vm + .vm_mut() + .screenshot_layout_snapshot(moli_layout::PaintViewport::new(400, 300, 1.0))? + .expect("used Grid track CSSOM screenshot layout"); + + let values = page_vm.vm_mut().eval( + r#"JSON.stringify({ +intrinsic:getComputedStyle(document.getElementById('intrinsic')).gridTemplateColumns, +rows:getComputedStyle(document.getElementById('rows')).gridTemplateRows, +named:getComputedStyle(document.getElementById('named')).gridTemplateColumns, +automatic:getComputedStyle(document.getElementById('automatic')).gridTemplateColumns, +autoFit:getComputedStyle(document.getElementById('auto-fit')).gridTemplateColumns, +implicit:getComputedStyle(document.getElementById('implicit')).gridTemplateColumns, +leading:getComputedStyle(document.getElementById('leading')).gridTemplateColumns, +areas:getComputedStyle(document.getElementById('areas')).gridTemplateColumns, +areaRepeat:getComputedStyle(document.getElementById('area-repeat')).gridTemplateColumns, +fractional:getComputedStyle(document.getElementById('fractional')).gridTemplateColumns, +zoomed:getComputedStyle(document.getElementById('zoomed')).gridTemplateColumns, +vertical:getComputedStyle(document.getElementById('vertical')).gridTemplateColumns +})"#, + )?; + let values: serde_json::Value = serde_json::from_str(&values)?; + assert_eq!( + values, + serde_json::json!({ + "intrinsic": "75px", + "rows": "30px 70px", + "named": "[a] 21px [b c] 22px [d] 23px [e c] 22px [d] 23px [e f] 189px [g]", + "automatic": "[a] 21px [b c] 22px [d] 23px [e c] 22px [d] 23px [e c] 22px [d] 23px [e c] 22px [d] 23px [e c] 22px [d] 23px [e f] 24px [g]", + "autoFit": "1px [a b] 0px [c b] 0px [c d] 3px", + "implicit": "35px", + "leading": "15px [a] 40px [b]", + "areas": "50px 50px", + "areaRepeat": "20px 20px 20px 20px 20px 0px 0px 0px", + "fractional": "33.3281px 33.3281px 33.3281px", + "zoomed": "25px 75px", + "vertical": "1fr 3fr", + }), + "resolved horizontal Grid longhands must expose used tracks while preserving expanded line names, without publishing physical-axis values for vertical Grid", + ); + + page_vm + .vm_mut() + .eval("document.getElementById('named').style.cssText='width:400px;grid-template-columns:[new] 1fr 1fr';'mutated'")?; + assert_eq!( + page_vm.vm_mut().eval( + "getComputedStyle(document.getElementById('named')).gridTemplateColumns", + )?, + "[a] 21px [b c] 22px [d] 23px [e c] 22px [d] 23px [e f] 189px [g]", + "a synchronous style read must stay on the last published layout epoch", + ); + page_vm + .vm_mut() + .screenshot_layout_snapshot(moli_layout::PaintViewport::new(400, 300, 1.0))? + .expect("updated used Grid track CSSOM screenshot layout"); + assert_eq!( + page_vm.vm_mut().eval( + "getComputedStyle(document.getElementById('named')).gridTemplateColumns", + )?, + "[new] 200px 200px", + "a screenshot must publish the new Grid track sizes", + ); + Ok::<_, anyhow::Error>(()) + }) + .await + .expect("used Grid track CSSOM fixture should run"); +} diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs index cc3063d4d..020a561e8 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs @@ -123,6 +123,7 @@ mod element_toggle_event; mod fetch_xhr; mod file_entry_file_callback; mod file_system_directory_reader; +mod grid_resolved_track_values; mod hash_change_delivery; mod history_traversal; mod image_load_event;