diff --git a/crates/obscura-render/src/dom.rs b/crates/obscura-render/src/dom.rs index 06bb3e1..c7f2aee 100644 --- a/crates/obscura-render/src/dom.rs +++ b/crates/obscura-render/src/dom.rs @@ -2233,7 +2233,36 @@ fn layout_dom_once( // (updated to its content width inside the block below). let mut child_cb_width = inh.cb_width; let mut child_cb_height_definite = false; + let inherited_grid_auto_tracks = styles.get(&id).and_then(|style| { + (style.grid_auto_columns_inherit || style.grid_auto_rows_inherit) + .then(|| { + tree.get_node(id) + .and_then(|node| node.parent) + .and_then(|parent| styles.get(&parent)) + .map(|parent| { + ( + parent.grid_auto_columns.clone(), + parent.grid_auto_rows.clone(), + ) + }) + .unwrap_or_default() + }) + }); if let Some(style) = styles.get_mut(&id) { + if style.grid_auto_columns_inherit { + style.grid_auto_columns = inherited_grid_auto_tracks + .as_ref() + .map(|tracks| tracks.0.clone()) + .unwrap_or_default(); + style.grid_auto_columns_inherit = false; + } + if style.grid_auto_rows_inherit { + style.grid_auto_rows = inherited_grid_auto_tracks + .as_ref() + .map(|tracks| tracks.1.clone()) + .unwrap_or_default(); + style.grid_auto_rows_inherit = false; + } if style.display_inherit { style.display = inh.display; style.display_contents = inh.display_contents; @@ -2690,6 +2719,8 @@ fn layout_dom_once( let host_is_inline_block = style.is_inline_block; let host_flow_root = style.flow_root; let host_is_table_box = style.is_table_box; + let host_grid_auto_columns = style.grid_auto_columns.clone(); + let host_grid_auto_rows = style.grid_auto_rows.clone(); let host_overflow_x = if style.overflow_scroll_x { 2 } else if style.overflow_clip_x { @@ -2705,6 +2736,14 @@ fn layout_dom_once( 0 }; let settle_pseudo = |pseudo: &mut crate::LayoutStyle| { + if pseudo.grid_auto_columns_inherit { + pseudo.grid_auto_columns = host_grid_auto_columns.clone(); + pseudo.grid_auto_columns_inherit = false; + } + if pseudo.grid_auto_rows_inherit { + pseudo.grid_auto_rows = host_grid_auto_rows.clone(); + pseudo.grid_auto_rows_inherit = false; + } if pseudo.display_inherit { pseudo.display = host_display; pseudo.display_contents = host_display_contents; @@ -11615,4 +11654,110 @@ mod tests { ); } + #[test] + fn grid_auto_columns_size_successive_implicit_column_tracks() { + let tree = parse_html( + r#" +
+
+
+
+
"#, + ); + let laid = layout_dom(&tree, (500.0, 200.0)); + let rect = |id| laid.rects[&tree.get_element_by_id(id).unwrap()]; + + assert_eq!( + rect("first"), + Rect { x: 0.0, y: 0.0, width: 100.0, height: 20.0 } + ); + assert_eq!( + rect("second"), + Rect { x: 100.0, y: 0.0, width: 50.0, height: 20.0 } + ); + assert_eq!( + rect("third"), + Rect { x: 150.0, y: 0.0, width: 50.0, height: 20.0 } + ); + } + + #[test] + fn grid_auto_rows_size_successive_implicit_row_tracks() { + let tree = parse_html( + r#" +
+
+
+
+
"#, + ); + let laid = layout_dom(&tree, (500.0, 400.0)); + let rect = |id| laid.rects[&tree.get_element_by_id(id).unwrap()]; + + assert_eq!( + rect("first"), + Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 } + ); + assert_eq!( + rect("second"), + Rect { x: 0.0, y: 100.0, width: 100.0, height: 50.0 } + ); + assert_eq!( + rect("third"), + Rect { x: 0.0, y: 150.0, width: 100.0, height: 50.0 } + ); + } + + #[test] + fn grid_auto_track_inherit_uses_the_parent_computed_lists() { + let tree = parse_html( + r#" +
+
+
"#, + ); + let laid = layout_dom(&tree, (500.0, 300.0)); + let grid = tree.get_element_by_id("grid").unwrap(); + let implicit = tree.get_element_by_id("implicit").unwrap(); + + assert_eq!(laid.rects[&implicit].x, 100.0); + assert_eq!(laid.rects[&implicit].width, 60.0); + assert_eq!(laid.styles[&grid].grid_auto_columns.len(), 1); + assert_eq!(laid.styles[&grid].grid_auto_rows.len(), 1); + assert!(!laid.styles[&grid].grid_auto_columns_inherit); + assert!(!laid.styles[&grid].grid_auto_rows_inherit); + } + } diff --git a/crates/obscura-render/src/lib.rs b/crates/obscura-render/src/lib.rs index 83abbfc..a727135 100644 --- a/crates/obscura-render/src/lib.rs +++ b/crates/obscura-render/src/lib.rs @@ -733,6 +733,16 @@ pub struct LayoutStyle { // resolved to line placements on children in a later pass. pub grid_template_columns: Vec>, pub grid_template_rows: Vec>, + /// Track sizing functions for columns created outside the explicit grid. + /// An empty list is the CSS initial `auto` value: taffy supplies one + /// automatic implicit track and cycles a non-empty authored list. + pub grid_auto_columns: Vec, + /// Track sizing functions for rows created outside the explicit grid. + pub grid_auto_rows: Vec, + /// Explicit CSS-wide `inherit` markers. The properties are normally + /// non-inherited, so only the keyword copies the parent's computed list. + pub(crate) grid_auto_columns_inherit: bool, + pub(crate) grid_auto_rows_inherit: bool, /// `grid-template-columns: subgrid`. Taffy has no native subgrid track /// component, so `dom` resolves the safe full-span column subset after /// measuring the non-subgridded ancestor's intrinsic track contributions. @@ -1457,6 +1467,12 @@ pub(crate) fn to_taffy_style(style: &LayoutStyle) -> Style { if !style.grid_template_rows.is_empty() { s.grid_template_rows = style.grid_template_rows.clone(); } + if !style.grid_auto_columns.is_empty() { + s.grid_auto_columns = style.grid_auto_columns.clone(); + } + if !style.grid_auto_rows.is_empty() { + s.grid_auto_rows = style.grid_auto_rows.clone(); + } if let Some(flow) = style.grid_auto_flow { s.grid_auto_flow = flow; } diff --git a/crates/obscura-render/src/style.rs b/crates/obscura-render/src/style.rs index 05a5e1d..a758870 100644 --- a/crates/obscura-render/src/style.rs +++ b/crates/obscura-render/src/style.rs @@ -1552,6 +1552,8 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) { style.grid_template_rows = tracks; style.grid_row_line_names = (!names.is_empty()).then(|| build_line_map(names)); } + "grid-auto-columns" => apply_grid_auto_tracks(style, value, true), + "grid-auto-rows" => apply_grid_auto_tracks(style, value, false), "grid-template-areas" => style.grid_areas = Some(parse_grid_areas(value)), "grid-template" => parse_grid_template(style, value), "grid" => parse_grid_shorthand(style, value), @@ -1786,6 +1788,8 @@ pub(crate) fn supports_declaration(name: &str, value: &str) -> bool { | "border-collapse" | "grid-template-columns" | "grid-template-rows" + | "grid-auto-columns" + | "grid-auto-rows" | "grid-template-areas" | "grid-template" | "grid" @@ -1901,6 +1905,9 @@ pub(crate) fn supports_declaration(name: &str, value: &str) -> bool { !part.is_empty() && parse_grid_line_kind(part).is_some() }) } + "grid-auto-columns" | "grid-auto-rows" => { + parse_grid_auto_track_list(value).is_some() + } "color" | "-webkit-text-fill-color" | "background-color" | "border-color" => { parse_color(value).is_some() } @@ -2764,6 +2771,105 @@ fn track(tok: &str) -> taffy::TrackSizingFunction { MinMax { min: min_track(t), max: max_track(t) } } +/// Parse the `+` grammar shared by `grid-auto-columns` and +/// `grid-auto-rows`. Unlike template track lists, implicit track lists do not +/// accept line names or `repeat()`: the authored list itself is repeated as +/// needed for successive implicit tracks. +fn parse_grid_auto_track_list(value: &str) -> Option> { + let tokens = tokenize_tracks(value); + if tokens.is_empty() + || tokens.iter().any(|token| { + let lower = token.trim().to_ascii_lowercase(); + lower.starts_with('[') + || lower.starts_with("repeat(") + || lower == "subgrid" + || matches!( + lower.as_str(), + "initial" | "inherit" | "unset" | "revert" | "revert-layer" + ) + || !valid_grid_auto_track(&lower) + }) + { + return None; + } + Some(tokens.iter().map(|token| track(token)).collect()) +} + +fn valid_grid_auto_track(value: &str) -> bool { + if matches!(value, "auto" | "min-content" | "max-content") { + return true; + } + if let Some(inner) = value + .strip_prefix("minmax(") + .and_then(|inner| inner.strip_suffix(')')) + { + let Some((min, max)) = inner.split_once(',') else { + return false; + }; + return valid_grid_track_breadth(min.trim(), false) + && valid_grid_track_breadth(max.trim(), true); + } + if let Some(inner) = value + .strip_prefix("fit-content(") + .and_then(|inner| inner.strip_suffix(')')) + { + return valid_grid_track_length_percentage(inner.trim()); + } + valid_grid_track_breadth(value, true) +} + +fn valid_grid_track_breadth(value: &str, flex_allowed: bool) -> bool { + matches!(value, "auto" | "min-content" | "max-content") + || (flex_allowed + && value + .strip_suffix("fr") + .and_then(|number| number.trim().parse::().ok()) + .is_some_and(|number| number.is_finite() && number >= 0.0)) + || valid_grid_track_length_percentage(value) +} + +fn valid_grid_track_length_percentage(value: &str) -> bool { + let lower = value.to_ascii_lowercase(); + if lower == "0" { + return true; + } + for suffix in ["rem", "vmin", "vmax", "px", "pt", "em", "ex", "vw", "vh", "%"] { + if let Some(number) = lower.strip_suffix(suffix) { + return number + .trim() + .parse::() + .ok() + .is_some_and(|number| number.is_finite() && number >= 0.0); + } + } + false +} + +fn apply_grid_auto_tracks(style: &mut LayoutStyle, value: &str, columns: bool) { + let lower = value.trim().to_ascii_lowercase(); + let (tracks, inherit) = match lower.as_str() { + "inherit" => (Vec::new(), true), + // These properties are non-inherited. The compact cascade has no + // retained lower-origin/layer declaration for revert, so it follows + // the existing non-inherited-property policy and uses the initial + // automatic implicit track. + "initial" | "unset" | "revert" | "revert-layer" => (Vec::new(), false), + _ => { + let Some(tracks) = parse_grid_auto_track_list(value) else { + return; + }; + (tracks, false) + } + }; + if columns { + style.grid_auto_columns = tracks; + style.grid_auto_columns_inherit = inherit; + } else { + style.grid_auto_rows = tracks; + style.grid_auto_rows_inherit = inherit; + } +} + fn min_track(tok: &str) -> taffy::MinTrackSizingFunction { use taffy::MinTrackSizingFunction as M; let lower = tok.to_ascii_lowercase(); @@ -6604,6 +6710,45 @@ mod tests { assert_eq!(shorthand.grid_template_columns.len(), 3); } + #[test] + fn implicit_grid_track_lists_parse_and_css_wide_values_reset_them() { + let tracks = compute_style( + "div", + Some( + "grid-auto-columns:50px minmax(20px,1fr);\ + grid-auto-rows:min-content 25%;", + ), + ); + assert_eq!(tracks.grid_auto_columns.len(), 2); + assert_eq!(tracks.grid_auto_rows.len(), 2); + assert!(supports_declaration("grid-auto-columns", "50px minmax(20px,1fr)")); + assert!(supports_declaration("grid-auto-rows", "min-content 25%")); + assert!(!supports_declaration("grid-auto-columns", "repeat(2,50px)")); + + for keyword in ["initial", "unset", "revert", "revert-layer"] { + let reset = compute_style( + "div", + Some(&format!( + "grid-auto-columns:50px;grid-auto-columns:{keyword};\ + grid-auto-rows:60px;grid-auto-rows:{keyword}" + )), + ); + assert!( + reset.grid_auto_columns.is_empty() && reset.grid_auto_rows.is_empty(), + "{keyword} must restore the initial automatic implicit track" + ); + assert!(!reset.grid_auto_columns_inherit); + assert!(!reset.grid_auto_rows_inherit); + } + + let inherited = compute_style( + "div", + Some("grid-auto-columns:inherit;grid-auto-rows:inherit"), + ); + assert!(inherited.grid_auto_columns_inherit); + assert!(inherited.grid_auto_rows_inherit); + } + #[test] fn grid_placement_longhands_preserve_the_opposite_side() { let style = compute_style(