ratatui_auto_grid/
lib.rs

1use ratatui::layout::{Constraint, Layout, Rect};
2
3/// Arranges `n` items in an automatic grid layout within the given area.
4///
5/// Uses a square root approach to determine grid dimensions:
6/// - Calculates columns as √n (rounded up)
7/// - Calculates rows as n/cols (rounded up)
8///
9/// # Arguments
10///
11/// * `area` - The rectangular area to split into a grid
12/// * `n` - Number of cells needed in the grid
13/// * `spacing` - Space between cells
14///
15/// # Returns
16///
17/// A vector of `n` Rects, arranged in row-major order (left-to-right, top-to-bottom)
18///
19/// # Example
20///
21/// ```
22/// use ratatui::layout::Rect;
23/// use ratatui_auto_grid::auto_grid;
24///
25/// let area = Rect::new(0, 0, 100, 100);
26/// let cells = auto_grid(area, 9, 1);
27/// assert_eq!(cells.len(), 9);
28/// ```
29pub fn auto_grid(area: Rect, n: usize, spacing: u16) -> Vec<Rect> {
30    if n == 0 {
31        return Vec::new();
32    }
33
34    let cols = (n as f64).sqrt().ceil() as u16;
35    let rows = ((n as f64) / f64::from(cols)).ceil() as u16;
36
37    let row_constraints: Vec<Constraint> =
38        std::iter::repeat_n(Constraint::Ratio(1, rows.into()), rows as usize).collect();
39
40    let col_constraints: Vec<Constraint> =
41        std::iter::repeat_n(Constraint::Ratio(1, cols.into()), cols as usize).collect();
42
43    let row_areas = Layout::vertical(row_constraints)
44        .spacing(spacing)
45        .split(area);
46
47    let mut out = Vec::with_capacity(n);
48    'outer: for r in 0..rows as usize {
49        let col_areas = Layout::horizontal(col_constraints.clone())
50            .spacing(spacing)
51            .split(row_areas[r]);
52        for &rect in col_areas.iter() {
53            if out.len() == n {
54                break 'outer;
55            }
56            out.push(rect);
57        }
58    }
59    out
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn empty_grid() {
68        let area = Rect::new(0, 0, 100, 100);
69        let result = auto_grid(area, 0, 0);
70        assert_eq!(result.len(), 0);
71    }
72
73    #[test]
74    fn single_cell() {
75        let area = Rect::new(0, 0, 100, 100);
76        let result = auto_grid(area, 1, 0);
77        assert_eq!(result.len(), 1);
78        assert_eq!(result[0], area, "Single cell should fill entire area");
79    }
80
81    #[test]
82    fn four_cells_perfect_square() {
83        let area = Rect::new(0, 0, 100, 100);
84        let result = auto_grid(area, 4, 0);
85
86        assert_eq!(result.len(), 4);
87
88        // Top left
89        assert_eq!(result[0].x, 0);
90        assert_eq!(result[0].y, 0);
91        assert_eq!(result[0].width, 50);
92        assert_eq!(result[0].height, 50);
93
94        // Top right
95        assert_eq!(result[1].x, 50);
96        assert_eq!(result[1].y, 0);
97
98        // Bottom left
99        assert_eq!(result[2].x, 0);
100        assert_eq!(result[2].y, 50);
101
102        // Bottom right
103        assert_eq!(result[3].x, 50);
104        assert_eq!(result[3].y, 50);
105    }
106
107    #[test]
108    fn nine_cells() {
109        let area = Rect::new(0, 0, 99, 99);
110        let result = auto_grid(area, 9, 0);
111
112        assert_eq!(result.len(), 9);
113
114        // first cell
115        assert_eq!(result[0].x, 0);
116        assert_eq!(result[0].y, 0);
117        assert_eq!(result[0].width, 33);
118        assert_eq!(result[0].height, 33);
119
120        // last cell
121        assert_eq!(result[8].x, 66);
122        assert_eq!(result[8].y, 66);
123    }
124
125    #[test]
126    fn non_square_grid() {
127        let area = Rect::new(0, 0, 100, 100);
128        let result = auto_grid(area, 6, 0);
129
130        assert_eq!(result.len(), 6);
131
132        // First row
133        assert_eq!(result[0].y, result[1].y);
134        assert_eq!(result[1].y, result[2].y);
135
136        // Second row
137        assert_eq!(result[3].y, result[4].y);
138        assert_eq!(result[4].y, result[5].y);
139
140        assert_ne!(result[0].y, result[3].y);
141    }
142
143    #[test]
144    fn with_spacing() {
145        let area = Rect::new(0, 0, 100, 100);
146        let result_no_spacing = auto_grid(area, 4, 0);
147        let result_with_spacing = auto_grid(area, 4, 2);
148
149        assert_eq!(result_no_spacing.len(), 4);
150        assert_eq!(result_with_spacing.len(), 4);
151
152        assert!(result_with_spacing[0].width <= result_no_spacing[0].width);
153        assert!(result_with_spacing[0].height <= result_no_spacing[0].height);
154
155        let gap_no_spacing =
156            result_no_spacing[1].x - (result_no_spacing[0].x + result_no_spacing[0].width);
157        let gap_with_spacing =
158            result_with_spacing[1].x - (result_with_spacing[0].x + result_with_spacing[0].width);
159        assert!(gap_with_spacing >= gap_no_spacing);
160    }
161
162    #[test]
163    fn all_cells_within_bounds() {
164        let area = Rect::new(10, 10, 200, 150);
165        let result = auto_grid(area, 7, 1);
166
167        for (i, rect) in result.iter().enumerate() {
168            assert!(
169                rect.x >= area.x,
170                "Cell {} x position {} should be >= area.x {}",
171                i,
172                rect.x,
173                area.x
174            );
175            assert!(
176                rect.y >= area.y,
177                "Cell {} y position {} should be >= area.y {}",
178                i,
179                rect.y,
180                area.y
181            );
182            assert!(
183                rect.x + rect.width <= area.x + area.width,
184                "Cell {} right edge should be within area bounds",
185                i
186            );
187            assert!(
188                rect.y + rect.height <= area.y + area.height,
189                "Cell {} bottom edge should be withing area bounds",
190                i
191            );
192        }
193    }
194
195    #[test]
196    fn row_major_order() {
197        let area = Rect::new(0, 0, 100, 100);
198        let result = auto_grid(area, 6, 0);
199
200        assert_eq!(result[0].y, result[1].y);
201        assert_eq!(result[1].y, result[2].y);
202
203        assert_eq!(result[3].y, result[4].y);
204        assert_eq!(result[4].y, result[5].y);
205
206        assert!(result[0].y < result[3].y);
207    }
208
209    #[test]
210    fn exact_count_returned() {
211        for n in 1..=20 {
212            let area = Rect::new(0, 0, 100, 100);
213            let result = auto_grid(area, n, 0);
214            assert_eq!(
215                result.len(),
216                n,
217                "should return exactly {} cells, got {}",
218                n,
219                result.len()
220            );
221        }
222    }
223}