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(