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 cell_spanning() {
415 let grid = Grid::new()
416 .rows([
417 Constraint::Fixed(10),
418 Constraint::Fixed(10),
419 Constraint::Fixed(10),
420 ])
421 .columns([
422 Constraint::Fixed(20),
423 Constraint::Fixed(20),
424 Constraint::Fixed(20),
425 ]);
426
427 let layout = grid.split(Rect::new(0, 0, 100, 50));
428
429 assert_eq!(layout.span(0, 0, 1, 1), Rect::new(0, 0, 20, 10));
431
432 assert_eq!(layout.span(0, 0, 1, 2), Rect::new(0, 0, 40, 10));
434
435 assert_eq!(layout.span(0, 0, 2, 1), Rect::new(0, 0, 20, 20));
437
438 assert_eq!(layout.span(0, 0, 2, 2), Rect::new(0, 0, 40, 20));
440 }
441
442 #[test]
443 fn cell_spanning_with_gaps() {
444 let grid = Grid::new()
445 .rows([Constraint::Fixed(10), Constraint::Fixed(10)])
446 .columns([Constraint::Fixed(20), Constraint::Fixed(20)])
447 .row_gap(2)
448 .col_gap(5);
449
450 let layout = grid.split(Rect::new(0, 0, 100, 50));
451
452 let full = layout.span(0, 0, 2, 2);
454 assert_eq!(full.width, 45);
457 assert_eq!(full.height, 22);
458 }
459
460 #[test]
461 fn named_areas() {
462 let grid = Grid::new()
463 .rows([
464 Constraint::Fixed(5),
465 Constraint::Min(10),
466 Constraint::Fixed(3),
467 ])
468 .columns([Constraint::Fixed(20), Constraint::Min(30)])
469 .area("header", GridArea::span(0, 0, 1, 2))
470 .area("sidebar", GridArea::span(1, 0, 2, 1))
471 .area("content", GridArea::cell(1, 1))
472 .area("footer", GridArea::cell(2, 1));
473
474 let layout = grid.split(Rect::new(0, 0, 80, 30));
475
476 let header = layout.area("header").unwrap();
478 assert_eq!(header.y, 0);
479 assert_eq!(header.height, 5);
480
481 let sidebar = layout.area("sidebar").unwrap();
483 assert_eq!(sidebar.x, 0);
484 assert_eq!(sidebar.width, 20);
485
486 let content = layout.area("content").unwrap();
488 assert_eq!(content.x, 20);
489 assert_eq!(content.y, 5);
490
491 let footer = layout.area("footer").unwrap();
493 assert_eq!(
494 footer.y,
495 layout.area("content").unwrap().y + layout.area("content").unwrap().height
496 );
497 }
498
499 #[test]
500 fn out_of_bounds_returns_empty() {
501 let grid = Grid::new()
502 .rows([Constraint::Fixed(10)])
503 .columns([Constraint::Fixed(20)]);
504
505 let layout = grid.split(Rect::new(0, 0, 100, 50));
506
507 assert_eq!(layout.cell(5, 5), Rect::default());
509 assert_eq!(layout.cell(0, 5), Rect::default());
510 assert_eq!(layout.cell(5, 0), Rect::default());
511 }
512
513 #[test]
514 fn iter_cells() {
515 let grid = Grid::new()
516 .rows([Constraint::Fixed(10), Constraint::Fixed(10)])
517 .columns([Constraint::Fixed(20), Constraint::Fixed(20)]);
518
519 let layout = grid.split(Rect::new(0, 0, 100, 50));
520
521 let cells: Vec<_> = layout.iter_cells().collect();
522 assert_eq!(cells.len(), 4);
523 assert_eq!(cells[0], (0, 0, Rect::new(0, 0, 20, 10)));
524 assert_eq!(cells[1], (0, 1, Rect::new(20, 0, 20, 10)));
525 assert_eq!(cells[2], (1, 0, Rect::new(0, 10, 20, 10)));
526 assert_eq!(cells[3], (1, 1, Rect::new(20, 10, 20, 10)));
527 }
528
529 #[test]
530 fn undefined_area_returns_none() {
531 let grid = Grid::new()
532 .rows([Constraint::Fixed(10)])
533 .columns([Constraint::Fixed(20)]);
534
535 let layout = grid.split(Rect::new(0, 0, 100, 50));
536
537 assert!(layout.area("nonexistent").is_none());
538 }
539
540 #[test]
541 fn empty_area_produces_empty_cells() {
542 let grid = Grid::new()
543 .rows([Constraint::Fixed(10)])
544 .columns([Constraint::Fixed(20)]);
545
546 let layout = grid.split(Rect::new(0, 0, 0, 0));
547
548 assert_eq!(layout.cell(0, 0), Rect::new(0, 0, 0, 0));
549 }
550
551 #[test]
552 fn offset_area() {
553 let grid = Grid::new()
554 .rows([Constraint::Fixed(10)])
555 .columns([Constraint::Fixed(20)]);
556
557 let layout = grid.split(Rect::new(10, 5, 100, 50));
558
559 assert_eq!(layout.cell(0, 0), Rect::new(10, 5, 20, 10));
561 }
562
563 #[test]
564 fn ratio_constraints() {
565 let grid = Grid::new()
566 .rows([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
567 .columns([Constraint::Fixed(30)]);
568
569 let layout = grid.split(Rect::new(0, 0, 30, 30));
570
571 assert_eq!(layout.row_height(0), 10);
573 assert_eq!(layout.row_height(1), 20);
574 }
575
576 #[test]
577 fn max_constraints() {
578 let grid = Grid::new()
580 .rows([Constraint::Max(5), Constraint::Fixed(20)])
581 .columns([Constraint::Fixed(30)]);
582
583 let layout = grid.split(Rect::new(0, 0, 30, 30));
584
585 assert!(layout.row_height(0) <= 5);
588 assert_eq!(layout.row_height(1), 20);
590 }
591
592 #[test]
595 fn uniform_gap_sets_both() {
596 let grid = Grid::new()
597 .rows([Constraint::Fixed(10), Constraint::Fixed(10)])
598 .columns([Constraint::Fixed(20), Constraint::Fixed(20)])
599 .gap(3);
600
601 let layout = grid.split(Rect::new(0, 0, 100, 50));
602
603 assert_eq!(layout.cell(0, 1).x, 23); assert_eq!(layout.cell(1, 0).y, 13); }
607
608 #[test]
609 fn grid_area_cell_is_1x1_span() {
610 let a = GridArea::cell(2, 3);
611 assert_eq!(a.row, 2);
612 assert_eq!(a.col, 3);
613 assert_eq!(a.rowspan, 1);
614 assert_eq!(a.colspan, 1);
615 }
616
617 #[test]
618 fn grid_area_span_clamps_zero() {
619 let a = GridArea::span(0, 0, 0, 0);
621 assert_eq!(a.rowspan, 1);
622 assert_eq!(a.colspan, 1);
623 }
624
625 #[test]
626 fn grid_num_rows_cols() {
627 let grid = Grid::new()
628 .rows([
629 Constraint::Fixed(5),
630 Constraint::Fixed(5),
631 Constraint::Fixed(5),
632 ])
633 .columns([Constraint::Fixed(10), Constraint::Fixed(10)]);
634 assert_eq!(grid.num_rows(), 3);
635 assert_eq!(grid.num_cols(), 2);
636 }
637
638 #[test]
639 fn grid_row_height_col_width_out_of_bounds() {
640 let grid = Grid::new()
641 .rows([Constraint::Fixed(10)])
642 .columns([Constraint::Fixed(20)]);
643 let layout = grid.split(Rect::new(0, 0, 100, 50));
644 assert_eq!(layout.row_height(0), 10);
645 assert_eq!(layout.row_height(99), 0); assert_eq!(layout.col_width(0), 20);
647 assert_eq!(layout.col_width(99), 0); }
649
650 #[test]
651 fn grid_span_clamped_to_bounds() {
652 let grid = Grid::new()
653 .rows([Constraint::Fixed(10)])
654 .columns([Constraint::Fixed(20)]);
655 let layout = grid.split(Rect::new(0, 0, 100, 50));
656
657 let r = layout.span(0, 0, 5, 5);
659 assert_eq!(r, Rect::new(0, 0, 20, 10));
661 }
662
663 #[test]
664 fn grid_with_all_constraint_types() {
665 let grid = Grid::new()
666 .rows([
667 Constraint::Fixed(5),
668 Constraint::Percentage(20.0),
669 Constraint::Min(3),
670 Constraint::Max(10),
671 Constraint::Ratio(1, 4),
672 ])
673 .columns([Constraint::Fixed(30)]);
674
675 let layout = grid.split(Rect::new(0, 0, 30, 50));
676
677 let total: u16 = (0..layout.num_rows()).map(|r| layout.row_height(r)).sum();
679 assert!(total <= 50);
680 }
681
682 #[test]
684 fn invariant_total_size_within_bounds() {
685 for (width, height) in [(50, 30), (100, 50), (80, 24)] {
686 let grid = Grid::new()
687 .rows([
688 Constraint::Fixed(10),
689 Constraint::Min(5),
690 Constraint::Percentage(20.0),
691 ])
692 .columns([
693 Constraint::Fixed(15),
694 Constraint::Min(10),
695 Constraint::Ratio(1, 2),
696 ]);
697
698 let layout = grid.split(Rect::new(0, 0, width, height));
699
700 let total_height: u16 = (0..layout.num_rows()).map(|r| layout.row_height(r)).sum();
701 let total_width: u16 = (0..layout.num_cols()).map(|c| layout.col_width(c)).sum();
702
703 assert!(
704 total_height <= height,
705 "Total height {} exceeds available {}",
706 total_height,
707 height
708 );
709 assert!(
710 total_width <= width,
711 "Total width {} exceeds available {}",
712 total_width,
713 width
714 );
715 }
716 }
717
718 #[test]
719 fn invariant_cells_within_area() {
720 let area = Rect::new(10, 20, 80, 60);
721 let grid = Grid::new()
722 .rows([
723 Constraint::Fixed(15),
724 Constraint::Min(10),
725 Constraint::Fixed(15),
726 ])
727 .columns([
728 Constraint::Fixed(20),
729 Constraint::Min(20),
730 Constraint::Fixed(20),
731 ])
732 .row_gap(2)
733 .col_gap(3);
734
735 let layout = grid.split(area);
736
737 for (row, col, cell) in layout.iter_cells() {
738 assert!(
739 cell.x >= area.x,
740 "Cell ({},{}) x {} < area x {}",
741 row,
742 col,
743 cell.x,
744 area.x
745 );
746 assert!(
747 cell.y >= area.y,
748 "Cell ({},{}) y {} < area y {}",
749 row,
750 col,
751 cell.y,
752 area.y
753 );
754 assert!(
755 cell.right() <= area.right(),
756 "Cell ({},{}) right {} > area right {}",
757 row,
758 col,
759 cell.right(),
760 area.right()
761 );
762 assert!(
763 cell.bottom() <= area.bottom(),
764 "Cell ({},{}) bottom {} > area bottom {}",
765 row,
766 col,
767 cell.bottom(),
768 area.bottom()
769 );
770 }
771 }
772}