feat(layout): expose resolved Grid tracks

This commit is contained in:
ldm0
2026-08-30 05:41:30 +08:00
committed by Donough Liu
parent 840ba602ac
commit b09fee1df2
17 changed files with 668 additions and 40 deletions
Generated
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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"
+230
View File
@@ -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<Atom>,
detailed: taffy::DetailedGridInfo,
) -> Option<LayoutResolvedGridTracks> {
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<Vec<Atom>>,
) -> Option<LayoutResolvedGridTrackList> {
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::<Option<Vec<_>>>()?;
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<f32> {
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<Atom>],
template_line_names: &[Vec<Atom>],
explicit_track_count: usize,
auto_repetitions: usize,
) -> Option<Vec<Vec<Atom>>> {
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<Atom>, 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<Atom> {
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);
}
}
+34
View File
@@ -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<f32>,
/// 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<Vec<Atom>>,
}
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 {
+10 -2
View File
@@ -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<N> {
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<N> {
ContentQuads(Vec<LayoutQuad>),
TextRangeRects(Vec<LayoutQuad>),
ElementMetrics(Option<LayoutElementMetrics<N>>),
UsedGridTracks(Option<LayoutResolvedGridTracks>),
ScrollIntoViewGeometry(Option<LayoutScrollIntoViewGeometry<N>>),
IntersectionGeometry(Option<LayoutIntersectionGeometry>),
HitTest(Option<LayoutHit<N>>),
@@ -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),
+22 -1
View File
@@ -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<LayoutResolvedGridTracks> {
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![
+33 -3
View File
@@ -25,6 +25,7 @@ pub struct FrozenLayoutBox<N> {
pub geometry_source: Option<N>,
pub principal_source: Option<N>,
pub hit_source: Option<N>,
pub resolved_grid_tracks: Option<super::model::LayoutResolvedGridTracks>,
/// Paint ordinal of this box's scrollbar/corner surface, if any.
pub(crate) control_paint_order: Option<u32>,
}
@@ -122,9 +123,38 @@ where
}
let box_allocations = self.boxes.iter().fold(0usize, |bytes, layout_box| {
bytes.saturating_add(allocation::<LayoutFragmentId>(
layout_box.fragments.capacity(),
))
let fragment_bytes = allocation::<LayoutFragmentId>(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::<f32>(capacity))
});
let line_name_bytes =
[&grid.rows, &grid.columns]
.into_iter()
.fold(0usize, |bytes, tracks| {
let outer =
allocation::<Vec<String>>(tracks.explicit_line_names.capacity());
tracks.explicit_line_names.iter().fold(
bytes.saturating_add(outer),
|bytes, names| {
let atoms = allocation::<style::Atom>(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::<Self>()
.saturating_add(allocation::<FrozenLayoutBox<N>>(self.boxes.capacity()))
+6 -5
View File
@@ -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::{
+15 -8
View File
@@ -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();
+29 -8
View File
@@ -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<PositionedStaticPlaceholder>,
numeric_unrounded_layouts: Vec<Layout>,
@@ -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<N> RoundTree for LayoutWorld<N>
+9 -3
View File
@@ -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<N> {
/// 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<LayoutResolvedGridTracks>,
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),
@@ -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,
@@ -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,
@@ -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<Option<LayoutResolvedGridTracks>, 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,
@@ -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<String> {
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<String> {
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<f64> {
}
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::<i32>()
.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();
@@ -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 = `<style>
html,body{margin:0}
.grid{display:grid;width:300px}
#intrinsic{grid-template-columns:fit-content(75%)}
#intrinsic>div{width:75px}
#rows{height:100px;grid-template-rows:30px 1fr}
#named{grid-template-columns:[a] 21px [b] repeat(2,[c] 22px [d] 23px [e]) [f] 1fr [g]}
#automatic{grid-template-columns:[a] 21px [b] repeat(auto-fill,[c] 22px [d] 23px [e]) [f] 24px [g]}
#auto-fit{width:44px;grid-template-columns:1px [a] repeat(auto-fit,[b] 20px [c]) [d] 3px}
#implicit{grid-template-columns:none;grid-auto-columns:35px}
#implicit>div{grid-column:1}
#leading{grid-template-columns:[a] 40px [b];grid-auto-columns:15px}
#leading>div{grid-column:-3}
#areas{width:100px;grid-template-areas:'a a';grid-template-columns:none}
#area-repeat{width:100px;grid-template-areas:'a a a a a a a a';grid-template-columns:repeat(auto-fill,20px)}
#fractional{width:100px;grid-template-columns:repeat(3,1fr)}
#zoomed{zoom:2;width:100px;grid-template-columns:1fr 3fr}
#vertical{writing-mode:vertical-rl;width:100px;height:300px;grid-template-columns:1fr 3fr}
</style>`;
document.body.innerHTML = `
<div class=grid id=intrinsic><div></div></div>
<div class=grid id=rows></div>
<div class=grid id=named></div>
<div class=grid id=automatic></div>
<div class=grid id=auto-fit></div>
<div class=grid id=implicit><div></div></div>
<div class=grid id=leading><div></div></div>
<div class=grid id=areas></div>
<div class=grid id=area-repeat></div>
<div class=grid id=fractional></div>
<div class=grid id=zoomed></div>
<div class=grid id=vertical></div>`;
'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");
}
@@ -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;