1use ratatui::layout::{Constraint, Direction, Layout, Rect};
4
5use crate::config::AppearanceConfig;
6
7pub const MIN_WIDTH: u16 = 40;
9pub const MIN_HEIGHT: u16 = 10;
10
11#[derive(Debug, Clone, Copy)]
13pub struct MainLayout {
14 pub header: Rect,
16 pub filter: Rect,
18 pub scripts: Rect,
20 pub description: Rect,
22 pub footer: Rect,
24}
25
26impl MainLayout {
27 pub fn new(area: Rect) -> Self {
29 Self::with_config(area, &AppearanceConfig::default())
30 }
31
32 pub fn with_config(area: Rect, config: &AppearanceConfig) -> Self {
34 if area.width < MIN_WIDTH || area.height < MIN_HEIGHT {
36 return Self::minimal_layout(area);
37 }
38
39 let (header_height, filter_height, desc_height, footer_height) = if config.compact {
40 (1, 1, 2, if config.show_footer { 1 } else { 0 })
41 } else {
42 (1, 1, 4, if config.show_footer { 1 } else { 0 })
43 };
44
45 let constraints = if config.show_footer {
46 vec![
47 Constraint::Length(header_height),
48 Constraint::Length(filter_height),
49 Constraint::Min(3), Constraint::Length(desc_height),
51 Constraint::Length(footer_height),
52 ]
53 } else {
54 vec![
55 Constraint::Length(header_height),
56 Constraint::Length(filter_height),
57 Constraint::Min(3),
58 Constraint::Length(desc_height),
59 Constraint::Length(0), ]
61 };
62
63 let chunks = Layout::default()
64 .direction(Direction::Vertical)
65 .constraints(constraints)
66 .split(area);
67
68 Self {
69 header: chunks[0],
70 filter: chunks[1],
71 scripts: chunks[2],
72 description: chunks[3],
73 footer: chunks[4],
74 }
75 }
76
77 fn minimal_layout(area: Rect) -> Self {
79 let chunks = Layout::default()
80 .direction(Direction::Vertical)
81 .constraints([
82 Constraint::Length(1), Constraint::Length(1), Constraint::Min(1), Constraint::Length(1), Constraint::Length(1), ])
88 .split(area);
89
90 Self {
91 header: chunks[0],
92 filter: chunks[1],
93 scripts: chunks[2],
94 description: chunks[3],
95 footer: chunks[4],
96 }
97 }
98
99 pub fn script_rows(&self) -> usize {
101 self.scripts.height as usize
102 }
103
104 pub fn is_too_small(&self) -> bool {
106 self.scripts.height < 1
107 }
108}
109
110pub fn calculate_columns(width: u16) -> usize {
112 match width {
113 0..=59 => 1,
114 60..=89 => 2,
115 90..=119 => 3,
116 120..=159 => 4,
117 _ => 5,
118 }
119}
120
121pub fn calculate_column_width(total_width: u16, columns: usize) -> u16 {
123 if columns == 0 {
124 return total_width;
125 }
126 let padding = 2; let available = total_width.saturating_sub(padding);
128 let gap_count = columns.saturating_sub(1) as u16;
129 let gaps_width = gap_count; available.saturating_sub(gaps_width) / columns as u16
131}
132
133#[derive(Debug, Clone, Copy)]
135pub struct GridLayout {
136 pub columns: usize,
138 pub rows: usize,
140 pub column_width: u16,
142 pub visible_items: usize,
144}
145
146impl GridLayout {
147 pub fn new(area: Rect, total_items: usize) -> Self {
149 let columns = calculate_columns(area.width);
150 let column_width = calculate_column_width(area.width, columns);
151 let rows = area.height as usize;
152 let visible_items = (rows * columns).min(total_items);
153
154 Self {
155 columns,
156 rows,
157 column_width,
158 visible_items,
159 }
160 }
161
162 pub fn position(&self, index: usize) -> (usize, usize) {
164 let row = index / self.columns;
165 let col = index % self.columns;
166 (row, col)
167 }
168
169 pub fn index(&self, row: usize, col: usize) -> usize {
171 row * self.columns + col
172 }
173
174 pub fn is_visible(&self, index: usize, scroll_offset: usize) -> bool {
176 index >= scroll_offset && index < scroll_offset + self.visible_items
177 }
178
179 pub fn display_position(&self, index: usize, scroll_offset: usize) -> Option<(usize, usize)> {
181 if self.is_visible(index, scroll_offset) {
182 let display_index = index - scroll_offset;
183 Some(self.position(display_index))
184 } else {
185 None
186 }
187 }
188}
189
190pub fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
192 let popup_layout = Layout::default()
193 .direction(Direction::Vertical)
194 .constraints([
195 Constraint::Percentage((100 - percent_y) / 2),
196 Constraint::Percentage(percent_y),
197 Constraint::Percentage((100 - percent_y) / 2),
198 ])
199 .split(area);
200
201 Layout::default()
202 .direction(Direction::Horizontal)
203 .constraints([
204 Constraint::Percentage((100 - percent_x) / 2),
205 Constraint::Percentage(percent_x),
206 Constraint::Percentage((100 - percent_x) / 2),
207 ])
208 .split(popup_layout[1])[1]
209}
210
211pub fn centered_rect_fixed(width: u16, height: u16, area: Rect) -> Rect {
213 let actual_width = width.min(area.width);
214 let actual_height = height.min(area.height);
215
216 let x = area.x + (area.width.saturating_sub(actual_width)) / 2;
217 let y = area.y + (area.height.saturating_sub(actual_height)) / 2;
218
219 Rect::new(x, y, actual_width, actual_height)
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn test_main_layout_default() {
228 let area = Rect::new(0, 0, 100, 30);
229 let layout = MainLayout::new(area);
230
231 assert_eq!(layout.header.height, 1);
232 assert_eq!(layout.filter.height, 1);
233 assert!(layout.scripts.height >= 3);
234 assert_eq!(layout.description.height, 4);
235 assert_eq!(layout.footer.height, 1);
236 }
237
238 #[test]
239 fn test_main_layout_compact() {
240 let area = Rect::new(0, 0, 100, 30);
241 let config = AppearanceConfig {
242 compact: true,
243 ..Default::default()
244 };
245 let layout = MainLayout::with_config(area, &config);
246
247 assert_eq!(layout.description.height, 2);
248 }
249
250 #[test]
251 fn test_main_layout_no_footer() {
252 let area = Rect::new(0, 0, 100, 30);
253 let config = AppearanceConfig {
254 show_footer: false,
255 ..Default::default()
256 };
257 let layout = MainLayout::with_config(area, &config);
258
259 assert_eq!(layout.footer.height, 0);
260 }
261
262 #[test]
263 fn test_main_layout_small_terminal() {
264 let area = Rect::new(0, 0, 30, 8);
265 let layout = MainLayout::new(area);
266
267 assert_eq!(layout.header.height, 1);
269 assert_eq!(layout.filter.height, 1);
270 assert_eq!(layout.description.height, 1);
271 assert_eq!(layout.footer.height, 1);
272 }
273
274 #[test]
275 fn test_calculate_columns() {
276 assert_eq!(calculate_columns(40), 1);
277 assert_eq!(calculate_columns(59), 1);
278 assert_eq!(calculate_columns(60), 2);
279 assert_eq!(calculate_columns(89), 2);
280 assert_eq!(calculate_columns(90), 3);
281 assert_eq!(calculate_columns(119), 3);
282 assert_eq!(calculate_columns(120), 4);
283 assert_eq!(calculate_columns(159), 4);
284 assert_eq!(calculate_columns(160), 5);
285 }
286
287 #[test]
288 fn test_calculate_column_width() {
289 assert_eq!(calculate_column_width(100, 2), 48);
290 assert_eq!(calculate_column_width(120, 3), 38);
291 assert_eq!(calculate_column_width(60, 1), 58);
292 assert_eq!(calculate_column_width(50, 0), 50);
293 }
294
295 #[test]
296 fn test_grid_layout_position() {
297 let area = Rect::new(0, 0, 100, 10);
298 let grid = GridLayout::new(area, 30);
299
300 assert_eq!(grid.columns, 3);
301 assert_eq!(grid.rows, 10);
302
303 assert_eq!(grid.position(0), (0, 0));
304 assert_eq!(grid.position(1), (0, 1));
305 assert_eq!(grid.position(2), (0, 2));
306 assert_eq!(grid.position(3), (1, 0));
307 assert_eq!(grid.position(5), (1, 2));
308 }
309
310 #[test]
311 fn test_grid_layout_index() {
312 let area = Rect::new(0, 0, 100, 10);
313 let grid = GridLayout::new(area, 30);
314
315 assert_eq!(grid.index(0, 0), 0);
316 assert_eq!(grid.index(0, 2), 2);
317 assert_eq!(grid.index(1, 0), 3);
318 assert_eq!(grid.index(2, 1), 7);
319 }
320
321 #[test]
322 fn test_grid_layout_visibility() {
323 let area = Rect::new(0, 0, 60, 5); let grid = GridLayout::new(area, 20);
325
326 assert_eq!(grid.visible_items, 10);
327 assert!(grid.is_visible(0, 0));
328 assert!(grid.is_visible(9, 0));
329 assert!(!grid.is_visible(10, 0));
330
331 assert!(!grid.is_visible(0, 5));
333 assert!(grid.is_visible(5, 5));
334 assert!(grid.is_visible(14, 5));
335 }
336
337 #[test]
338 fn test_centered_rect() {
339 let area = Rect::new(0, 0, 100, 50);
340 let centered = centered_rect(50, 50, area);
341
342 assert!(centered.x >= 20 && centered.x <= 30);
344 assert!(centered.y >= 10 && centered.y <= 15);
345 assert!(centered.width >= 45 && centered.width <= 55);
346 assert!(centered.height >= 22 && centered.height <= 28);
347 }
348
349 #[test]
350 fn test_centered_rect_fixed() {
351 let area = Rect::new(0, 0, 100, 50);
352 let centered = centered_rect_fixed(40, 20, area);
353
354 assert_eq!(centered.width, 40);
355 assert_eq!(centered.height, 20);
356 assert_eq!(centered.x, 30); assert_eq!(centered.y, 15); }
359
360 #[test]
361 fn test_centered_rect_fixed_clamps_to_area() {
362 let area = Rect::new(0, 0, 30, 20);
363 let centered = centered_rect_fixed(100, 50, area);
364
365 assert_eq!(centered.width, 30);
366 assert_eq!(centered.height, 20);
367 assert_eq!(centered.x, 0);
368 assert_eq!(centered.y, 0);
369 }
370}