use std::collections::{HashMap, HashSet};
use crate::geometry::{Rect, Size};
#[derive(Clone, Debug)]
pub enum TrackSizing {
Fixed(f32),
Fr(f32),
Auto,
MinMax(Box<TrackSizing>, Box<TrackSizing>),
MinContent,
MaxContent,
Repeat(usize, Box<TrackSizing>),
}
#[derive(Clone, Debug, Default)]
pub struct GridTemplate {
pub rows: Vec<TrackSizing>,
pub cols: Vec<TrackSizing>,
pub areas: Option<Vec<Vec<Option<String>>>>,
pub row_gap: f32,
pub col_gap: f32,
}
#[derive(Clone, Debug)]
pub enum GridLine {
Line(i32),
Auto,
Named(String),
}
#[derive(Clone, Debug)]
pub struct GridSpan {
pub line: GridLine,
pub span: usize,
}
#[derive(Clone, Debug)]
pub struct GridPlacement {
pub row: GridSpan,
pub col: GridSpan,
}
impl GridPlacement {
pub fn auto() -> Self {
Self {
row: GridSpan {
line: GridLine::Auto,
span: 1,
},
col: GridSpan {
line: GridLine::Auto,
span: 1,
},
}
}
pub fn at(row: i32, col: i32) -> Self {
Self {
row: GridSpan {
line: GridLine::Line(row),
span: 1,
},
col: GridSpan {
line: GridLine::Line(col),
span: 1,
},
}
}
pub fn span(row: i32, col: i32, row_span: usize, col_span: usize) -> Self {
Self {
row: GridSpan {
line: GridLine::Line(row),
span: row_span.max(1),
},
col: GridSpan {
line: GridLine::Line(col),
span: col_span.max(1),
},
}
}
}
#[derive(Clone, Debug)]
pub struct GridItem {
pub placement: GridPlacement,
pub min_content_size: Size,
pub max_content_size: Size,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ResolvedPlacement {
row_start: usize,
row_end: usize,
col_start: usize,
col_end: usize,
}
#[derive(Clone, Debug)]
struct TrackRecord {
sizing: TrackSizing,
base: f32,
growth_limit: f32,
}
fn build_area_map(areas: &[Vec<Option<String>>]) -> HashMap<String, (usize, usize, usize, usize)> {
let mut map: HashMap<String, (usize, usize, usize, usize)> = HashMap::new();
for (r, row) in areas.iter().enumerate() {
for (c, cell) in row.iter().enumerate() {
if let Some(name) = cell {
let row1 = r + 1;
let col1 = c + 1;
map.entry(name.clone())
.and_modify(|e| {
e.0 = e.0.min(row1);
e.1 = e.1.min(col1);
e.2 = e.2.max(row1 + 1);
e.3 = e.3.max(col1 + 1);
})
.or_insert((row1, col1, row1 + 1, col1 + 1));
}
}
}
map
}
fn expand_tracks(specs: &[TrackSizing]) -> Vec<TrackSizing> {
let mut out = Vec::new();
for s in specs {
expand_one(s, &mut out);
}
out
}
fn expand_one(s: &TrackSizing, out: &mut Vec<TrackSizing>) {
match s {
TrackSizing::Repeat(n, inner) => {
for _ in 0..*n {
expand_one(inner, out);
}
}
other => out.push(other.clone()),
}
}
fn ensure_track_count(tracks: &mut Vec<TrackRecord>, needed: usize) {
while tracks.len() < needed {
tracks.push(TrackRecord {
sizing: TrackSizing::Auto,
base: 0.0,
growth_limit: f32::INFINITY,
});
}
}
fn make_track_record(sizing: &TrackSizing) -> TrackRecord {
let (base, growth_limit) = initial_base_growth(sizing);
TrackRecord {
sizing: sizing.clone(),
base,
growth_limit,
}
}
fn initial_base_growth(sizing: &TrackSizing) -> (f32, f32) {
match sizing {
TrackSizing::Fixed(px) => (*px, *px),
TrackSizing::Fr(_) => (0.0, f32::INFINITY),
TrackSizing::Auto => (0.0, f32::INFINITY),
TrackSizing::MinContent => (0.0, f32::INFINITY),
TrackSizing::MaxContent => (0.0, f32::INFINITY),
TrackSizing::MinMax(min, max) => {
let (b, _) = initial_base_growth(min);
let (_, g) = initial_base_growth(max);
(b, g)
}
TrackSizing::Repeat(_, inner) => initial_base_growth(inner),
}
}
fn is_intrinsic_min(sizing: &TrackSizing) -> bool {
matches!(
sizing,
TrackSizing::Auto | TrackSizing::MinContent | TrackSizing::MaxContent
)
}
fn uses_max_content_base(sizing: &TrackSizing) -> bool {
matches!(sizing, TrackSizing::MaxContent)
}
pub fn compute_grid(template: &GridTemplate, items: &[GridItem], available: Size) -> Vec<Rect> {
if items.is_empty() {
return Vec::new();
}
let explicit_col_specs = expand_tracks(&template.cols);
let explicit_row_specs = expand_tracks(&template.rows);
let explicit_cols = explicit_col_specs.len();
let explicit_rows = explicit_row_specs.len();
let area_map: HashMap<String, (usize, usize, usize, usize)> = template
.areas
.as_deref()
.map(build_area_map)
.unwrap_or_default();
let mut pre_placements: Vec<Option<ResolvedPlacement>> = vec![None; items.len()];
for (idx, item) in items.iter().enumerate() {
let row_line = resolve_grid_line(&item.placement.row.line, &area_map, true);
let col_line = resolve_grid_line(&item.placement.col.line, &area_map, false);
if let (Some(rs), Some(cs)) = (row_line, col_line) {
let re = rs + item.placement.row.span;
let ce = cs + item.placement.col.span;
pre_placements[idx] = Some(ResolvedPlacement {
row_start: rs,
row_end: re,
col_start: cs,
col_end: ce,
});
} else if let (Some(rs), None) = (row_line, col_line) {
let re = rs + item.placement.row.span;
pre_placements[idx] = Some(ResolvedPlacement {
row_start: rs,
row_end: re,
col_start: 0, col_end: 0,
});
}
}
let mut max_row = explicit_rows.max(1);
let mut max_col = explicit_cols.max(1);
for p in pre_placements.iter().flatten() {
if p.col_start != 0 {
max_row = max_row.max(p.row_end.saturating_sub(1));
max_col = max_col.max(p.col_end.saturating_sub(1));
}
}
let mut row_tracks: Vec<TrackRecord> =
explicit_row_specs.iter().map(make_track_record).collect();
let mut col_tracks: Vec<TrackRecord> =
explicit_col_specs.iter().map(make_track_record).collect();
ensure_track_count(&mut row_tracks, max_row);
ensure_track_count(&mut col_tracks, max_col);
let mut occupied: HashSet<(usize, usize)> = HashSet::new();
for p in pre_placements.iter().flatten() {
if p.col_start != 0 {
mark_occupied(&mut occupied, p);
}
}
let mut placements: Vec<ResolvedPlacement> = vec![
ResolvedPlacement {
row_start: 1,
row_end: 2,
col_start: 1,
col_end: 2
};
items.len()
];
for (idx, pre) in pre_placements.iter().enumerate() {
if let Some(p) = pre {
if p.col_start != 0 {
placements[idx] = p.clone();
}
}
}
let mut cur_row: usize = 1;
let mut cur_col: usize = 1;
let auto_col_count = |col_tracks: &Vec<TrackRecord>| col_tracks.len();
for (idx, item) in items.iter().enumerate() {
let pre = &pre_placements[idx];
if let Some(p) = pre {
if p.col_start != 0 {
continue;
}
}
let span_row = item.placement.row.span.max(1);
let span_col = item.placement.col.span.max(1);
if let Some(p) = pre {
let fixed_rs = p.row_start;
let fixed_re = p.row_end;
let mut c = 1usize;
loop {
if c + span_col - 1 > auto_col_count(&col_tracks) {
ensure_track_count(&mut col_tracks, c + span_col - 1);
}
if slots_free(&occupied, fixed_rs, fixed_re, c, c + span_col) {
break;
}
c += 1;
ensure_track_count(&mut col_tracks, c + span_col - 1);
}
let placement = ResolvedPlacement {
row_start: fixed_rs,
row_end: fixed_re,
col_start: c,
col_end: c + span_col,
};
mark_occupied(&mut occupied, &placement);
placements[idx] = placement;
continue;
}
loop {
let needed_cols = auto_col_count(&col_tracks).max(span_col);
ensure_track_count(&mut col_tracks, needed_cols);
let col_limit = auto_col_count(&col_tracks);
if cur_col + span_col - 1 > col_limit {
cur_row += 1;
cur_col = 1;
ensure_track_count(&mut row_tracks, cur_row + span_row - 1);
}
let rs = cur_row;
let re = cur_row + span_row;
let cs = cur_col;
let ce = cur_col + span_col;
ensure_track_count(&mut row_tracks, re.saturating_sub(1).max(1));
if slots_free(&occupied, rs, re, cs, ce) {
let placement = ResolvedPlacement {
row_start: rs,
row_end: re,
col_start: cs,
col_end: ce,
};
mark_occupied(&mut occupied, &placement);
placements[idx] = placement;
cur_col = cs + span_col;
if cur_col > auto_col_count(&col_tracks) {
cur_row += 1;
cur_col = 1;
}
break;
} else {
cur_col += 1;
if cur_col > col_limit {
cur_row += 1;
cur_col = 1;
ensure_track_count(&mut row_tracks, cur_row);
}
}
}
}
for (item, placement) in items.iter().zip(placements.iter()) {
if placement.row_end - placement.row_start == 1 {
let ri = placement.row_start - 1; if ri < row_tracks.len() {
let track = &mut row_tracks[ri];
if is_intrinsic_min(&track.sizing) {
let content = if uses_max_content_base(&track.sizing) {
item.max_content_size.height
} else {
item.min_content_size.height
};
track.base = track.base.max(content);
}
}
}
if placement.col_end - placement.col_start == 1 {
let ci = placement.col_start - 1;
if ci < col_tracks.len() {
let track = &mut col_tracks[ci];
if is_intrinsic_min(&track.sizing) {
let content = if uses_max_content_base(&track.sizing) {
item.max_content_size.width
} else {
item.min_content_size.width
};
track.base = track.base.max(content);
}
}
}
}
apply_minmax_clamps(&mut col_tracks);
apply_minmax_clamps(&mut row_tracks);
distribute_fr(&mut col_tracks, available.width, template.col_gap);
distribute_fr(&mut row_tracks, available.height, template.row_gap);
let col_starts = compute_starts(&col_tracks, template.col_gap);
let row_starts = compute_starts(&row_tracks, template.row_gap);
let mut out = Vec::with_capacity(items.len());
for placement in &placements {
let cs = placement.col_start.saturating_sub(1); let ce = (placement.col_end - 1).saturating_sub(1); let rs = placement.row_start.saturating_sub(1);
let re = (placement.row_end - 1).saturating_sub(1);
let x = col_starts.get(cs).copied().unwrap_or(0.0);
let y = row_starts.get(rs).copied().unwrap_or(0.0);
let x_end = if ce < col_starts.len() && ce < col_tracks.len() {
col_starts[ce] + col_tracks[ce].base
} else if cs < col_starts.len() && cs < col_tracks.len() {
col_starts[cs] + col_tracks[cs].base
} else {
x
};
let y_end = if re < row_starts.len() && re < row_tracks.len() {
row_starts[re] + row_tracks[re].base
} else if rs < row_starts.len() && rs < row_tracks.len() {
row_starts[rs] + row_tracks[rs].base
} else {
y
};
let w = (x_end - x).max(0.0);
let h = (y_end - y).max(0.0);
out.push(Rect::new(x, y, w, h));
}
out
}
fn resolve_grid_line(
line: &GridLine,
area_map: &HashMap<String, (usize, usize, usize, usize)>,
is_row: bool,
) -> Option<usize> {
match line {
GridLine::Line(n) => {
if *n >= 1 {
Some(*n as usize)
} else if *n < 0 {
Some(1)
} else {
None
}
}
GridLine::Named(name) => area_map
.get(name)
.map(|&(rs, cs, _re, _ce)| if is_row { rs } else { cs }),
GridLine::Auto => None,
}
}
fn mark_occupied(occupied: &mut HashSet<(usize, usize)>, p: &ResolvedPlacement) {
for r in p.row_start..p.row_end {
for c in p.col_start..p.col_end {
occupied.insert((r, c));
}
}
}
fn slots_free(
occupied: &HashSet<(usize, usize)>,
row_start: usize,
row_end: usize,
col_start: usize,
col_end: usize,
) -> bool {
for r in row_start..row_end {
for c in col_start..col_end {
if occupied.contains(&(r, c)) {
return false;
}
}
}
true
}
fn apply_minmax_clamps(tracks: &mut [TrackRecord]) {
for track in tracks.iter_mut() {
if let TrackSizing::MinMax(min_spec, max_spec) = &track.sizing.clone() {
let floor = match min_spec.as_ref() {
TrackSizing::Fixed(px) => *px,
TrackSizing::MinContent | TrackSizing::Auto => track.base,
_ => 0.0,
};
let ceil = match max_spec.as_ref() {
TrackSizing::Fixed(px) => *px,
TrackSizing::MaxContent => f32::INFINITY,
TrackSizing::Fr(_) => f32::INFINITY, _ => f32::INFINITY,
};
track.base =
track
.base
.max(floor)
.min(if ceil.is_finite() { ceil } else { track.base });
track.growth_limit = ceil;
}
}
}
fn distribute_fr(tracks: &mut [TrackRecord], available: f32, gap: f32) {
let gap_total = if tracks.len() > 1 {
gap * (tracks.len() as f32 - 1.0)
} else {
0.0
};
let fixed_sum: f32 = tracks
.iter()
.map(|t| match &t.sizing {
TrackSizing::Fr(_) => 0.0,
TrackSizing::MinMax(_, max) if matches!(max.as_ref(), TrackSizing::Fr(_)) => 0.0,
_ => t.base,
})
.sum();
let free = (available - gap_total - fixed_sum).max(0.0);
let fr_indices: Vec<usize> = tracks
.iter()
.enumerate()
.filter_map(|(i, t)| match &t.sizing {
TrackSizing::Fr(_) => Some(i),
TrackSizing::MinMax(_, max) => {
if matches!(max.as_ref(), TrackSizing::Fr(_)) {
Some(i)
} else {
None
}
}
_ => None,
})
.collect();
if fr_indices.is_empty() {
return;
}
let sum_fr: f32 = fr_indices
.iter()
.map(|&i| fr_value_of(&tracks[i].sizing))
.sum();
if sum_fr <= 0.0 {
return;
}
for i in fr_indices {
let frac = fr_value_of(&tracks[i].sizing);
let computed = frac * free / sum_fr;
let base_floor = tracks[i].base;
tracks[i].base = computed.max(base_floor);
}
}
fn fr_value_of(sizing: &TrackSizing) -> f32 {
match sizing {
TrackSizing::Fr(f) => *f,
TrackSizing::MinMax(_, max) => match max.as_ref() {
TrackSizing::Fr(f) => *f,
_ => 0.0,
},
_ => 0.0,
}
}
fn compute_starts(tracks: &[TrackRecord], gap: f32) -> Vec<f32> {
let mut starts = Vec::with_capacity(tracks.len());
let mut offset = 0.0f32;
for (i, track) in tracks.iter().enumerate() {
if i > 0 {
offset += gap;
}
starts.push(offset);
offset += track.base;
}
starts
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::Size;
fn fixed_item(r: i32, c: i32) -> GridItem {
GridItem {
placement: GridPlacement::at(r, c),
min_content_size: Size::ZERO,
max_content_size: Size::ZERO,
}
}
fn auto_item() -> GridItem {
GridItem {
placement: GridPlacement::auto(),
min_content_size: Size::ZERO,
max_content_size: Size::ZERO,
}
}
#[test]
fn test_single_fixed_track_row() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(100.0)],
cols: vec![TrackSizing::Fixed(200.0)],
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let items = vec![fixed_item(1, 1)];
let rects = compute_grid(&template, &items, Size::new(200.0, 200.0));
assert_eq!(rects.len(), 1);
assert_eq!(rects[0].origin.y, 0.0);
assert_eq!(rects[0].size.height, 100.0);
}
#[test]
fn test_single_fixed_track_col() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(200.0)],
cols: vec![TrackSizing::Fixed(100.0)],
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let items = vec![fixed_item(1, 1)];
let rects = compute_grid(&template, &items, Size::new(200.0, 200.0));
assert_eq!(rects.len(), 1);
assert_eq!(rects[0].origin.x, 0.0);
assert_eq!(rects[0].size.width, 100.0);
}
#[test]
fn test_three_equal_fr_tracks() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(50.0)],
cols: vec![
TrackSizing::Fr(1.0),
TrackSizing::Fr(1.0),
TrackSizing::Fr(1.0),
],
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let items = vec![fixed_item(1, 1), fixed_item(1, 2), fixed_item(1, 3)];
let rects = compute_grid(&template, &items, Size::new(300.0, 50.0));
assert_eq!(rects.len(), 3);
for r in &rects {
assert!(
(r.size.width - 100.0).abs() < 1e-4,
"expected 100px, got {}",
r.size.width
);
}
}
#[test]
fn test_fr_proportional_split_unequal() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(50.0)],
cols: vec![TrackSizing::Fr(1.0), TrackSizing::Fr(2.0)],
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let items = vec![fixed_item(1, 1), fixed_item(1, 2)];
let rects = compute_grid(&template, &items, Size::new(300.0, 50.0));
assert_eq!(rects.len(), 2);
assert!(
(rects[0].size.width - 100.0).abs() < 1e-4,
"col1 = {}",
rects[0].size.width
);
assert!(
(rects[1].size.width - 200.0).abs() < 1e-4,
"col2 = {}",
rects[1].size.width
);
}
#[test]
fn test_minmax_clamps() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(50.0)],
cols: vec![TrackSizing::MinMax(
Box::new(TrackSizing::Fixed(100.0)),
Box::new(TrackSizing::Fr(1.0)),
)],
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let items = vec![fixed_item(1, 1)];
let rects = compute_grid(&template, &items, Size::new(50.0, 50.0));
assert_eq!(rects.len(), 1);
assert!(
rects[0].size.width >= 100.0,
"minmax floor violated: {}",
rects[0].size.width
);
}
#[test]
fn test_nested_minmax_fr() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(50.0)],
cols: vec![TrackSizing::MinMax(
Box::new(TrackSizing::Fixed(50.0)),
Box::new(TrackSizing::Fr(1.0)),
)],
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let items = vec![fixed_item(1, 1)];
let rects = compute_grid(&template, &items, Size::new(200.0, 50.0));
assert_eq!(rects.len(), 1);
assert!(
(rects[0].size.width - 200.0).abs() < 1e-4,
"minmax(50,1fr) with 200px available: expected 200, got {}",
rects[0].size.width
);
}
#[test]
fn test_auto_track_sizes_to_content() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(50.0)],
cols: vec![TrackSizing::Auto],
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let items = vec![GridItem {
placement: GridPlacement::at(1, 1),
min_content_size: Size::new(40.0, 50.0),
max_content_size: Size::new(80.0, 50.0),
}];
let rects = compute_grid(&template, &items, Size::new(200.0, 50.0));
assert_eq!(rects.len(), 1);
assert!(
rects[0].size.width >= 40.0,
"auto track should be ≥ min_content: {}",
rects[0].size.width
);
}
#[test]
fn test_explicit_placement_at_line() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(50.0), TrackSizing::Fixed(50.0)],
cols: vec![
TrackSizing::Fixed(50.0),
TrackSizing::Fixed(50.0),
TrackSizing::Fixed(50.0),
],
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let items = vec![fixed_item(2, 3)];
let rects = compute_grid(&template, &items, Size::new(150.0, 100.0));
assert_eq!(rects.len(), 1);
assert!(
(rects[0].origin.x - 100.0).abs() < 1e-4,
"x = {}",
rects[0].origin.x
);
assert!(
(rects[0].origin.y - 50.0).abs() < 1e-4,
"y = {}",
rects[0].origin.y
);
}
#[test]
fn test_span_2_occupies_two_tracks() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(50.0)],
cols: vec![TrackSizing::Fixed(100.0), TrackSizing::Fixed(100.0)],
areas: None,
row_gap: 0.0,
col_gap: 10.0,
};
let items = vec![GridItem {
placement: GridPlacement::span(1, 1, 1, 2),
min_content_size: Size::ZERO,
max_content_size: Size::ZERO,
}];
let rects = compute_grid(&template, &items, Size::new(210.0, 50.0));
assert_eq!(rects.len(), 1);
assert!(
(rects[0].size.width - 210.0).abs() < 1e-4,
"span width = {}",
rects[0].size.width
);
}
#[test]
fn test_auto_placement_fills_row_major() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(40.0), TrackSizing::Fixed(40.0)],
cols: vec![TrackSizing::Fixed(50.0), TrackSizing::Fixed(50.0)],
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let items = vec![auto_item(), auto_item(), auto_item(), auto_item()];
let rects = compute_grid(&template, &items, Size::new(100.0, 80.0));
assert_eq!(rects.len(), 4);
assert!((rects[0].origin.x - 0.0).abs() < 1e-4);
assert!((rects[1].origin.x - 50.0).abs() < 1e-4);
assert!(
(rects[2].origin.y - 40.0).abs() < 1e-4,
"row2 y = {}",
rects[2].origin.y
);
assert!((rects[2].origin.x - 0.0).abs() < 1e-4);
assert!((rects[3].origin.x - 50.0).abs() < 1e-4);
}
#[test]
fn test_auto_placement_with_hole() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(50.0)],
cols: vec![TrackSizing::Fixed(50.0), TrackSizing::Fixed(50.0)],
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let explicit = GridItem {
placement: GridPlacement::at(1, 2),
min_content_size: Size::ZERO,
max_content_size: Size::ZERO,
};
let items = vec![explicit, auto_item(), auto_item(), auto_item()];
let rects = compute_grid(&template, &items, Size::new(100.0, 200.0));
assert_eq!(rects.len(), 4);
assert!(
(rects[0].origin.x - 50.0).abs() < 1e-4,
"explicit x = {}",
rects[0].origin.x
);
let positions: Vec<(i32, i32)> = rects
.iter()
.map(|r| (r.origin.x as i32, r.origin.y as i32))
.collect();
let unique: std::collections::HashSet<_> = positions.iter().cloned().collect();
assert_eq!(
positions.len(),
unique.len(),
"duplicate positions: {:?}",
positions
);
}
#[test]
fn test_template_areas_named_item() {
let areas = vec![
vec![Some("header".to_string()), Some("header".to_string())],
vec![Some("main".to_string()), Some("sidebar".to_string())],
];
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(60.0), TrackSizing::Fixed(100.0)],
cols: vec![TrackSizing::Fixed(120.0), TrackSizing::Fixed(80.0)],
areas: Some(areas),
row_gap: 0.0,
col_gap: 0.0,
};
let items = vec![GridItem {
placement: GridPlacement {
row: GridSpan {
line: GridLine::Named("header".to_string()),
span: 1,
},
col: GridSpan {
line: GridLine::Named("header".to_string()),
span: 2,
},
},
min_content_size: Size::ZERO,
max_content_size: Size::ZERO,
}];
let rects = compute_grid(&template, &items, Size::new(200.0, 160.0));
assert_eq!(rects.len(), 1);
assert!(
(rects[0].size.width - 200.0).abs() < 1e-4,
"header width = {}",
rects[0].size.width
);
assert!((rects[0].origin.y - 0.0).abs() < 1e-4);
}
#[test]
fn test_row_col_gap_offsets() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(50.0)],
cols: vec![TrackSizing::Fixed(50.0), TrackSizing::Fixed(50.0)],
areas: None,
row_gap: 0.0,
col_gap: 10.0,
};
let items = vec![fixed_item(1, 1), fixed_item(1, 2)];
let rects = compute_grid(&template, &items, Size::new(110.0, 50.0));
assert_eq!(rects.len(), 2);
assert!((rects[0].origin.x - 0.0).abs() < 1e-4);
assert!(
(rects[1].origin.x - 60.0).abs() < 1e-4,
"second col x = {}",
rects[1].origin.x
);
}
#[test]
fn test_over_constrained_shrinks_gracefully() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(50.0)],
cols: vec![TrackSizing::Fixed(1000.0)],
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let items = vec![fixed_item(1, 1)];
let rects = compute_grid(&template, &items, Size::new(10.0, 50.0));
assert_eq!(rects.len(), 1);
assert!(rects[0].size.width >= 0.0);
assert!(rects[0].size.height >= 0.0);
}
#[test]
fn test_empty_grid_empty_rects() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(50.0)],
cols: vec![TrackSizing::Fixed(50.0)],
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let rects = compute_grid(&template, &[], Size::new(100.0, 100.0));
assert!(rects.is_empty());
}
#[test]
fn test_repeat_expands_tracks() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(50.0)],
cols: vec![TrackSizing::Repeat(3, Box::new(TrackSizing::Fixed(50.0)))],
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let items = vec![fixed_item(1, 1), fixed_item(1, 2), fixed_item(1, 3)];
let rects = compute_grid(&template, &items, Size::new(150.0, 50.0));
assert_eq!(rects.len(), 3);
assert!((rects[0].size.width - 50.0).abs() < 1e-4);
assert!((rects[1].size.width - 50.0).abs() < 1e-4);
assert!((rects[2].size.width - 50.0).abs() < 1e-4);
assert!((rects[1].origin.x - 50.0).abs() < 1e-4);
assert!((rects[2].origin.x - 100.0).abs() < 1e-4);
}
#[test]
fn test_spec_12col_grid_with_span() {
let cols: Vec<TrackSizing> = (0..12).map(|_| TrackSizing::Fixed(10.0)).collect();
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(50.0)],
cols,
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let items = vec![GridItem {
placement: GridPlacement::span(1, 5, 1, 3),
min_content_size: Size::ZERO,
max_content_size: Size::ZERO,
}];
let rects = compute_grid(&template, &items, Size::new(120.0, 50.0));
assert_eq!(rects.len(), 1);
assert!(
(rects[0].origin.x - 40.0).abs() < 1e-4,
"x = {}",
rects[0].origin.x
);
assert!(
(rects[0].size.width - 30.0).abs() < 1e-4,
"w = {}",
rects[0].size.width
);
}
#[test]
fn test_spec_auto_placement_dense_after_hole() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(50.0), TrackSizing::Fixed(50.0)],
cols: vec![
TrackSizing::Fixed(30.0),
TrackSizing::Fixed(30.0),
TrackSizing::Fixed(30.0),
],
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let explicit = GridItem {
placement: GridPlacement::at(1, 2),
min_content_size: Size::ZERO,
max_content_size: Size::ZERO,
};
let items = vec![explicit, auto_item()];
let rects = compute_grid(&template, &items, Size::new(90.0, 100.0));
assert_eq!(rects.len(), 2);
assert!(
(rects[1].origin.x - 0.0).abs() < 1e-4,
"auto x = {}",
rects[1].origin.x
);
assert!(
(rects[1].origin.y - 0.0).abs() < 1e-4,
"auto y = {}",
rects[1].origin.y
);
}
#[test]
fn test_spec_mixed_fixed_and_fr() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(50.0)],
cols: vec![TrackSizing::Fixed(50.0), TrackSizing::Fr(1.0)],
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let items = vec![fixed_item(1, 1), fixed_item(1, 2)];
let rects = compute_grid(&template, &items, Size::new(200.0, 50.0));
assert_eq!(rects.len(), 2);
assert!(
(rects[0].size.width - 50.0).abs() < 1e-4,
"fixed = {}",
rects[0].size.width
);
assert!(
(rects[1].size.width - 150.0).abs() < 1e-4,
"fr = {}",
rects[1].size.width
);
}
#[test]
fn test_spec_row_gap_affects_offsets() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(40.0), TrackSizing::Fixed(60.0)],
cols: vec![TrackSizing::Fixed(100.0)],
areas: None,
row_gap: 8.0,
col_gap: 0.0,
};
let items = vec![fixed_item(1, 1), fixed_item(2, 1)];
let rects = compute_grid(&template, &items, Size::new(100.0, 108.0));
assert_eq!(rects.len(), 2);
assert!((rects[0].origin.y - 0.0).abs() < 1e-4);
assert!(
(rects[1].origin.y - 48.0).abs() < 1e-4,
"row2 y = {}",
rects[1].origin.y
);
}
#[test]
fn test_spec_template_areas_two_items() {
let areas = vec![vec![Some("nav".to_string()), Some("content".to_string())]];
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(80.0)],
cols: vec![TrackSizing::Fixed(60.0), TrackSizing::Fixed(140.0)],
areas: Some(areas),
row_gap: 0.0,
col_gap: 0.0,
};
let nav_item = GridItem {
placement: GridPlacement {
row: GridSpan {
line: GridLine::Named("nav".to_string()),
span: 1,
},
col: GridSpan {
line: GridLine::Named("nav".to_string()),
span: 1,
},
},
min_content_size: Size::ZERO,
max_content_size: Size::ZERO,
};
let content_item = GridItem {
placement: GridPlacement {
row: GridSpan {
line: GridLine::Named("content".to_string()),
span: 1,
},
col: GridSpan {
line: GridLine::Named("content".to_string()),
span: 1,
},
},
min_content_size: Size::ZERO,
max_content_size: Size::ZERO,
};
let items = vec![nav_item, content_item];
let rects = compute_grid(&template, &items, Size::new(200.0, 80.0));
assert_eq!(rects.len(), 2);
assert!(
(rects[0].origin.x - 0.0).abs() < 1e-4,
"nav x = {}",
rects[0].origin.x
);
assert!(
(rects[0].size.width - 60.0).abs() < 1e-4,
"nav w = {}",
rects[0].size.width
);
assert!(
(rects[1].origin.x - 60.0).abs() < 1e-4,
"content x = {}",
rects[1].origin.x
);
assert!(
(rects[1].size.width - 140.0).abs() < 1e-4,
"content w = {}",
rects[1].size.width
);
}
#[test]
fn test_spec_implicit_row_creation() {
let template = GridTemplate {
rows: vec![TrackSizing::Fixed(30.0)],
cols: vec![TrackSizing::Fixed(100.0)],
areas: None,
row_gap: 0.0,
col_gap: 0.0,
};
let items = vec![auto_item(), auto_item(), auto_item()];
let rects = compute_grid(&template, &items, Size::new(100.0, 90.0));
assert_eq!(rects.len(), 3);
assert!((rects[0].origin.x - 0.0).abs() < 1e-4);
assert!((rects[1].origin.x - 0.0).abs() < 1e-4);
assert!((rects[2].origin.x - 0.0).abs() < 1e-4);
assert!(rects[1].origin.y >= rects[0].origin.y + rects[0].size.height);
assert!(rects[2].origin.y >= rects[1].origin.y + rects[1].size.height);
}
}