1#![forbid(unsafe_code)]
2
3use crate::Constraint;
41use ftui_core::geometry::Rect;
42use std::collections::HashMap;
43
44#[derive(Debug, Clone, Default)]
46pub struct Grid {
47 row_constraints: Vec<Constraint>,
49 col_constraints: Vec<Constraint>,
51 row_gap: u16,
53 col_gap: u16,
55 named_areas: HashMap<String, GridArea>,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct GridArea {
62 pub row: usize,
64 pub col: usize,
66 pub rowspan: usize,
68 pub colspan: usize,
70}
71
72impl GridArea {
73 pub fn cell(row: usize, col: usize) -> Self {
75 Self {
76 row,
77 col,
78 rowspan: 1,
79 colspan: 1,
80 }
81 }
82
83 pub fn span(row: usize, col: usize, rowspan: usize, colspan: usize) -> Self {
85 Self {
86 row,
87 col,
88 rowspan: rowspan.max(1),
89 colspan: colspan.max(1),
90 }
91 }
92}
93
94#[derive(Debug, Clone)]
96pub struct GridLayout {
97 row_heights: Vec<u16>,
99 col_widths: Vec<u16>,
101 row_positions: Vec<u16>,
103 col_positions: Vec<u16>,
105 named_areas: HashMap<String, GridArea>,
107 row_gap: u16,
109 col_gap: u16,
111}
112
113impl Grid {
114 pub fn new() -> Self {
116 Self::default()
117 }
118
119 pub fn rows(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
121 self.row_constraints = constraints.into_iter().collect();
122 self
123 }
124
125 pub fn columns(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
127 self.col_constraints = constraints.into_iter().collect();
128 self
129 }
130
131 pub fn row_gap(mut self, gap: u16) -> Self {
133 self.row_gap = gap;
134 self
135 }
136
137 pub fn col_gap(mut self, gap: u16) -> Self {
139 self.col_gap = gap;
140 self
141 }
142
143 pub fn gap(self, gap: u16) -> Self {
145 self.row_gap(gap).col_gap(gap)
146 }
147
148 pub fn area(mut self, name: impl Into<String>, area: GridArea) -> Self {
160 self.named_areas.insert(name.into(), area);
161 self
162 }
163
164 pub fn num_rows(&self) -> usize {
166 self.row_constraints.len()
167 }
168
169 pub fn num_cols(&self) -> usize {
171 self.col_constraints.len()
172 }
173
174 pub fn split(&self, area: Rect) -> GridLayout {
176 let num_rows = self.row_constraints.len();
177 let num_cols = self.col_constraints.len();
178
179 if num_rows == 0 || num_cols == 0 || area.is_empty() {
180 return GridLayout {
181 row_heights: vec![0; num_rows],
182 col_widths: vec![0; num_cols],
183 row_positions: vec![area.y; num_rows],
184 col_positions: vec![area.x; num_cols],
185 named_areas: self.named_areas.clone(),
186 row_gap: self.row_gap,
187 col_gap: self.col_gap,
188 };
189 }
190
191 let total_row_gap = if num_rows > 1 {
193 let gaps = (num_rows - 1) as u64;
194 (gaps * self.row_gap as u64).min(u16::MAX as u64) as u16
195 } else {
196 0
197 };
198 let total_col_gap = if num_cols > 1 {
199 let gaps = (num_cols - 1) as u64;
200 (gaps * self.col_gap as u64).min(u16::MAX as u64) as u16
201 } else {
202 0
203 };
204
205 let available_height = area.height.saturating_sub(total_row_gap);
207 let available_width = area.width.saturating_sub(total_col_gap);
208
209 let row_heights = crate::solve_constraints(&self.row_constraints, available_height);
211 let col_widths = crate::solve_constraints(&self.col_constraints, available_width);
212
213 let row_positions = self.calculate_positions(&row_heights, area.y, self.row_gap);
215 let col_positions = self.calculate_positions(&col_widths, area.x, self.col_gap);
216
217 GridLayout {
218 row_heights,
219 col_widths,
220 row_positions,
221 col_positions,
222 named_areas: self.named_areas.clone(),
223 row_gap: self.row_gap,
224 col_gap: self.col_gap,
225 }
226 }
227
228 fn calculate_positions(&self, sizes: &[u16], start: u16, gap: u16) -> Vec<u16> {
230 let mut positions = Vec::with_capacity(sizes.len());
231 let mut pos = start;
232
233 for (i, &size) in sizes.iter().enumerate() {
234 positions.push(pos);
235 pos = pos.saturating_add(size);
236 if i < sizes.len() - 1 {
237 pos = pos.saturating_add(gap);
238 }
239 }
240
241 positions
242 }
243}
244
245impl GridLayout {
246 pub fn cell(&self, row: usize, col: usize) -> Rect {
250 self.span(row, col, 1, 1)
251 }
252
253 pub fn span(&self, row: usize, col: usize, rowspan: usize, colspan: usize) -> Rect {
257 let rowspan = rowspan.max(1);
258 let colspan = colspan.max(1);
259
260 if row >= self.row_heights.len() || col >= self.col_widths.len() {
262 return Rect::default();
263 }
264
265 let end_row = (row + rowspan).min(self.row_heights.len());
266 let end_col = (col + colspan).min(self.col_widths.len());
267
268 let x = self.col_positions[col];
270 let y = self.row_positions[row];
271
272 let mut width: u16 = 0;
274 for c in col..end_col {
275 width = width.saturating_add(self.col_widths[c]);
276 }
277 if end_col > col + 1 {
279 let gap_count = (end_col - col - 1) as u16;
280 width = width.saturating_add(self.col_gap.saturating_mul(gap_count));
281 }
282
283 let mut height: u16 = 0;
285 for r in row..end_row {
286 height = height.saturating_add(self.row_heights[r]);
287 }
288 if end_row > row + 1 {
289 let gap_count = (end_row - row - 1) as u16;
290 height = height.saturating_add(self.row_gap.saturating_mul(gap_count));
291 }
292
293 Rect::new(x, y, width, height)
294 }
295
296 pub fn area(&self, name: &str) -> Option<Rect> {
300 self.named_areas
301 .get(name)
302 .map(|a| self.span(a.row, a.col, a.rowspan, a.colspan))
303 }
304
305 pub fn num_rows(&self) -> usize {
307 self.row_heights.len()
308 }
309
310 pub fn num_cols(&self) -> usize {
312 self.col_widths.len()
313 }
314
315 pub fn row_height(&self, row: usize) -> u16 {
317 self.row_heights.get(row).copied().unwrap_or(0)
318 }
319
320 pub fn col_width(&self, col: usize) -> u16 {
322 self.col_widths.get(col).copied().unwrap_or(0)
323 }
324
325 pub fn iter_cells(&self) -> impl Iterator<Item = (usize, usize, Rect)> + '_ {
327 let num_rows = self.num_rows();
328 let num_cols = self.num_cols();
329 (0..num_rows)
330 .flat_map(move |row| (0..num_cols).map(move |col| (row, col, self.cell(row, col))))
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn empty_grid() {
340 let grid = Grid::new();
341 let layout = grid.split(Rect::new(0, 0, 100, 50));
342 assert_eq!(layout.num_rows(), 0);
343 assert_eq!(layout.num_cols(), 0);
344 }
345
346 #[test]
347 fn simple_2x2_grid() {
348 let grid = Grid::new()
349 .rows([Constraint::Fixed(10), Constraint::Fixed(10)])
350 .columns([Constraint::Fixed(20), Constraint::Fixed(20)]);
351
352 let layout = grid.split(Rect::new(0, 0, 100, 50));
353
354 assert_eq!(layout.num_rows(), 2);
355 assert_eq!(layout.num_cols(), 2);
356
357 assert_eq!(layout.cell(0, 0), Rect::new(0, 0, 20, 10));
359 assert_eq!(layout.cell(0, 1), Rect::new(20, 0, 20, 10));
360 assert_eq!(layout.cell(1, 0), Rect::new(0, 10, 20, 10));
361 assert_eq!(layout.cell(1, 1), Rect::new(20, 10, 20, 10));
362 }
363
364 #[test]
365 fn grid_with_gaps() {
366 let grid = Grid::new()
367 .rows([Constraint::Fixed(10), Constraint::Fixed(10)])
368 .columns([Constraint::Fixed(20), Constraint::Fixed(20)])
369 .row_gap(2)
370 .col_gap(5);
371
372 let layout = grid.split(Rect::new(0, 0, 100, 50));
373
374 assert_eq!(layout.cell(0, 0), Rect::new(0, 0, 20, 10));
376 assert_eq!(layout.cell(0, 1), Rect::new(25, 0, 20, 10));
378 assert_eq!(layout.cell(1, 0), Rect::new(0, 12, 20, 10));
380 assert_eq!(layout.cell(1, 1), Rect::new(25, 12, 20, 10));
382 }
383
384 #[test]
385 fn percentage_constraints() {
386 let grid = Grid::new()
387 .rows([Constraint::Percentage(50.0), Constraint::Percentage(50.0)])
388 .columns([Constraint::Percentage(30.0), Constraint::Percentage(70.0)]);
389
390 let layout = grid.split(Rect::new(0, 0, 100, 50));
391
392 assert_eq!(layout.row_height(0), 25);
393 assert_eq!(layout.row_height(1), 25);
394 assert_eq!(layout.col_width(0), 30);
395 assert_eq!(layout.col_width(1), 70);
396 }
397
398 #[test]
399 fn min_constraints_fill_space() {
400 let grid = Grid::new()
401 .rows([Constraint::Fixed(10), Constraint::Min(5)])
402 .columns([Constraint::Fixed(20), Constraint::Min(10)]);
403
404 let layout = grid.split(Rect::new(0, 0, 100, 50));
405
406 assert_eq!(layout.row_height(0), 10);
408 assert_eq!(layout.row_height(1), 40); assert_eq!(layout.col_width(0), 20);
410 assert_eq!(layout.col_width(1), 80); }
412
413 #[test]
414 fn grid_span_clamps_out_of_bounds() {
415 let grid = Grid::new()
416 .rows([Constraint::Fixed(4), Constraint::Fixed(6)])
417 .columns([Constraint::Fixed(8), Constraint::Fixed(12)]);
418
419 let layout = grid.split(Rect::new(0, 0, 40, 20));
420 let span = layout.span(1, 1, 5, 5);
421
422 assert_eq!(span, Rect::new(8, 4, 12, 6));
423 }
424
425 #[test]
426 fn grid_span_includes_gaps_between_tracks() {
427 let grid = Grid::new()
428 .rows([Constraint::Fixed(3)])
429 .columns([
430 Constraint::Fixed(2),
431 Constraint::Fixed(2),
432 Constraint::Fixed(2),
433 ])
434 .col_gap(1);
435
436 let layout = grid.split(Rect::new(0, 0, 20, 10));
437 let span = layout.span(0, 0, 1, 3);
438
439 assert_eq!(span.width, 8); assert_eq!(span.height, 3);
441 }
442
443 #[test]
444 fn grid_tiny_area_with_gaps_produces_zero_tracks() {
445 let grid = Grid::new()
446 .rows([Constraint::Fixed(1), Constraint::Fixed(1)])
447 .columns([Constraint::Fixed(1), Constraint::Fixed(1)])
448 .row_gap(2)
449 .col_gap(2);
450
451 let layout = grid.split(Rect::new(0, 0, 1, 1));
452 assert_eq!(layout.row_height(0), 0);
453 assert_eq!(layout.row_height(1), 0);
454 assert_eq!(layout.col_width(0), 0);
455 assert_eq!(layout.col_width(1), 0);
456 }
457
458 #[test]
459 fn cell_spanning() {
460 let grid = Grid::new()
461 .rows([
462 Constraint::Fixed(10),
463 Constraint::Fixed(10),
464 Constraint::Fixed(10),
465 ])
466 .columns([
467 Constraint::Fixed(20),
468 Constraint::Fixed(20),
469 Constraint::Fixed(20),
470 ]);
471
472 let layout = grid.split(Rect::new(0, 0, 100, 50));
473
474 assert_eq!(layout.span(0, 0, 1, 1), Rect::new(0, 0, 20, 10));
476
477 assert_eq!(layout.span(0, 0, 1, 2), Rect::new(0, 0, 40, 10));
479
480 assert_eq!(layout.span(0, 0, 2, 1), Rect::new(0, 0, 20, 20));
482
483 assert_eq!(layout.span(0, 0, 2, 2), Rect::new(0, 0, 40, 20));
485 }
486
487 #[test]
488 fn cell_spanning_with_gaps() {
489 let grid = Grid::new()
490 .rows([Constraint::Fixed(10), Constraint::Fixed(10)])
491 .columns([Constraint::Fixed(20), Constraint::Fixed(20)])
492 .row_gap(2)
493 .col_gap(5);
494
495 let layout = grid.split(Rect::new(0, 0, 100, 50));
496
497 let full = layout.span(0, 0, 2, 2);
499 assert_eq!(full.width, 45);
502 assert_eq!(full.height, 22);
503 }
504
505 #[test]
506 fn named_areas() {
507 let grid = Grid::new()
508 .rows([
509 Constraint::Fixed(5),
510 Constraint::Min(10),
511 Constraint::Fixed(3),
512 ])
513 .columns([Constraint::Fixed(20), Constraint::Min(30)])
514 .area("header", GridArea::span(0, 0, 1, 2))
515 .area("sidebar", GridArea::span(1, 0, 2, 1))
516 .area("content", GridArea::cell(1, 1))
517 .area("footer", GridArea::cell(2, 1));
518
519 let layout = grid.split(Rect::new(0, 0, 80, 30));
520
521 let header = layout.area("header").unwrap();
523 assert_eq!(header.y, 0);
524 assert_eq!(header.height, 5);
525
526 let sidebar = layout.area("sidebar").unwrap();
528 assert_eq!(sidebar.x, 0);
529 assert_eq!(sidebar.width, 20);
530
531 let content = layout.area("content").unwrap();
533 assert_eq!(content.x, 20);
534 assert_eq!(content.y, 5);
535
536 let footer = layout.area("footer").unwrap();
538 assert_eq!(
539 footer.y,
540 layout.area("content").unwrap().y + layout.area("content").unwrap().height
541 );
542 }
543
544 #[test]
545 fn out_of_bounds_returns_empty() {
546 let grid = Grid::new()
547 .rows([Constraint::Fixed(10)])
548 .columns([Constraint::Fixed(20)]);
549
550 let layout = grid.split(Rect::new(0, 0, 100, 50));
551
552 assert_eq!(layout.cell(5, 5), Rect::default());
554 assert_eq!(layout.cell(0, 5), Rect::default());
555 assert_eq!(layout.cell(5, 0), Rect::default());
556 }
557
558 #[test]
559 fn iter_cells() {
560 let grid = Grid::new()
561 .rows([Constraint::Fixed(10), Constraint::Fixed(10)])
562 .columns([Constraint::Fixed(20), Constraint::Fixed(20)]);
563
564 let layout = grid.split(Rect::new(0, 0, 100, 50));
565
566 let cells: Vec<_> = layout.iter_cells().collect();
567 assert_eq!(cells.len(), 4);
568 assert_eq!(cells[0], (0, 0, Rect::new(0, 0, 20, 10)));
569 assert_eq!(cells[1], (0, 1, Rect::new(20, 0, 20, 10)));
570 assert_eq!(cells[2], (1, 0, Rect::new(0, 10, 20, 10)));
571 assert_eq!(cells[3], (1, 1, Rect::new(20, 10, 20, 10)));
572 }
573
574 #[test]
575 fn undefined_area_returns_none() {
576 let grid = Grid::new()
577 .rows([Constraint::Fixed(10)])
578 .columns([Constraint::Fixed(20)]);
579
580 let layout = grid.split(Rect::new(0, 0, 100, 50));
581
582 assert!(layout.area("nonexistent").is_none());
583 }
584
585 #[test]
586 fn empty_area_produces_empty_cells() {
587 let grid = Grid::new()
588 .rows([Constraint::Fixed(10)])
589 .columns([Constraint::Fixed(20)]);
590
591 let layout = grid.split(Rect::new(0, 0, 0, 0));
592
593 assert_eq!(layout.cell(0, 0), Rect::new(0, 0, 0, 0));
594 }
595
596 #[test]
597 fn offset_area() {
598 let grid = Grid::new()
599 .rows([Constraint::Fixed(10)])
600 .columns([Constraint::Fixed(20)]);
601
602 let layout = grid.split(Rect::new(10, 5, 100, 50));
603
604 assert_eq!(layout.cell(0, 0), Rect::new(10, 5, 20, 10));
606 }
607
608 #[test]
609 fn ratio_constraints() {
610 let grid = Grid::new()
611 .rows([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
612 .columns([Constraint::Fixed(30)]);
613
614 let layout = grid.split(Rect::new(0, 0, 30, 30));
615
616 assert_eq!(layout.row_height(0), 10);
618 assert_eq!(layout.row_height(1), 20);
619 }
620
621 #[test]
622 fn max_constraints() {
623 let grid = Grid::new()
625 .rows([Constraint::Max(5), Constraint::Fixed(20)])
626 .columns([Constraint::Fixed(30)]);
627
628 let layout = grid.split(Rect::new(0, 0, 30, 30));
629
630 assert!(layout.row_height(0) <= 5);
633 assert_eq!(layout.row_height(1), 20);
635 }
636
637 #[test]
638 fn fixed_constraints_exceed_available_clamped() {
639 let grid = Grid::new()
640 .rows([Constraint::Fixed(10), Constraint::Fixed(10)])
641 .columns([Constraint::Fixed(7), Constraint::Fixed(7)]);
642
643 let layout = grid.split(Rect::new(0, 0, 10, 15));
644
645 assert_eq!(layout.row_height(0), 10);
646 assert_eq!(layout.row_height(1), 5);
647 assert_eq!(layout.col_width(0), 7);
648 assert_eq!(layout.col_width(1), 3);
649 }
650
651 #[test]
652 fn ratio_constraints_rounding_sums_to_available() {
653 let grid = Grid::new()
654 .rows([Constraint::Fixed(1)])
655 .columns([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
656
657 let layout = grid.split(Rect::new(0, 0, 5, 1));
658
659 let total = layout.col_width(0) + layout.col_width(1);
660 assert_eq!(total, 5);
661 assert_eq!(layout.col_width(0), 1);
662 assert_eq!(layout.col_width(1), 4);
663 }
664
665 #[test]
668 fn uniform_gap_sets_both() {
669 let grid = Grid::new()
670 .rows([Constraint::Fixed(10), Constraint::Fixed(10)])
671 .columns([Constraint::Fixed(20), Constraint::Fixed(20)])
672 .gap(3);
673
674 let layout = grid.split(Rect::new(0, 0, 100, 50));
675
676 assert_eq!(layout.cell(0, 1).x, 23); assert_eq!(layout.cell(1, 0).y, 13); }
680
681 #[test]
682 fn grid_area_cell_is_1x1_span() {
683 let a = GridArea::cell(2, 3);
684 assert_eq!(a.row, 2);
685 assert_eq!(a.col, 3);
686 assert_eq!(a.rowspan, 1);
687 assert_eq!(a.colspan, 1);
688 }
689
690 #[test]
691 fn grid_area_span_clamps_zero() {
692 let a = GridArea::span(0, 0, 0, 0);
694 assert_eq!(a.rowspan, 1);
695 assert_eq!(a.colspan, 1);
696 }
697
698 #[test]
699 fn grid_num_rows_cols() {
700 let grid = Grid::new()
701 .rows([
702 Constraint::Fixed(5),
703 Constraint::Fixed(5),
704 Constraint::Fixed(5),
705 ])
706 .columns([Constraint::Fixed(10), Constraint::Fixed(10)]);
707 assert_eq!(grid.num_rows(), 3);
708 assert_eq!(grid.num_cols(), 2);
709 }
710
711 #[test]
712 fn grid_row_height_col_width_out_of_bounds() {
713 let grid = Grid::new()
714 .rows([Constraint::Fixed(10)])
715 .columns([Constraint::Fixed(20)]);
716 let layout = grid.split(Rect::new(0, 0, 100, 50));
717 assert_eq!(layout.row_height(0), 10);
718 assert_eq!(layout.row_height(99), 0); assert_eq!(layout.col_width(0), 20);
720 assert_eq!(layout.col_width(99), 0); }
722
723 #[test]
724 fn grid_span_clamped_to_bounds() {
725 let grid = Grid::new()
726 .rows([Constraint::Fixed(10)])
727 .columns([Constraint::Fixed(20)]);
728 let layout = grid.split(Rect::new(0, 0, 100, 50));
729
730 let r = layout.span(0, 0, 5, 5);
732 assert_eq!(r, Rect::new(0, 0, 20, 10));
734 }
735
736 #[test]
737 fn grid_with_all_constraint_types() {
738 let grid = Grid::new()
739 .rows([
740 Constraint::Fixed(5),
741 Constraint::Percentage(20.0),
742 Constraint::Min(3),
743 Constraint::Max(10),
744 Constraint::Ratio(1, 4),
745 ])
746 .columns([Constraint::Fixed(30)]);
747
748 let layout = grid.split(Rect::new(0, 0, 30, 50));
749
750 let total: u16 = (0..layout.num_rows()).map(|r| layout.row_height(r)).sum();
752 assert!(total <= 50);
753 }
754
755 #[test]
757 fn invariant_total_size_within_bounds() {
758 for (width, height) in [(50, 30), (100, 50), (80, 24)] {
759 let grid = Grid::new()
760 .rows([
761 Constraint::Fixed(10),
762 Constraint::Min(5),
763 Constraint::Percentage(20.0),
764 ])
765 .columns([
766 Constraint::Fixed(15),
767 Constraint::Min(10),
768 Constraint::Ratio(1, 2),
769 ]);
770
771 let layout = grid.split(Rect::new(0, 0, width, height));
772
773 let total_height: u16 = (0..layout.num_rows()).map(|r| layout.row_height(r)).sum();
774 let total_width: u16 = (0..layout.num_cols()).map(|c| layout.col_width(c)).sum();
775
776 assert!(
777 total_height <= height,
778 "Total height {} exceeds available {}",
779 total_height,
780 height
781 );
782 assert!(
783 total_width <= width,
784 "Total width {} exceeds available {}",
785 total_width,
786 width
787 );
788 }
789 }
790
791 #[test]
792 fn invariant_cells_within_area() {
793 let area = Rect::new(10, 20, 80, 60);
794 let grid = Grid::new()
795 .rows([
796 Constraint::Fixed(15),
797 Constraint::Min(10),
798 Constraint::Fixed(15),
799 ])
800 .columns([
801 Constraint::Fixed(20),
802 Constraint::Min(20),
803 Constraint::Fixed(20),
804 ])
805 .row_gap(2)
806 .col_gap(3);
807
808 let layout = grid.split(area);
809
810 for (row, col, cell) in layout.iter_cells() {
811 assert!(
812 cell.x >= area.x,
813 "Cell ({},{}) x {} < area x {}",
814 row,
815 col,
816 cell.x,
817 area.x
818 );
819 assert!(
820 cell.y >= area.y,
821 "Cell ({},{}) y {} < area y {}",
822 row,
823 col,
824 cell.y,
825 area.y
826 );
827 assert!(
828 cell.right() <= area.right(),
829 "Cell ({},{}) right {} > area right {}",
830 row,
831 col,
832 cell.right(),
833 area.right()
834 );
835 assert!(
836 cell.bottom() <= area.bottom(),
837 "Cell ({},{}) bottom {} > area bottom {}",
838 row,
839 col,
840 cell.bottom(),
841 area.bottom()
842 );
843 }
844 }
845}