From df9359ec870489a59663d58c7e9f4854774f5670 Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:41:56 +0000 Subject: [PATCH] render: honor responsive grid track syntax --- crates/obscura-render/src/lib.rs | 28 +++---- crates/obscura-render/src/style.rs | 115 ++++++++++++++++++++++++++--- 2 files changed, 117 insertions(+), 26 deletions(-) diff --git a/crates/obscura-render/src/lib.rs b/crates/obscura-render/src/lib.rs index 0f48e02..0089dbc 100644 --- a/crates/obscura-render/src/lib.rs +++ b/crates/obscura-render/src/lib.rs @@ -278,8 +278,9 @@ pub struct LayoutStyle { // CSS Grid. Tracks are stored as taffy sizing functions; `grid_areas` is the // parsed `grid-template-areas` matrix (one Vec per row, `.` for a null cell), // resolved to line placements on children in a later pass. - pub grid_template_columns: Vec, - pub grid_template_rows: Vec, + pub grid_template_columns: Vec>, + pub grid_template_rows: Vec>, + pub grid_auto_flow: Option, pub grid_areas: Option>>, pub grid_area_name: Option, pub grid_column: Option>, @@ -702,29 +703,22 @@ pub(crate) fn to_taffy_style(style: &LayoutStyle) -> Style { s.flex_basis = dimension(style.flex_basis); } - // Grid container tracks and gaps. obscura expands `repeat()` itself during - // parsing, so every stored track is a single (non-repeated) `MinMax`; wrap - // each as a `GridTemplateComponent::Single` for taffy 0.12. The 0.7-era + // Grid container tracks and gaps. Numeric repeat() values are expanded + // during parsing, while auto-fill/auto-fit remain native taffy repetition + // components so their count can use the final container size. The 0.7-era // fr->Auto row workaround (which stopped `minmax(0,1fr)` image rows from // collapsing to a sliver) is gone: taffy 0.12 treats an in-flow child's // vertical available space as indefinite, so fr rows of an auto-height grid // size to their content the way real CSS does. if style.display == Display::Grid { if !style.grid_template_columns.is_empty() { - s.grid_template_columns = style - .grid_template_columns - .iter() - .cloned() - .map(taffy::GridTemplateComponent::Single) - .collect(); + s.grid_template_columns = style.grid_template_columns.clone(); } if !style.grid_template_rows.is_empty() { - s.grid_template_rows = style - .grid_template_rows - .iter() - .cloned() - .map(taffy::GridTemplateComponent::Single) - .collect(); + s.grid_template_rows = style.grid_template_rows.clone(); + } + if let Some(flow) = style.grid_auto_flow { + s.grid_auto_flow = flow; } } let cg = style.column_gap.unwrap_or(0.0); diff --git a/crates/obscura-render/src/style.rs b/crates/obscura-render/src/style.rs index b34a25e..2c8c666 100644 --- a/crates/obscura-render/src/style.rs +++ b/crates/obscura-render/src/style.rs @@ -588,6 +588,8 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) { } "grid-template-areas" => style.grid_areas = Some(parse_grid_areas(value)), "grid-template" => parse_grid_template(style, value), + "grid" => parse_grid_shorthand(style, value), + "grid-auto-flow" => style.grid_auto_flow = parse_grid_auto_flow(value), "grid-area" => { // Named area (single ident) or line form `r/c/r/c`. We only resolve // the named-area case here; line forms are handled by grid-row/column. @@ -787,11 +789,16 @@ fn scale_number(s: &str) -> Option { /// minmax(0,1fr)`) into taffy sizing functions. Tokenizes respecting the /// parentheses in `minmax(...)` / `fit-content(...)`. /// Parse a track list into taffy sizing functions plus the `[line-name]` map -/// (name -> 1-based grid line number). `repeat(n, ...)` is expanded to n copies -/// (it was previously collapsed to a single phantom `Auto` track, which broke -/// every `repeat()`-based grid) and `[name]` annotations are captured, not +/// (name -> 1-based grid line number). `repeat(n, ...)` is expanded to n copies; +/// auto-fill/auto-fit remain typed repetitions so layout can derive their count +/// from the used container width. `[name]` annotations are captured rather than /// turned into tracks. First occurrence of a name wins. -pub(crate) fn parse_track_list_named(value: &str) -> (Vec, Vec<(String, i16)>) { +pub(crate) fn parse_track_list_named( + value: &str, +) -> ( + Vec>, + Vec<(String, i16)>, +) { let mut tracks = Vec::new(); let mut names = Vec::new(); let mut line: i16 = 1; @@ -803,7 +810,7 @@ pub(crate) fn parse_track_list_named(value: &str) -> (Vec, + tracks: &mut Vec>, names: &mut Vec<(String, i16)>, line: &mut i16, ) { @@ -819,10 +826,29 @@ fn expand_track_token( if lower.starts_with("repeat(") && t.ends_with(')') { let inner = &t["repeat(".len()..t.len() - 1]; if let Some((cnt, sub)) = inner.split_once(',') { - // `auto-fill`/`auto-fit` counts need the container width; fall back - // to one repetition (a reasonable minimum) rather than dropping it. - let count = cnt.trim().parse::().unwrap_or(1).min(1000); let subtoks = tokenize_tracks(sub.trim()); + let repetition = match cnt.trim().to_ascii_lowercase().as_str() { + "auto-fill" => Some(taffy::RepetitionCount::AutoFill), + "auto-fit" => Some(taffy::RepetitionCount::AutoFit), + _ => None, + }; + if let Some(count) = repetition { + let repeated_tracks = subtoks + .iter() + .filter(|st| !st.trim_start().starts_with('[')) + .map(|st| track(st)) + .collect(); + tracks.push(taffy::GridTemplateComponent::Repeat( + taffy::GridTemplateRepetition { + count, + tracks: repeated_tracks, + line_names: Vec::new(), + }, + )); + *line += 1; + return; + } + let count = cnt.trim().parse::().unwrap_or(1).min(1000); for _ in 0..count { for st in &subtoks { expand_track_token(st, tracks, names, line); @@ -831,7 +857,7 @@ fn expand_track_token( } return; } - tracks.push(track(t)); + tracks.push(taffy::GridTemplateComponent::Single(track(t))); *line += 1; } @@ -974,6 +1000,53 @@ fn parse_grid_template(style: &mut LayoutStyle, value: &str) { } } +/// Parse the common `grid` shorthand forms. A side containing `auto-flow` +/// defines the implicit placement axis; the opposite side is the explicit +/// track list. Without `auto-flow`, this is the `grid-template` shorthand. +fn parse_grid_shorthand(style: &mut LayoutStyle, value: &str) { + let Some((rows, columns)) = value.split_once('/') else { + parse_grid_template(style, value); + return; + }; + let rows = rows.trim(); + let columns = columns.trim(); + if rows.to_ascii_lowercase().contains("auto-flow") { + style.grid_template_rows.clear(); + let (tracks, names) = parse_track_list_named(columns); + style.grid_template_columns = tracks; + style.grid_col_line_names = (!names.is_empty()).then(|| build_line_map(names)); + style.grid_auto_flow = Some(if rows.to_ascii_lowercase().contains("dense") { + taffy::GridAutoFlow::RowDense + } else { + taffy::GridAutoFlow::Row + }); + } else if columns.to_ascii_lowercase().contains("auto-flow") { + style.grid_template_columns.clear(); + let (tracks, names) = parse_track_list_named(rows); + style.grid_template_rows = tracks; + style.grid_row_line_names = (!names.is_empty()).then(|| build_line_map(names)); + style.grid_auto_flow = Some(if columns.to_ascii_lowercase().contains("dense") { + taffy::GridAutoFlow::ColumnDense + } else { + taffy::GridAutoFlow::Column + }); + } else { + parse_grid_template(style, value); + } +} + +fn parse_grid_auto_flow(value: &str) -> Option { + let lower = value.trim().to_ascii_lowercase(); + let dense = lower.split_whitespace().any(|token| token == "dense"); + let column = lower.split_whitespace().any(|token| token == "column"); + Some(match (column, dense) { + (false, false) => taffy::GridAutoFlow::Row, + (false, true) => taffy::GridAutoFlow::RowDense, + (true, false) => taffy::GridAutoFlow::Column, + (true, true) => taffy::GridAutoFlow::ColumnDense, + }) +} + /// Store a `grid-column`/`grid-row` value. Numeric/`span` forms resolve to a /// `taffy::Line` now; a value that names a grid line (`content-start / /// content-end`, or the `grid-column: content` area shorthand) is kept raw and @@ -2252,6 +2325,30 @@ mod tests { assert_eq!(s.padding_relative[1], Some(crate::Dimension::Vmin(2.0))); } + #[test] + fn responsive_grid_repetitions_and_shorthand_remain_typed() { + let auto = compute_style( + "div", + Some("display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr))"), + ); + assert!(matches!( + auto.grid_template_columns.as_slice(), + [taffy::GridTemplateComponent::Repeat( + taffy::GridTemplateRepetition { + count: taffy::RepetitionCount::AutoFit, + .. + } + )] + )); + + let shorthand = compute_style( + "div", + Some("display:grid;grid:auto-flow/repeat(3,1fr)"), + ); + assert_eq!(shorthand.grid_auto_flow, Some(taffy::GridAutoFlow::Row)); + assert_eq!(shorthand.grid_template_columns.len(), 3); + } + #[test] fn ua_defaults_and_ignore_unknown() { let s = compute_style("span", Some("color: red; ; bogus: ; display: none"));