use std::collections::HashMap;
use crate::style::{GridLines, GridPlace, Style};
#[derive(Debug, Clone, Default)]
pub(crate) struct ResolvedGrid {
col_lines: HashMap<String, i16>,
row_lines: HashMap<String, i16>,
}
pub(crate) fn resolve(style: &Style) -> Option<ResolvedGrid> {
if style.grid_template_areas.is_empty()
&& style.grid_col_line_names.is_empty()
&& style.grid_row_line_names.is_empty()
{
return None;
}
let mut grid = ResolvedGrid::default();
insert_line_names(&mut grid.col_lines, &style.grid_col_line_names);
insert_line_names(&mut grid.row_lines, &style.grid_row_line_names);
for (name, rect) in area_rects(&style.grid_template_areas) {
grid.col_lines
.entry(format!("{name}-start"))
.or_insert(rect.col_start);
grid.col_lines
.entry(format!("{name}-end"))
.or_insert(rect.col_end);
grid.row_lines
.entry(format!("{name}-start"))
.or_insert(rect.row_start);
grid.row_lines
.entry(format!("{name}-end"))
.or_insert(rect.row_end);
}
Some(grid)
}
fn insert_line_names(out: &mut HashMap<String, i16>, names: &[Vec<String>]) {
for (i, line) in names.iter().enumerate() {
for name in line {
out.entry(name.clone()).or_insert(line_no(i));
}
}
}
pub(crate) fn place(child: &Style, grid: &ResolvedGrid) -> (GridPlace, GridPlace) {
if let Some(area) = &child.grid_area {
let col = axis_from_lines(
grid.col_lines.get(&format!("{area}-start")).copied(),
grid.col_lines.get(&format!("{area}-end")).copied(),
);
let row = axis_from_lines(
grid.row_lines.get(&format!("{area}-start")).copied(),
grid.row_lines.get(&format!("{area}-end")).copied(),
);
return (
col.unwrap_or(child.grid_column),
row.unwrap_or(child.grid_row),
);
}
let col = named_axis(&child.grid_column_lines, &grid.col_lines).unwrap_or(child.grid_column);
let row = named_axis(&child.grid_row_lines, &grid.row_lines).unwrap_or(child.grid_row);
(col, row)
}
pub(crate) fn area_dims(areas: &[Vec<Option<String>>]) -> (usize, usize) {
let rows = areas.len();
let cols = areas.iter().map(Vec::len).max().unwrap_or(0);
(rows, cols)
}
fn named_axis(lines: &GridLines, table: &HashMap<String, i16>) -> Option<GridPlace> {
let start = lines.start.as_ref().and_then(|n| table.get(n).copied());
let end = lines.end.as_ref().and_then(|n| table.get(n).copied());
if start.is_none() && end.is_none() {
return None;
}
axis_from_lines(start, end)
}
fn axis_from_lines(start: Option<i16>, end: Option<i16>) -> Option<GridPlace> {
match (start, end) {
(Some(s), Some(e)) if e > s => Some(GridPlace {
start: Some(s),
span: u16::try_from(e - s).ok().filter(|n| *n > 1),
}),
(Some(s), _) => Some(GridPlace {
start: Some(s),
span: None,
}),
(None, Some(e)) => Some(GridPlace {
start: Some(e),
span: None,
}),
(None, None) => None,
}
}
struct AreaRect {
row_start: i16,
row_end: i16,
col_start: i16,
col_end: i16,
}
fn area_rects(areas: &[Vec<Option<String>>]) -> Vec<(String, AreaRect)> {
let mut bounds: HashMap<String, [usize; 5]> = HashMap::new();
for (r, row) in areas.iter().enumerate() {
for (c, cell) in row.iter().enumerate() {
let Some(name) = cell else { continue };
let e = bounds.entry(name.clone()).or_insert([r, r, c, c, 0]);
e[0] = e[0].min(r);
e[1] = e[1].max(r);
e[2] = e[2].min(c);
e[3] = e[3].max(c);
e[4] += 1;
}
}
let mut out = Vec::with_capacity(bounds.len());
for (name, [r0, r1, c0, c1, count]) in bounds {
if (r1 - r0 + 1) * (c1 - c0 + 1) != count {
continue;
}
out.push((
name,
AreaRect {
row_start: line_no(r0),
row_end: line_no(r1 + 1),
col_start: line_no(c0),
col_end: line_no(c1 + 1),
},
));
}
out
}
fn line_no(index: usize) -> i16 {
i16::try_from(index + 1).unwrap_or(i16::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Style;
fn areas(rows: &[&str]) -> Style {
Style::default().grid_template_areas(rows.iter().copied())
}
#[test]
fn area_resolves_to_lines_on_both_axes() {
let parent = areas(&["header header", "nav main"]);
let grid = resolve(&parent).expect("has areas");
let (col, row) = place(&Style::default().grid_area("header"), &grid);
assert_eq!(col.start, Some(1));
assert_eq!(col.span, Some(2));
assert_eq!(row.start, Some(1));
assert_eq!(row.span, None);
let (col, row) = place(&Style::default().grid_area("main"), &grid);
assert_eq!(col.start, Some(2));
assert_eq!(col.span, None);
assert_eq!(row.start, Some(2));
}
#[test]
fn dot_is_an_empty_cell() {
let parent = areas(&["logo .", ". main"]);
let grid = resolve(&parent).expect("has areas");
let (col, row) = place(&Style::default().grid_area("main"), &grid);
assert_eq!((col.start, row.start), (Some(2), Some(2)));
}
#[test]
fn non_rectangular_area_is_dropped() {
let parent = areas(&["bad bad", "bad ."]);
let grid = resolve(&parent).expect("has areas");
let fallback = Style::default().grid_area("bad").grid_col(7, 1);
let (col, _row) = place(&fallback, &grid);
assert_eq!(col.start, Some(7), "unresolved area keeps numeric fallback");
}
#[test]
fn explicit_named_lines_place_a_span() {
let parent = Style::default()
.grid_cols([crate::style::Track::Px(100.0); 3])
.grid_col_names(["sidebar", "divider", "content", "edge"]);
let grid = resolve(&parent).expect("has line names");
let child = Style::default().grid_col_lines("sidebar", "content");
let (col, _row) = place(&child, &grid);
assert_eq!(col.start, Some(1));
assert_eq!(col.span, Some(2));
}
#[test]
fn no_names_means_no_resolved_grid() {
assert!(resolve(&Style::default().grid_cols([crate::style::Track::Fr(1.0)])).is_none());
}
}