ratatui_toolkit/master_layout/
layout.rs1use ratatui::layout::{Constraint, Direction, Layout, Rect};
4
5#[derive(Debug, Clone)]
7pub enum PaneLayout {
8 Horizontal(Vec<u16>),
11
12 Vertical(Vec<u16>),
15
16 Grid { rows: usize, cols: usize },
19
20 Custom(fn(Rect) -> Vec<Rect>),
23}
24
25impl PaneLayout {
26 pub fn calculate_areas(&self, available_area: Rect, pane_count: usize) -> Vec<Rect> {
28 match self {
29 PaneLayout::Horizontal(percentages) => {
30 self.calculate_horizontal(available_area, percentages, pane_count)
31 }
32 PaneLayout::Vertical(percentages) => {
33 self.calculate_vertical(available_area, percentages, pane_count)
34 }
35 PaneLayout::Grid { rows, cols } => {
36 self.calculate_grid(available_area, *rows, *cols, pane_count)
37 }
38 PaneLayout::Custom(func) => func(available_area),
39 }
40 }
41
42 fn calculate_horizontal(
43 &self,
44 available_area: Rect,
45 percentages: &[u16],
46 pane_count: usize,
47 ) -> Vec<Rect> {
48 let constraints: Vec<Constraint> = if percentages.is_empty() {
50 let percent = 100 / pane_count.max(1) as u16;
52 (0..pane_count)
53 .map(|_| Constraint::Percentage(percent))
54 .collect()
55 } else {
56 let mut constraints = percentages
58 .iter()
59 .map(|&p| Constraint::Percentage(p))
60 .collect::<Vec<_>>();
61
62 if pane_count > percentages.len() {
64 let used: u16 = percentages.iter().sum();
65 let remaining = 100_u16.saturating_sub(used);
66 let additional_panes = pane_count - percentages.len();
67 let each = remaining / additional_panes as u16;
68
69 for _ in 0..additional_panes {
70 constraints.push(Constraint::Percentage(each));
71 }
72 }
73
74 constraints
75 };
76
77 let chunks = Layout::default()
78 .direction(Direction::Horizontal)
79 .constraints(constraints)
80 .split(available_area);
81
82 chunks.iter().copied().take(pane_count).collect()
83 }
84
85 fn calculate_vertical(
86 &self,
87 available_area: Rect,
88 percentages: &[u16],
89 pane_count: usize,
90 ) -> Vec<Rect> {
91 let constraints: Vec<Constraint> = if percentages.is_empty() {
92 let percent = 100 / pane_count.max(1) as u16;
93 (0..pane_count)
94 .map(|_| Constraint::Percentage(percent))
95 .collect()
96 } else {
97 let mut constraints = percentages
98 .iter()
99 .map(|&p| Constraint::Percentage(p))
100 .collect::<Vec<_>>();
101
102 if pane_count > percentages.len() {
103 let used: u16 = percentages.iter().sum();
104 let remaining = 100_u16.saturating_sub(used);
105 let additional_panes = pane_count - percentages.len();
106 let each = remaining / additional_panes as u16;
107
108 for _ in 0..additional_panes {
109 constraints.push(Constraint::Percentage(each));
110 }
111 }
112
113 constraints
114 };
115
116 let chunks = Layout::default()
117 .direction(Direction::Vertical)
118 .constraints(constraints)
119 .split(available_area);
120
121 chunks.iter().copied().take(pane_count).collect()
122 }
123
124 fn calculate_grid(
125 &self,
126 available_area: Rect,
127 rows: usize,
128 cols: usize,
129 pane_count: usize,
130 ) -> Vec<Rect> {
131 let mut areas = Vec::new();
132
133 let cell_height = available_area.height / rows.max(1) as u16;
135 let cell_width = available_area.width / cols.max(1) as u16;
136
137 for i in 0..pane_count {
139 let row = i / cols;
140 let col = i % cols;
141
142 if row >= rows {
143 break; }
145
146 let x = available_area.x + (col as u16 * cell_width);
147 let y = available_area.y + (row as u16 * cell_height);
148
149 let width = if col == cols - 1 {
151 available_area.width - (col as u16 * cell_width)
152 } else {
153 cell_width
154 };
155
156 let height = if row == rows - 1 {
157 available_area.height - (row as u16 * cell_height)
158 } else {
159 cell_height
160 };
161
162 areas.push(Rect::new(x, y, width, height));
163 }
164
165 areas
166 }
167}
168
169impl Default for PaneLayout {
170 fn default() -> Self {
171 PaneLayout::Horizontal(vec![])
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn test_horizontal_equal_split() {
182 let layout = PaneLayout::Horizontal(vec![]);
183 let area = Rect::new(0, 0, 100, 50);
184 let areas = layout.calculate_areas(area, 2);
185
186 assert_eq!(areas.len(), 2);
187 assert_eq!(areas[0].width, 50);
188 assert_eq!(areas[1].width, 50);
189 }
190
191 #[test]
192 fn test_horizontal_custom_percentages() {
193 let layout = PaneLayout::Horizontal(vec![30, 70]);
194 let area = Rect::new(0, 0, 100, 50);
195 let areas = layout.calculate_areas(area, 2);
196
197 assert_eq!(areas.len(), 2);
198 assert!(areas[0].width <= 30);
200 assert!(areas[1].width >= 70);
201 }
202
203 #[test]
204 fn test_vertical_equal_split() {
205 let layout = PaneLayout::Vertical(vec![]);
206 let area = Rect::new(0, 0, 100, 100);
207 let areas = layout.calculate_areas(area, 2);
208
209 assert_eq!(areas.len(), 2);
210 assert_eq!(areas[0].height, 50);
211 assert_eq!(areas[1].height, 50);
212 }
213
214 #[test]
215 fn test_vertical_custom_percentages() {
216 let layout = PaneLayout::Vertical(vec![25, 75]);
217 let area = Rect::new(0, 0, 100, 100);
218 let areas = layout.calculate_areas(area, 2);
219
220 assert_eq!(areas.len(), 2);
221 assert!(areas[0].height <= 25);
222 assert!(areas[1].height >= 75);
223 }
224
225 #[test]
226 fn test_grid_2x2() {
227 let layout = PaneLayout::Grid { rows: 2, cols: 2 };
228 let area = Rect::new(0, 0, 100, 100);
229 let areas = layout.calculate_areas(area, 4);
230
231 assert_eq!(areas.len(), 4);
232
233 assert_eq!(areas[0].x, 0);
235 assert_eq!(areas[0].y, 0);
236
237 assert_eq!(areas[1].x, 50);
238 assert_eq!(areas[1].y, 0);
239
240 assert_eq!(areas[2].x, 0);
241 assert_eq!(areas[2].y, 50);
242
243 assert_eq!(areas[3].x, 50);
244 assert_eq!(areas[3].y, 50);
245 }
246
247 #[test]
248 fn test_grid_incomplete() {
249 let layout = PaneLayout::Grid { rows: 2, cols: 2 };
250 let area = Rect::new(0, 0, 100, 100);
251 let areas = layout.calculate_areas(area, 3);
252
253 assert_eq!(areas.len(), 3);
255 }
256
257 #[test]
258 fn test_grid_overflow() {
259 let layout = PaneLayout::Grid { rows: 2, cols: 2 };
260 let area = Rect::new(0, 0, 100, 100);
261 let areas = layout.calculate_areas(area, 10);
262
263 assert_eq!(areas.len(), 4);
265 }
266
267 #[test]
268 fn test_custom_layout() {
269 fn custom_layout(area: Rect) -> Vec<Rect> {
270 vec![
271 Rect::new(area.x, area.y, area.width / 3, area.height),
272 Rect::new(
273 area.x + area.width / 3,
274 area.y,
275 area.width * 2 / 3,
276 area.height,
277 ),
278 ]
279 }
280
281 let layout = PaneLayout::Custom(custom_layout);
282 let area = Rect::new(0, 0, 90, 50);
283 let areas = layout.calculate_areas(area, 2);
284
285 assert_eq!(areas.len(), 2);
286 assert_eq!(areas[0].width, 30);
287 assert_eq!(areas[1].width, 60);
288 }
289
290 #[test]
291 fn test_horizontal_more_panes_than_percentages() {
292 let layout = PaneLayout::Horizontal(vec![40]);
293 let area = Rect::new(0, 0, 100, 50);
294 let areas = layout.calculate_areas(area, 3);
295
296 assert_eq!(areas.len(), 3);
297 }
299
300 #[test]
301 fn test_vertical_more_panes_than_percentages() {
302 let layout = PaneLayout::Vertical(vec![30]);
303 let area = Rect::new(0, 0, 100, 100);
304 let areas = layout.calculate_areas(area, 3);
305
306 assert_eq!(areas.len(), 3);
307 }
309
310 #[test]
311 fn test_default_layout() {
312 let layout = PaneLayout::default();
313 let area = Rect::new(0, 0, 100, 50);
314 let areas = layout.calculate_areas(area, 3);
315
316 assert_eq!(areas.len(), 3);
318 for area in areas {
320 assert!(area.width > 0);
321 }
322 }
323}