1use std::collections::{HashMap, HashSet};
32
33use crate::geometry::{Rect, Size};
34
35#[derive(Clone, Debug)]
41pub enum TrackSizing {
42 Fixed(f32),
44 Fr(f32),
46 Auto,
48 MinMax(Box<TrackSizing>, Box<TrackSizing>),
50 MinContent,
52 MaxContent,
54 Repeat(usize, Box<TrackSizing>),
56}
57
58#[derive(Clone, Debug, Default)]
62pub struct GridTemplate {
63 pub rows: Vec<TrackSizing>,
65 pub cols: Vec<TrackSizing>,
67 pub areas: Option<Vec<Vec<Option<String>>>>,
70 pub row_gap: f32,
72 pub col_gap: f32,
74}
75
76#[derive(Clone, Debug)]
80pub enum GridLine {
81 Line(i32),
85 Auto,
87 Named(String),
89}
90
91#[derive(Clone, Debug)]
93pub struct GridSpan {
94 pub line: GridLine,
96 pub span: usize,
98}
99
100#[derive(Clone, Debug)]
102pub struct GridPlacement {
103 pub row: GridSpan,
105 pub col: GridSpan,
107}
108
109impl GridPlacement {
110 pub fn auto() -> Self {
112 Self {
113 row: GridSpan {
114 line: GridLine::Auto,
115 span: 1,
116 },
117 col: GridSpan {
118 line: GridLine::Auto,
119 span: 1,
120 },
121 }
122 }
123
124 pub fn at(row: i32, col: i32) -> Self {
128 Self {
129 row: GridSpan {
130 line: GridLine::Line(row),
131 span: 1,
132 },
133 col: GridSpan {
134 line: GridLine::Line(col),
135 span: 1,
136 },
137 }
138 }
139
140 pub fn span(row: i32, col: i32, row_span: usize, col_span: usize) -> Self {
142 Self {
143 row: GridSpan {
144 line: GridLine::Line(row),
145 span: row_span.max(1),
146 },
147 col: GridSpan {
148 line: GridLine::Line(col),
149 span: col_span.max(1),
150 },
151 }
152 }
153}
154
155#[derive(Clone, Debug)]
159pub struct GridItem {
160 pub placement: GridPlacement,
162 pub min_content_size: Size,
164 pub max_content_size: Size,
166}
167
168#[derive(Clone, Debug, PartialEq, Eq)]
173struct ResolvedPlacement {
174 row_start: usize,
176 row_end: usize,
178 col_start: usize,
180 col_end: usize,
182}
183
184#[derive(Clone, Debug)]
188struct TrackRecord {
189 sizing: TrackSizing,
191 base: f32,
193 growth_limit: f32,
195}
196
197fn build_area_map(areas: &[Vec<Option<String>>]) -> HashMap<String, (usize, usize, usize, usize)> {
202 let mut map: HashMap<String, (usize, usize, usize, usize)> = HashMap::new();
203 for (r, row) in areas.iter().enumerate() {
204 for (c, cell) in row.iter().enumerate() {
205 if let Some(name) = cell {
206 let row1 = r + 1;
207 let col1 = c + 1;
208 map.entry(name.clone())
209 .and_modify(|e| {
210 e.0 = e.0.min(row1);
212 e.1 = e.1.min(col1);
213 e.2 = e.2.max(row1 + 1);
214 e.3 = e.3.max(col1 + 1);
215 })
216 .or_insert((row1, col1, row1 + 1, col1 + 1));
217 }
218 }
219 }
220 map
221}
222
223fn expand_tracks(specs: &[TrackSizing]) -> Vec<TrackSizing> {
227 let mut out = Vec::new();
228 for s in specs {
229 expand_one(s, &mut out);
230 }
231 out
232}
233
234fn expand_one(s: &TrackSizing, out: &mut Vec<TrackSizing>) {
235 match s {
236 TrackSizing::Repeat(n, inner) => {
237 for _ in 0..*n {
238 expand_one(inner, out);
239 }
240 }
241 other => out.push(other.clone()),
242 }
243}
244
245fn ensure_track_count(tracks: &mut Vec<TrackRecord>, needed: usize) {
249 while tracks.len() < needed {
250 tracks.push(TrackRecord {
251 sizing: TrackSizing::Auto,
252 base: 0.0,
253 growth_limit: f32::INFINITY,
254 });
255 }
256}
257
258fn make_track_record(sizing: &TrackSizing) -> TrackRecord {
264 let (base, growth_limit) = initial_base_growth(sizing);
265 TrackRecord {
266 sizing: sizing.clone(),
267 base,
268 growth_limit,
269 }
270}
271
272fn initial_base_growth(sizing: &TrackSizing) -> (f32, f32) {
273 match sizing {
274 TrackSizing::Fixed(px) => (*px, *px),
275 TrackSizing::Fr(_) => (0.0, f32::INFINITY),
276 TrackSizing::Auto => (0.0, f32::INFINITY),
277 TrackSizing::MinContent => (0.0, f32::INFINITY),
278 TrackSizing::MaxContent => (0.0, f32::INFINITY),
279 TrackSizing::MinMax(min, max) => {
280 let (b, _) = initial_base_growth(min);
281 let (_, g) = initial_base_growth(max);
282 (b, g)
283 }
284 TrackSizing::Repeat(_, inner) => initial_base_growth(inner),
285 }
286}
287
288fn is_intrinsic_min(sizing: &TrackSizing) -> bool {
293 matches!(
294 sizing,
295 TrackSizing::Auto | TrackSizing::MinContent | TrackSizing::MaxContent
296 )
297}
298
299fn uses_max_content_base(sizing: &TrackSizing) -> bool {
302 matches!(sizing, TrackSizing::MaxContent)
303}
304
305pub fn compute_grid(template: &GridTemplate, items: &[GridItem], available: Size) -> Vec<Rect> {
324 if items.is_empty() {
325 return Vec::new();
326 }
327
328 let explicit_col_specs = expand_tracks(&template.cols);
330 let explicit_row_specs = expand_tracks(&template.rows);
331 let explicit_cols = explicit_col_specs.len();
332 let explicit_rows = explicit_row_specs.len();
333
334 let area_map: HashMap<String, (usize, usize, usize, usize)> = template
336 .areas
337 .as_deref()
338 .map(build_area_map)
339 .unwrap_or_default();
340
341 let mut pre_placements: Vec<Option<ResolvedPlacement>> = vec![None; items.len()];
349
350 for (idx, item) in items.iter().enumerate() {
351 let row_line = resolve_grid_line(&item.placement.row.line, &area_map, true);
352 let col_line = resolve_grid_line(&item.placement.col.line, &area_map, false);
353
354 if let (Some(rs), Some(cs)) = (row_line, col_line) {
355 let re = rs + item.placement.row.span;
356 let ce = cs + item.placement.col.span;
357 pre_placements[idx] = Some(ResolvedPlacement {
358 row_start: rs,
359 row_end: re,
360 col_start: cs,
361 col_end: ce,
362 });
363 } else if let (Some(rs), None) = (row_line, col_line) {
364 let re = rs + item.placement.row.span;
366 pre_placements[idx] = Some(ResolvedPlacement {
367 row_start: rs,
368 row_end: re,
369 col_start: 0, col_end: 0,
371 });
372 }
373 }
374
375 let mut max_row = explicit_rows.max(1);
377 let mut max_col = explicit_cols.max(1);
378 for p in pre_placements.iter().flatten() {
379 if p.col_start != 0 {
380 max_row = max_row.max(p.row_end.saturating_sub(1));
381 max_col = max_col.max(p.col_end.saturating_sub(1));
382 }
383 }
384
385 let mut row_tracks: Vec<TrackRecord> =
387 explicit_row_specs.iter().map(make_track_record).collect();
388 let mut col_tracks: Vec<TrackRecord> =
389 explicit_col_specs.iter().map(make_track_record).collect();
390 ensure_track_count(&mut row_tracks, max_row);
391 ensure_track_count(&mut col_tracks, max_col);
392
393 let mut occupied: HashSet<(usize, usize)> = HashSet::new();
395
396 for p in pre_placements.iter().flatten() {
398 if p.col_start != 0 {
399 mark_occupied(&mut occupied, p);
400 }
401 }
402
403 let mut placements: Vec<ResolvedPlacement> = vec![
405 ResolvedPlacement {
406 row_start: 1,
407 row_end: 2,
408 col_start: 1,
409 col_end: 2
410 };
411 items.len()
412 ];
413
414 for (idx, pre) in pre_placements.iter().enumerate() {
416 if let Some(p) = pre {
417 if p.col_start != 0 {
418 placements[idx] = p.clone();
419 }
420 }
421 }
422
423 let mut cur_row: usize = 1;
425 let mut cur_col: usize = 1;
426
427 let auto_col_count = |col_tracks: &Vec<TrackRecord>| col_tracks.len();
430
431 for (idx, item) in items.iter().enumerate() {
432 let pre = &pre_placements[idx];
433 if let Some(p) = pre {
435 if p.col_start != 0 {
436 continue;
437 }
438 }
439
440 let span_row = item.placement.row.span.max(1);
441 let span_col = item.placement.col.span.max(1);
442
443 if let Some(p) = pre {
445 let fixed_rs = p.row_start;
447 let fixed_re = p.row_end;
448 let mut c = 1usize;
449 loop {
450 if c + span_col - 1 > auto_col_count(&col_tracks) {
451 ensure_track_count(&mut col_tracks, c + span_col - 1);
453 }
454 if slots_free(&occupied, fixed_rs, fixed_re, c, c + span_col) {
455 break;
456 }
457 c += 1;
458 ensure_track_count(&mut col_tracks, c + span_col - 1);
460 }
461 let placement = ResolvedPlacement {
462 row_start: fixed_rs,
463 row_end: fixed_re,
464 col_start: c,
465 col_end: c + span_col,
466 };
467 mark_occupied(&mut occupied, &placement);
468 placements[idx] = placement;
469 continue;
470 }
471
472 loop {
474 let needed_cols = auto_col_count(&col_tracks).max(span_col);
476 ensure_track_count(&mut col_tracks, needed_cols);
477
478 let col_limit = auto_col_count(&col_tracks);
479
480 if cur_col + span_col - 1 > col_limit {
481 cur_row += 1;
483 cur_col = 1;
484 ensure_track_count(&mut row_tracks, cur_row + span_row - 1);
485 }
486
487 let rs = cur_row;
488 let re = cur_row + span_row;
489 let cs = cur_col;
490 let ce = cur_col + span_col;
491
492 ensure_track_count(&mut row_tracks, re.saturating_sub(1).max(1));
494
495 if slots_free(&occupied, rs, re, cs, ce) {
496 let placement = ResolvedPlacement {
497 row_start: rs,
498 row_end: re,
499 col_start: cs,
500 col_end: ce,
501 };
502 mark_occupied(&mut occupied, &placement);
503 placements[idx] = placement;
504 cur_col = cs + span_col;
506 if cur_col > auto_col_count(&col_tracks) {
507 cur_row += 1;
508 cur_col = 1;
509 }
510 break;
511 } else {
512 cur_col += 1;
514 if cur_col > col_limit {
515 cur_row += 1;
516 cur_col = 1;
517 ensure_track_count(&mut row_tracks, cur_row);
518 }
519 }
520 }
521 }
522
523 for (item, placement) in items.iter().zip(placements.iter()) {
527 if placement.row_end - placement.row_start == 1 {
529 let ri = placement.row_start - 1; if ri < row_tracks.len() {
531 let track = &mut row_tracks[ri];
532 if is_intrinsic_min(&track.sizing) {
533 let content = if uses_max_content_base(&track.sizing) {
534 item.max_content_size.height
535 } else {
536 item.min_content_size.height
537 };
538 track.base = track.base.max(content);
539 }
540 }
541 }
542 if placement.col_end - placement.col_start == 1 {
544 let ci = placement.col_start - 1;
545 if ci < col_tracks.len() {
546 let track = &mut col_tracks[ci];
547 if is_intrinsic_min(&track.sizing) {
548 let content = if uses_max_content_base(&track.sizing) {
549 item.max_content_size.width
550 } else {
551 item.min_content_size.width
552 };
553 track.base = track.base.max(content);
554 }
555 }
556 }
557 }
558
559 apply_minmax_clamps(&mut col_tracks);
561 apply_minmax_clamps(&mut row_tracks);
562
563 distribute_fr(&mut col_tracks, available.width, template.col_gap);
565 distribute_fr(&mut row_tracks, available.height, template.row_gap);
566
567 let col_starts = compute_starts(&col_tracks, template.col_gap);
569 let row_starts = compute_starts(&row_tracks, template.row_gap);
570
571 let mut out = Vec::with_capacity(items.len());
573 for placement in &placements {
574 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);
577 let re = (placement.row_end - 1).saturating_sub(1);
578
579 let x = col_starts.get(cs).copied().unwrap_or(0.0);
580 let y = row_starts.get(rs).copied().unwrap_or(0.0);
581
582 let x_end = if ce < col_starts.len() && ce < col_tracks.len() {
583 col_starts[ce] + col_tracks[ce].base
584 } else if cs < col_starts.len() && cs < col_tracks.len() {
585 col_starts[cs] + col_tracks[cs].base
586 } else {
587 x
588 };
589
590 let y_end = if re < row_starts.len() && re < row_tracks.len() {
591 row_starts[re] + row_tracks[re].base
592 } else if rs < row_starts.len() && rs < row_tracks.len() {
593 row_starts[rs] + row_tracks[rs].base
594 } else {
595 y
596 };
597
598 let w = (x_end - x).max(0.0);
599 let h = (y_end - y).max(0.0);
600 out.push(Rect::new(x, y, w, h));
601 }
602
603 out
604}
605
606fn resolve_grid_line(
615 line: &GridLine,
616 area_map: &HashMap<String, (usize, usize, usize, usize)>,
617 is_row: bool,
618) -> Option<usize> {
619 match line {
620 GridLine::Line(n) => {
621 if *n >= 1 {
622 Some(*n as usize)
623 } else if *n < 0 {
624 Some(1)
628 } else {
629 None
630 }
631 }
632 GridLine::Named(name) => area_map
633 .get(name)
634 .map(|&(rs, cs, _re, _ce)| if is_row { rs } else { cs }),
635 GridLine::Auto => None,
636 }
637}
638
639fn mark_occupied(occupied: &mut HashSet<(usize, usize)>, p: &ResolvedPlacement) {
641 for r in p.row_start..p.row_end {
642 for c in p.col_start..p.col_end {
643 occupied.insert((r, c));
644 }
645 }
646}
647
648fn slots_free(
651 occupied: &HashSet<(usize, usize)>,
652 row_start: usize,
653 row_end: usize,
654 col_start: usize,
655 col_end: usize,
656) -> bool {
657 for r in row_start..row_end {
658 for c in col_start..col_end {
659 if occupied.contains(&(r, c)) {
660 return false;
661 }
662 }
663 }
664 true
665}
666
667fn apply_minmax_clamps(tracks: &mut [TrackRecord]) {
670 for track in tracks.iter_mut() {
671 if let TrackSizing::MinMax(min_spec, max_spec) = &track.sizing.clone() {
672 let floor = match min_spec.as_ref() {
673 TrackSizing::Fixed(px) => *px,
674 TrackSizing::MinContent | TrackSizing::Auto => track.base,
675 _ => 0.0,
676 };
677 let ceil = match max_spec.as_ref() {
678 TrackSizing::Fixed(px) => *px,
679 TrackSizing::MaxContent => f32::INFINITY,
680 TrackSizing::Fr(_) => f32::INFINITY, _ => f32::INFINITY,
682 };
683 track.base =
684 track
685 .base
686 .max(floor)
687 .min(if ceil.is_finite() { ceil } else { track.base });
688 track.growth_limit = ceil;
689 }
690 }
691}
692
693fn distribute_fr(tracks: &mut [TrackRecord], available: f32, gap: f32) {
698 let gap_total = if tracks.len() > 1 {
699 gap * (tracks.len() as f32 - 1.0)
700 } else {
701 0.0
702 };
703
704 let fixed_sum: f32 = tracks
708 .iter()
709 .map(|t| match &t.sizing {
710 TrackSizing::Fr(_) => 0.0,
711 TrackSizing::MinMax(_, max) if matches!(max.as_ref(), TrackSizing::Fr(_)) => 0.0,
712 _ => t.base,
713 })
714 .sum();
715
716 let free = (available - gap_total - fixed_sum).max(0.0);
717
718 let fr_indices: Vec<usize> = tracks
720 .iter()
721 .enumerate()
722 .filter_map(|(i, t)| match &t.sizing {
723 TrackSizing::Fr(_) => Some(i),
724 TrackSizing::MinMax(_, max) => {
726 if matches!(max.as_ref(), TrackSizing::Fr(_)) {
727 Some(i)
728 } else {
729 None
730 }
731 }
732 _ => None,
733 })
734 .collect();
735
736 if fr_indices.is_empty() {
737 return;
738 }
739
740 let sum_fr: f32 = fr_indices
741 .iter()
742 .map(|&i| fr_value_of(&tracks[i].sizing))
743 .sum();
744
745 if sum_fr <= 0.0 {
746 return;
747 }
748
749 for i in fr_indices {
750 let frac = fr_value_of(&tracks[i].sizing);
751 let computed = frac * free / sum_fr;
752 let base_floor = tracks[i].base;
753 tracks[i].base = computed.max(base_floor);
754 }
755}
756
757fn fr_value_of(sizing: &TrackSizing) -> f32 {
760 match sizing {
761 TrackSizing::Fr(f) => *f,
762 TrackSizing::MinMax(_, max) => match max.as_ref() {
763 TrackSizing::Fr(f) => *f,
764 _ => 0.0,
765 },
766 _ => 0.0,
767 }
768}
769
770fn compute_starts(tracks: &[TrackRecord], gap: f32) -> Vec<f32> {
772 let mut starts = Vec::with_capacity(tracks.len());
773 let mut offset = 0.0f32;
774 for (i, track) in tracks.iter().enumerate() {
775 if i > 0 {
776 offset += gap;
777 }
778 starts.push(offset);
779 offset += track.base;
780 }
781 starts
782}
783
784#[cfg(test)]
787mod tests {
788 use super::*;
789 use crate::geometry::Size;
790
791 fn fixed_item(r: i32, c: i32) -> GridItem {
792 GridItem {
793 placement: GridPlacement::at(r, c),
794 min_content_size: Size::ZERO,
795 max_content_size: Size::ZERO,
796 }
797 }
798
799 fn auto_item() -> GridItem {
800 GridItem {
801 placement: GridPlacement::auto(),
802 min_content_size: Size::ZERO,
803 max_content_size: Size::ZERO,
804 }
805 }
806
807 #[test]
810 fn test_single_fixed_track_row() {
811 let template = GridTemplate {
812 rows: vec![TrackSizing::Fixed(100.0)],
813 cols: vec![TrackSizing::Fixed(200.0)],
814 areas: None,
815 row_gap: 0.0,
816 col_gap: 0.0,
817 };
818 let items = vec![fixed_item(1, 1)];
819 let rects = compute_grid(&template, &items, Size::new(200.0, 200.0));
820 assert_eq!(rects.len(), 1);
821 assert_eq!(rects[0].origin.y, 0.0);
822 assert_eq!(rects[0].size.height, 100.0);
823 }
824
825 #[test]
826 fn test_single_fixed_track_col() {
827 let template = GridTemplate {
828 rows: vec![TrackSizing::Fixed(200.0)],
829 cols: vec![TrackSizing::Fixed(100.0)],
830 areas: None,
831 row_gap: 0.0,
832 col_gap: 0.0,
833 };
834 let items = vec![fixed_item(1, 1)];
835 let rects = compute_grid(&template, &items, Size::new(200.0, 200.0));
836 assert_eq!(rects.len(), 1);
837 assert_eq!(rects[0].origin.x, 0.0);
838 assert_eq!(rects[0].size.width, 100.0);
839 }
840
841 #[test]
844 fn test_three_equal_fr_tracks() {
845 let template = GridTemplate {
846 rows: vec![TrackSizing::Fixed(50.0)],
847 cols: vec![
848 TrackSizing::Fr(1.0),
849 TrackSizing::Fr(1.0),
850 TrackSizing::Fr(1.0),
851 ],
852 areas: None,
853 row_gap: 0.0,
854 col_gap: 0.0,
855 };
856 let items = vec![fixed_item(1, 1), fixed_item(1, 2), fixed_item(1, 3)];
857 let rects = compute_grid(&template, &items, Size::new(300.0, 50.0));
858 assert_eq!(rects.len(), 3);
859 for r in &rects {
860 assert!(
861 (r.size.width - 100.0).abs() < 1e-4,
862 "expected 100px, got {}",
863 r.size.width
864 );
865 }
866 }
867
868 #[test]
869 fn test_fr_proportional_split_unequal() {
870 let template = GridTemplate {
871 rows: vec![TrackSizing::Fixed(50.0)],
872 cols: vec![TrackSizing::Fr(1.0), TrackSizing::Fr(2.0)],
873 areas: None,
874 row_gap: 0.0,
875 col_gap: 0.0,
876 };
877 let items = vec![fixed_item(1, 1), fixed_item(1, 2)];
878 let rects = compute_grid(&template, &items, Size::new(300.0, 50.0));
879 assert_eq!(rects.len(), 2);
880 assert!(
881 (rects[0].size.width - 100.0).abs() < 1e-4,
882 "col1 = {}",
883 rects[0].size.width
884 );
885 assert!(
886 (rects[1].size.width - 200.0).abs() < 1e-4,
887 "col2 = {}",
888 rects[1].size.width
889 );
890 }
891
892 #[test]
895 fn test_minmax_clamps() {
896 let template = GridTemplate {
898 rows: vec![TrackSizing::Fixed(50.0)],
899 cols: vec![TrackSizing::MinMax(
900 Box::new(TrackSizing::Fixed(100.0)),
901 Box::new(TrackSizing::Fr(1.0)),
902 )],
903 areas: None,
904 row_gap: 0.0,
905 col_gap: 0.0,
906 };
907 let items = vec![fixed_item(1, 1)];
908 let rects = compute_grid(&template, &items, Size::new(50.0, 50.0));
909 assert_eq!(rects.len(), 1);
910 assert!(
911 rects[0].size.width >= 100.0,
912 "minmax floor violated: {}",
913 rects[0].size.width
914 );
915 }
916
917 #[test]
918 fn test_nested_minmax_fr() {
919 let template = GridTemplate {
922 rows: vec![TrackSizing::Fixed(50.0)],
923 cols: vec![TrackSizing::MinMax(
924 Box::new(TrackSizing::Fixed(50.0)),
925 Box::new(TrackSizing::Fr(1.0)),
926 )],
927 areas: None,
928 row_gap: 0.0,
929 col_gap: 0.0,
930 };
931 let items = vec![fixed_item(1, 1)];
932 let rects = compute_grid(&template, &items, Size::new(200.0, 50.0));
933 assert_eq!(rects.len(), 1);
934 assert!(
936 (rects[0].size.width - 200.0).abs() < 1e-4,
937 "minmax(50,1fr) with 200px available: expected 200, got {}",
938 rects[0].size.width
939 );
940 }
941
942 #[test]
945 fn test_auto_track_sizes_to_content() {
946 let template = GridTemplate {
947 rows: vec![TrackSizing::Fixed(50.0)],
948 cols: vec![TrackSizing::Auto],
949 areas: None,
950 row_gap: 0.0,
951 col_gap: 0.0,
952 };
953 let items = vec![GridItem {
954 placement: GridPlacement::at(1, 1),
955 min_content_size: Size::new(40.0, 50.0),
956 max_content_size: Size::new(80.0, 50.0),
957 }];
958 let rects = compute_grid(&template, &items, Size::new(200.0, 50.0));
959 assert_eq!(rects.len(), 1);
960 assert!(
961 rects[0].size.width >= 40.0,
962 "auto track should be ≥ min_content: {}",
963 rects[0].size.width
964 );
965 }
966
967 #[test]
970 fn test_explicit_placement_at_line() {
971 let template = GridTemplate {
973 rows: vec![TrackSizing::Fixed(50.0), TrackSizing::Fixed(50.0)],
974 cols: vec![
975 TrackSizing::Fixed(50.0),
976 TrackSizing::Fixed(50.0),
977 TrackSizing::Fixed(50.0),
978 ],
979 areas: None,
980 row_gap: 0.0,
981 col_gap: 0.0,
982 };
983 let items = vec![fixed_item(2, 3)];
984 let rects = compute_grid(&template, &items, Size::new(150.0, 100.0));
985 assert_eq!(rects.len(), 1);
986 assert!(
987 (rects[0].origin.x - 100.0).abs() < 1e-4,
988 "x = {}",
989 rects[0].origin.x
990 );
991 assert!(
992 (rects[0].origin.y - 50.0).abs() < 1e-4,
993 "y = {}",
994 rects[0].origin.y
995 );
996 }
997
998 #[test]
1001 fn test_span_2_occupies_two_tracks() {
1002 let template = GridTemplate {
1004 rows: vec![TrackSizing::Fixed(50.0)],
1005 cols: vec![TrackSizing::Fixed(100.0), TrackSizing::Fixed(100.0)],
1006 areas: None,
1007 row_gap: 0.0,
1008 col_gap: 10.0,
1009 };
1010 let items = vec![GridItem {
1011 placement: GridPlacement::span(1, 1, 1, 2),
1012 min_content_size: Size::ZERO,
1013 max_content_size: Size::ZERO,
1014 }];
1015 let rects = compute_grid(&template, &items, Size::new(210.0, 50.0));
1016 assert_eq!(rects.len(), 1);
1017 assert!(
1019 (rects[0].size.width - 210.0).abs() < 1e-4,
1020 "span width = {}",
1021 rects[0].size.width
1022 );
1023 }
1024
1025 #[test]
1028 fn test_auto_placement_fills_row_major() {
1029 let template = GridTemplate {
1031 rows: vec![TrackSizing::Fixed(40.0), TrackSizing::Fixed(40.0)],
1032 cols: vec![TrackSizing::Fixed(50.0), TrackSizing::Fixed(50.0)],
1033 areas: None,
1034 row_gap: 0.0,
1035 col_gap: 0.0,
1036 };
1037 let items = vec![auto_item(), auto_item(), auto_item(), auto_item()];
1038 let rects = compute_grid(&template, &items, Size::new(100.0, 80.0));
1039 assert_eq!(rects.len(), 4);
1040 assert!((rects[0].origin.x - 0.0).abs() < 1e-4);
1042 assert!((rects[1].origin.x - 50.0).abs() < 1e-4);
1043 assert!(
1044 (rects[2].origin.y - 40.0).abs() < 1e-4,
1045 "row2 y = {}",
1046 rects[2].origin.y
1047 );
1048 assert!((rects[2].origin.x - 0.0).abs() < 1e-4);
1049 assert!((rects[3].origin.x - 50.0).abs() < 1e-4);
1050 }
1051
1052 #[test]
1053 fn test_auto_placement_with_hole() {
1054 let template = GridTemplate {
1056 rows: vec![TrackSizing::Fixed(50.0)],
1057 cols: vec![TrackSizing::Fixed(50.0), TrackSizing::Fixed(50.0)],
1058 areas: None,
1059 row_gap: 0.0,
1060 col_gap: 0.0,
1061 };
1062 let explicit = GridItem {
1063 placement: GridPlacement::at(1, 2),
1064 min_content_size: Size::ZERO,
1065 max_content_size: Size::ZERO,
1066 };
1067 let items = vec![explicit, auto_item(), auto_item(), auto_item()];
1068 let rects = compute_grid(&template, &items, Size::new(100.0, 200.0));
1069 assert_eq!(rects.len(), 4);
1070 assert!(
1072 (rects[0].origin.x - 50.0).abs() < 1e-4,
1073 "explicit x = {}",
1074 rects[0].origin.x
1075 );
1076 let positions: Vec<(i32, i32)> = rects
1078 .iter()
1079 .map(|r| (r.origin.x as i32, r.origin.y as i32))
1080 .collect();
1081 let unique: std::collections::HashSet<_> = positions.iter().cloned().collect();
1082 assert_eq!(
1083 positions.len(),
1084 unique.len(),
1085 "duplicate positions: {:?}",
1086 positions
1087 );
1088 }
1089
1090 #[test]
1093 fn test_template_areas_named_item() {
1094 let areas = vec![
1096 vec![Some("header".to_string()), Some("header".to_string())],
1097 vec![Some("main".to_string()), Some("sidebar".to_string())],
1098 ];
1099 let template = GridTemplate {
1100 rows: vec![TrackSizing::Fixed(60.0), TrackSizing::Fixed(100.0)],
1101 cols: vec![TrackSizing::Fixed(120.0), TrackSizing::Fixed(80.0)],
1102 areas: Some(areas),
1103 row_gap: 0.0,
1104 col_gap: 0.0,
1105 };
1106 let items = vec![GridItem {
1107 placement: GridPlacement {
1108 row: GridSpan {
1109 line: GridLine::Named("header".to_string()),
1110 span: 1,
1111 },
1112 col: GridSpan {
1113 line: GridLine::Named("header".to_string()),
1114 span: 2,
1115 },
1116 },
1117 min_content_size: Size::ZERO,
1118 max_content_size: Size::ZERO,
1119 }];
1120 let rects = compute_grid(&template, &items, Size::new(200.0, 160.0));
1121 assert_eq!(rects.len(), 1);
1122 assert!(
1124 (rects[0].size.width - 200.0).abs() < 1e-4,
1125 "header width = {}",
1126 rects[0].size.width
1127 );
1128 assert!((rects[0].origin.y - 0.0).abs() < 1e-4);
1129 }
1130
1131 #[test]
1134 fn test_row_col_gap_offsets() {
1135 let template = GridTemplate {
1137 rows: vec![TrackSizing::Fixed(50.0)],
1138 cols: vec![TrackSizing::Fixed(50.0), TrackSizing::Fixed(50.0)],
1139 areas: None,
1140 row_gap: 0.0,
1141 col_gap: 10.0,
1142 };
1143 let items = vec![fixed_item(1, 1), fixed_item(1, 2)];
1144 let rects = compute_grid(&template, &items, Size::new(110.0, 50.0));
1145 assert_eq!(rects.len(), 2);
1146 assert!((rects[0].origin.x - 0.0).abs() < 1e-4);
1147 assert!(
1149 (rects[1].origin.x - 60.0).abs() < 1e-4,
1150 "second col x = {}",
1151 rects[1].origin.x
1152 );
1153 }
1154
1155 #[test]
1158 fn test_over_constrained_shrinks_gracefully() {
1159 let template = GridTemplate {
1161 rows: vec![TrackSizing::Fixed(50.0)],
1162 cols: vec![TrackSizing::Fixed(1000.0)],
1163 areas: None,
1164 row_gap: 0.0,
1165 col_gap: 0.0,
1166 };
1167 let items = vec![fixed_item(1, 1)];
1168 let rects = compute_grid(&template, &items, Size::new(10.0, 50.0));
1169 assert_eq!(rects.len(), 1);
1170 assert!(rects[0].size.width >= 0.0);
1171 assert!(rects[0].size.height >= 0.0);
1172 }
1173
1174 #[test]
1175 fn test_empty_grid_empty_rects() {
1176 let template = GridTemplate {
1177 rows: vec![TrackSizing::Fixed(50.0)],
1178 cols: vec![TrackSizing::Fixed(50.0)],
1179 areas: None,
1180 row_gap: 0.0,
1181 col_gap: 0.0,
1182 };
1183 let rects = compute_grid(&template, &[], Size::new(100.0, 100.0));
1184 assert!(rects.is_empty());
1185 }
1186
1187 #[test]
1190 fn test_repeat_expands_tracks() {
1191 let template = GridTemplate {
1193 rows: vec![TrackSizing::Fixed(50.0)],
1194 cols: vec![TrackSizing::Repeat(3, Box::new(TrackSizing::Fixed(50.0)))],
1195 areas: None,
1196 row_gap: 0.0,
1197 col_gap: 0.0,
1198 };
1199 let items = vec![fixed_item(1, 1), fixed_item(1, 2), fixed_item(1, 3)];
1200 let rects = compute_grid(&template, &items, Size::new(150.0, 50.0));
1201 assert_eq!(rects.len(), 3);
1202 assert!((rects[0].size.width - 50.0).abs() < 1e-4);
1203 assert!((rects[1].size.width - 50.0).abs() < 1e-4);
1204 assert!((rects[2].size.width - 50.0).abs() < 1e-4);
1205 assert!((rects[1].origin.x - 50.0).abs() < 1e-4);
1206 assert!((rects[2].origin.x - 100.0).abs() < 1e-4);
1207 }
1208
1209 #[test]
1213 fn test_spec_12col_grid_with_span() {
1214 let cols: Vec<TrackSizing> = (0..12).map(|_| TrackSizing::Fixed(10.0)).collect();
1215 let template = GridTemplate {
1216 rows: vec![TrackSizing::Fixed(50.0)],
1217 cols,
1218 areas: None,
1219 row_gap: 0.0,
1220 col_gap: 0.0,
1221 };
1222 let items = vec![GridItem {
1223 placement: GridPlacement::span(1, 5, 1, 3),
1224 min_content_size: Size::ZERO,
1225 max_content_size: Size::ZERO,
1226 }];
1227 let rects = compute_grid(&template, &items, Size::new(120.0, 50.0));
1228 assert_eq!(rects.len(), 1);
1229 assert!(
1231 (rects[0].origin.x - 40.0).abs() < 1e-4,
1232 "x = {}",
1233 rects[0].origin.x
1234 );
1235 assert!(
1236 (rects[0].size.width - 30.0).abs() < 1e-4,
1237 "w = {}",
1238 rects[0].size.width
1239 );
1240 }
1241
1242 #[test]
1244 fn test_spec_auto_placement_dense_after_hole() {
1245 let template = GridTemplate {
1247 rows: vec![TrackSizing::Fixed(50.0), TrackSizing::Fixed(50.0)],
1248 cols: vec![
1249 TrackSizing::Fixed(30.0),
1250 TrackSizing::Fixed(30.0),
1251 TrackSizing::Fixed(30.0),
1252 ],
1253 areas: None,
1254 row_gap: 0.0,
1255 col_gap: 0.0,
1256 };
1257 let explicit = GridItem {
1258 placement: GridPlacement::at(1, 2),
1259 min_content_size: Size::ZERO,
1260 max_content_size: Size::ZERO,
1261 };
1262 let items = vec![explicit, auto_item()];
1263 let rects = compute_grid(&template, &items, Size::new(90.0, 100.0));
1264 assert_eq!(rects.len(), 2);
1265 assert!(
1267 (rects[1].origin.x - 0.0).abs() < 1e-4,
1268 "auto x = {}",
1269 rects[1].origin.x
1270 );
1271 assert!(
1272 (rects[1].origin.y - 0.0).abs() < 1e-4,
1273 "auto y = {}",
1274 rects[1].origin.y
1275 );
1276 }
1277
1278 #[test]
1280 fn test_spec_mixed_fixed_and_fr() {
1281 let template = GridTemplate {
1283 rows: vec![TrackSizing::Fixed(50.0)],
1284 cols: vec![TrackSizing::Fixed(50.0), TrackSizing::Fr(1.0)],
1285 areas: None,
1286 row_gap: 0.0,
1287 col_gap: 0.0,
1288 };
1289 let items = vec![fixed_item(1, 1), fixed_item(1, 2)];
1290 let rects = compute_grid(&template, &items, Size::new(200.0, 50.0));
1291 assert_eq!(rects.len(), 2);
1292 assert!(
1293 (rects[0].size.width - 50.0).abs() < 1e-4,
1294 "fixed = {}",
1295 rects[0].size.width
1296 );
1297 assert!(
1298 (rects[1].size.width - 150.0).abs() < 1e-4,
1299 "fr = {}",
1300 rects[1].size.width
1301 );
1302 }
1303
1304 #[test]
1306 fn test_spec_row_gap_affects_offsets() {
1307 let template = GridTemplate {
1308 rows: vec![TrackSizing::Fixed(40.0), TrackSizing::Fixed(60.0)],
1309 cols: vec![TrackSizing::Fixed(100.0)],
1310 areas: None,
1311 row_gap: 8.0,
1312 col_gap: 0.0,
1313 };
1314 let items = vec![fixed_item(1, 1), fixed_item(2, 1)];
1315 let rects = compute_grid(&template, &items, Size::new(100.0, 108.0));
1316 assert_eq!(rects.len(), 2);
1317 assert!((rects[0].origin.y - 0.0).abs() < 1e-4);
1318 assert!(
1320 (rects[1].origin.y - 48.0).abs() < 1e-4,
1321 "row2 y = {}",
1322 rects[1].origin.y
1323 );
1324 }
1325
1326 #[test]
1328 fn test_spec_template_areas_two_items() {
1329 let areas = vec![vec![Some("nav".to_string()), Some("content".to_string())]];
1330 let template = GridTemplate {
1331 rows: vec![TrackSizing::Fixed(80.0)],
1332 cols: vec![TrackSizing::Fixed(60.0), TrackSizing::Fixed(140.0)],
1333 areas: Some(areas),
1334 row_gap: 0.0,
1335 col_gap: 0.0,
1336 };
1337 let nav_item = GridItem {
1338 placement: GridPlacement {
1339 row: GridSpan {
1340 line: GridLine::Named("nav".to_string()),
1341 span: 1,
1342 },
1343 col: GridSpan {
1344 line: GridLine::Named("nav".to_string()),
1345 span: 1,
1346 },
1347 },
1348 min_content_size: Size::ZERO,
1349 max_content_size: Size::ZERO,
1350 };
1351 let content_item = GridItem {
1352 placement: GridPlacement {
1353 row: GridSpan {
1354 line: GridLine::Named("content".to_string()),
1355 span: 1,
1356 },
1357 col: GridSpan {
1358 line: GridLine::Named("content".to_string()),
1359 span: 1,
1360 },
1361 },
1362 min_content_size: Size::ZERO,
1363 max_content_size: Size::ZERO,
1364 };
1365 let items = vec![nav_item, content_item];
1366 let rects = compute_grid(&template, &items, Size::new(200.0, 80.0));
1367 assert_eq!(rects.len(), 2);
1368 assert!(
1370 (rects[0].origin.x - 0.0).abs() < 1e-4,
1371 "nav x = {}",
1372 rects[0].origin.x
1373 );
1374 assert!(
1375 (rects[0].size.width - 60.0).abs() < 1e-4,
1376 "nav w = {}",
1377 rects[0].size.width
1378 );
1379 assert!(
1381 (rects[1].origin.x - 60.0).abs() < 1e-4,
1382 "content x = {}",
1383 rects[1].origin.x
1384 );
1385 assert!(
1386 (rects[1].size.width - 140.0).abs() < 1e-4,
1387 "content w = {}",
1388 rects[1].size.width
1389 );
1390 }
1391
1392 #[test]
1394 fn test_spec_implicit_row_creation() {
1395 let template = GridTemplate {
1397 rows: vec![TrackSizing::Fixed(30.0)],
1398 cols: vec![TrackSizing::Fixed(100.0)],
1399 areas: None,
1400 row_gap: 0.0,
1401 col_gap: 0.0,
1402 };
1403 let items = vec![auto_item(), auto_item(), auto_item()];
1404 let rects = compute_grid(&template, &items, Size::new(100.0, 90.0));
1405 assert_eq!(rects.len(), 3);
1406 assert!((rects[0].origin.x - 0.0).abs() < 1e-4);
1408 assert!((rects[1].origin.x - 0.0).abs() < 1e-4);
1409 assert!((rects[2].origin.x - 0.0).abs() < 1e-4);
1410 assert!(rects[1].origin.y >= rects[0].origin.y + rects[0].size.height);
1411 assert!(rects[2].origin.y >= rects[1].origin.y + rects[1].size.height);
1412 }
1413}