Skip to main content

ftui_layout/
grid.rs

1#![forbid(unsafe_code)]
2
3//! 2D Grid layout system for dashboard-style positioning.
4//!
5//! Grid provides constraint-based 2D positioning with support for:
6//! - Row and column constraints
7//! - Cell spanning (colspan, rowspan)
8//! - Named areas for semantic layout references
9//! - Gap configuration
10//!
11//! # Example
12//!
13//! ```
14//! use ftui_layout::grid::Grid;
15//! use ftui_layout::Constraint;
16//! use ftui_core::geometry::Rect;
17//!
18//! // Create a 3x2 grid (3 rows, 2 columns)
19//! let grid = Grid::new()
20//!     .rows([
21//!         Constraint::Fixed(3),      // Header
22//!         Constraint::Min(10),       // Content
23//!         Constraint::Fixed(1),      // Footer
24//!     ])
25//!     .columns([
26//!         Constraint::Percentage(30.0),  // Sidebar
27//!         Constraint::Min(20),            // Main
28//!     ])
29//!     .row_gap(1)
30//!     .col_gap(2);
31//!
32//! let area = Rect::new(0, 0, 80, 24);
33//! let layout = grid.split(area);
34//!
35//! // Access cell by (row, col)
36//! let header_left = layout.cell(0, 0);
37//! let content_main = layout.cell(1, 1);
38//! ```
39
40use crate::Constraint;
41use ftui_core::geometry::Rect;
42use std::collections::HashMap;
43
44/// A 2D grid layout container.
45#[derive(Debug, Clone, Default)]
46pub struct Grid {
47    /// Row constraints (height of each row).
48    row_constraints: Vec<Constraint>,
49    /// Column constraints (width of each column).
50    col_constraints: Vec<Constraint>,
51    /// Gap between rows.
52    row_gap: u16,
53    /// Gap between columns.
54    col_gap: u16,
55    /// Named areas mapping to (row, col, rowspan, colspan).
56    named_areas: HashMap<String, GridArea>,
57}
58
59/// Definition of a named grid area.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct GridArea {
62    /// Starting row (0-indexed).
63    pub row: usize,
64    /// Starting column (0-indexed).
65    pub col: usize,
66    /// Number of rows this area spans.
67    pub rowspan: usize,
68    /// Number of columns this area spans.
69    pub colspan: usize,
70}
71
72impl GridArea {
73    /// Create a single-cell area.
74    pub fn cell(row: usize, col: usize) -> Self {
75        Self {
76            row,
77            col,
78            rowspan: 1,
79            colspan: 1,
80        }
81    }
82
83    /// Create a spanning area.
84    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/// Result of solving a grid layout.
95#[derive(Debug, Clone)]
96pub struct GridLayout {
97    /// Row heights.
98    row_heights: Vec<u16>,
99    /// Column widths.
100    col_widths: Vec<u16>,
101    /// Row Y positions (cumulative with gaps).
102    row_positions: Vec<u16>,
103    /// Column X positions (cumulative with gaps).
104    col_positions: Vec<u16>,
105    /// Named areas from the grid definition.
106    named_areas: HashMap<String, GridArea>,
107    /// Gap between rows.
108    row_gap: u16,
109    /// Gap between columns.
110    col_gap: u16,
111}
112
113impl Grid {
114    /// Create a new empty grid.
115    pub fn new() -> Self {
116        Self::default()
117    }
118
119    /// Set the row constraints.
120    pub fn rows(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
121        self.row_constraints = constraints.into_iter().collect();
122        self
123    }
124
125    /// Set the column constraints.
126    pub fn columns(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
127        self.col_constraints = constraints.into_iter().collect();
128        self
129    }
130
131    /// Set the gap between rows.
132    pub fn row_gap(mut self, gap: u16) -> Self {
133        self.row_gap = gap;
134        self
135    }
136
137    /// Set the gap between columns.
138    pub fn col_gap(mut self, gap: u16) -> Self {
139        self.col_gap = gap;
140        self
141    }
142
143    /// Set uniform gap for both rows and columns.
144    pub fn gap(self, gap: u16) -> Self {
145        self.row_gap(gap).col_gap(gap)
146    }
147
148    /// Define a named area in the grid.
149    ///
150    /// Named areas allow semantic access to grid regions:
151    /// ```ignore
152    /// let grid = Grid::new()
153    ///     .rows([Constraint::Fixed(3), Constraint::Min(10)])
154    ///     .columns([Constraint::Fixed(20), Constraint::Min(40)])
155    ///     .area("sidebar", GridArea::span(0, 0, 2, 1))  // Left column, both rows
156    ///     .area("content", GridArea::cell(0, 1))        // Top right
157    ///     .area("footer", GridArea::cell(1, 1));        // Bottom right
158    /// ```
159    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    /// Get the number of rows.
165    pub fn num_rows(&self) -> usize {
166        self.row_constraints.len()
167    }
168
169    /// Get the number of columns.
170    pub fn num_cols(&self) -> usize {
171        self.col_constraints.len()
172    }
173
174    /// Split the given area according to the grid configuration.
175    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        // Calculate total gaps
192        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        // Available space after gaps
206        let available_height = area.height.saturating_sub(total_row_gap);
207        let available_width = area.width.saturating_sub(total_col_gap);
208
209        // Solve constraints
210        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        // Calculate positions
214        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    /// Calculate cumulative positions from sizes.
229    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    /// Get the rectangle for a specific cell.
247    ///
248    /// Returns an empty Rect if coordinates are out of bounds.
249    pub fn cell(&self, row: usize, col: usize) -> Rect {
250        self.span(row, col, 1, 1)
251    }
252
253    /// Get the rectangle for a spanning region.
254    ///
255    /// The region starts at (row, col) and spans rowspan rows and colspan columns.
256    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        // Bounds check
261        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        // Get starting position
269        let x = self.col_positions[col];
270        let y = self.row_positions[row];
271
272        // Calculate total width (sum of widths + gaps between spanned columns)
273        let mut width: u16 = 0;
274        for c in col..end_col {
275            width = width.saturating_add(self.col_widths[c]);
276        }
277        // Add gaps between columns (not after last)
278        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        // Calculate total height (sum of heights + gaps between spanned rows)
284        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    /// Get the rectangle for a named area.
297    ///
298    /// Returns None if the area name is not defined.
299    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    /// Get the number of rows in this layout.
306    pub fn num_rows(&self) -> usize {
307        self.row_heights.len()
308    }
309
310    /// Get the number of columns in this layout.
311    pub fn num_cols(&self) -> usize {
312        self.col_widths.len()
313    }
314
315    /// Get the height of a specific row.
316    pub fn row_height(&self, row: usize) -> u16 {
317        self.row_heights.get(row).copied().unwrap_or(0)
318    }
319
320    /// Get the width of a specific column.
321    pub fn col_width(&self, col: usize) -> u16 {
322        self.col_widths.get(col).copied().unwrap_or(0)
323    }
324
325    /// Iterate over all cells, yielding (row, col, Rect).
326    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        // Check each cell
358        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        // First row, first col
375        assert_eq!(layout.cell(0, 0), Rect::new(0, 0, 20, 10));
376        // First row, second col (after col_gap of 5)
377        assert_eq!(layout.cell(0, 1), Rect::new(25, 0, 20, 10));
378        // Second row, first col (after row_gap of 2)
379        assert_eq!(layout.cell(1, 0), Rect::new(0, 12, 20, 10));
380        // Second row, second col
381        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        // Min should expand to fill remaining space
407        assert_eq!(layout.row_height(0), 10);
408        assert_eq!(layout.row_height(1), 40); // 50 - 10 = 40
409        assert_eq!(layout.col_width(0), 20);
410        assert_eq!(layout.col_width(1), 80); // 100 - 20 = 80
411    }
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        // Single cell
430        assert_eq!(layout.span(0, 0, 1, 1), Rect::new(0, 0, 20, 10));
431
432        // Horizontal span (2 columns)
433        assert_eq!(layout.span(0, 0, 1, 2), Rect::new(0, 0, 40, 10));
434
435        // Vertical span (2 rows)
436        assert_eq!(layout.span(0, 0, 2, 1), Rect::new(0, 0, 20, 20));
437
438        // 2x2 block
439        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        // 2x2 span should include the gaps
453        let full = layout.span(0, 0, 2, 2);
454        // Width: 20 + 5 (gap) + 20 = 45
455        // Height: 10 + 2 (gap) + 10 = 22
456        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        // Header spans both columns
477        let header = layout.area("header").unwrap();
478        assert_eq!(header.y, 0);
479        assert_eq!(header.height, 5);
480
481        // Sidebar spans rows 1 and 2
482        let sidebar = layout.area("sidebar").unwrap();
483        assert_eq!(sidebar.x, 0);
484        assert_eq!(sidebar.width, 20);
485
486        // Content is in the middle
487        let content = layout.area("content").unwrap();
488        assert_eq!(content.x, 20);
489        assert_eq!(content.y, 5);
490
491        // Footer is at the bottom right
492        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        // Out of bounds
508        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        // Cell should be offset by the area origin
560        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        // 1:2 ratio should give roughly 10:20 split
572        assert_eq!(layout.row_height(0), 10);
573        assert_eq!(layout.row_height(1), 20);
574    }
575
576    #[test]
577    fn max_constraints() {
578        // Test that Max(N) clamps the size to at most N
579        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        // Max(5) should get at most 5, but the remaining 5 (from 30-20=10 available)
586        // is distributed to Max, giving 5 which is then clamped to 5
587        assert!(layout.row_height(0) <= 5);
588        // Fixed gets its exact size
589        assert_eq!(layout.row_height(1), 20);
590    }
591
592    // --- Additional Grid tests ---
593
594    #[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        // Both row_gap and col_gap should be 3
604        assert_eq!(layout.cell(0, 1).x, 23); // 20 + 3
605        assert_eq!(layout.cell(1, 0).y, 13); // 10 + 3
606    }
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        // Zero spans should be clamped to 1
620        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); // Out of bounds returns 0
646        assert_eq!(layout.col_width(0), 20);
647        assert_eq!(layout.col_width(99), 0); // Out of bounds returns 0
648    }
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        // Spanning beyond grid dimensions should clamp
658        let r = layout.span(0, 0, 5, 5);
659        // Should get the single cell (1x1 grid)
660        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        // All rows should have non-negative heights
678        let total: u16 = (0..layout.num_rows()).map(|r| layout.row_height(r)).sum();
679        assert!(total <= 50);
680    }
681
682    // Property-like invariant tests
683    #[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}