1use ratatui::{
2 layout::{Constraint, Direction, Layout, Rect},
3};
4
5#[derive(Debug, Clone)]
7pub struct LayoutManager {
8 pub main_area: Rect,
10 pub side_panel: Option<Rect>,
12 pub status_area: Rect,
14 pub input_area: Rect,
16 pub terminal_area: Rect,
18}
19
20impl LayoutManager {
21 pub fn new(area: Rect) -> Self {
23 let main_layout = Layout::default()
24 .direction(Direction::Vertical)
25 .constraints([
26 Constraint::Min(1), Constraint::Length(1), ])
29 .split(area);
30
31 let content_layout = Layout::default()
32 .direction(Direction::Vertical)
33 .constraints([
34 Constraint::Min(1), Constraint::Length(3), ])
37 .split(main_layout[0]);
38
39 Self {
40 main_area: content_layout[0],
41 side_panel: None,
42 status_area: main_layout[1],
43 input_area: content_layout[1],
44 terminal_area: area,
45 }
46 }
47
48 pub fn with_side_panel(area: Rect, panel_width: u16) -> Self {
50 let main_layout = Layout::default()
51 .direction(Direction::Vertical)
52 .constraints([
53 Constraint::Min(1), Constraint::Length(1), ])
56 .split(area);
57
58 let horizontal_layout = Layout::default()
59 .direction(Direction::Horizontal)
60 .constraints([
61 Constraint::Min(1), Constraint::Length(panel_width), ])
64 .split(main_layout[0]);
65
66 let content_layout = Layout::default()
67 .direction(Direction::Vertical)
68 .constraints([
69 Constraint::Min(1), Constraint::Length(3), ])
72 .split(horizontal_layout[0]);
73
74 Self {
75 main_area: content_layout[0],
76 side_panel: Some(horizontal_layout[1]),
77 status_area: main_layout[1],
78 input_area: content_layout[1],
79 terminal_area: area,
80 }
81 }
82
83 pub fn resize(&mut self, new_area: Rect) {
85 *self = if self.side_panel.is_some() {
86 Self::with_side_panel(new_area, 40) } else {
89 Self::new(new_area)
90 };
91 }
92}
93
94#[derive(Debug, Clone)]
96pub struct PopupLayout;
97
98impl PopupLayout {
99 pub fn centered(area: Rect, width: u16, height: u16) -> Rect {
101 let popup_layout = Layout::default()
102 .direction(Direction::Vertical)
103 .constraints([
104 Constraint::Length((area.height.saturating_sub(height)) / 2),
105 Constraint::Length(height),
106 Constraint::Length((area.height.saturating_sub(height)) / 2),
107 ])
108 .split(area);
109
110 Layout::default()
111 .direction(Direction::Horizontal)
112 .constraints([
113 Constraint::Length((area.width.saturating_sub(width)) / 2),
114 Constraint::Length(width),
115 Constraint::Length((area.width.saturating_sub(width)) / 2),
116 ])
117 .split(popup_layout[1])[1]
118 }
119
120 pub fn percentage(area: Rect, width_percent: u16, height_percent: u16) -> Rect {
122 let popup_layout = Layout::default()
123 .direction(Direction::Vertical)
124 .constraints([
125 Constraint::Percentage((100 - height_percent) / 2),
126 Constraint::Percentage(height_percent),
127 Constraint::Percentage((100 - height_percent) / 2),
128 ])
129 .split(area);
130
131 Layout::default()
132 .direction(Direction::Horizontal)
133 .constraints([
134 Constraint::Percentage((100 - width_percent) / 2),
135 Constraint::Percentage(width_percent),
136 Constraint::Percentage((100 - width_percent) / 2),
137 ])
138 .split(popup_layout[1])[1]
139 }
140}
141
142#[derive(Debug, Clone)]
144pub struct FlexLayout {
145 direction: FlexDirection,
146 justify_content: JustifyContent,
147 align_items: AlignItems,
148 wrap: bool,
149}
150
151#[derive(Debug, Clone, Copy)]
152pub enum FlexDirection {
153 Row,
154 Column,
155}
156
157#[derive(Debug, Clone, Copy)]
158pub enum JustifyContent {
159 FlexStart,
160 FlexEnd,
161 Center,
162 SpaceBetween,
163 SpaceAround,
164 SpaceEvenly,
165}
166
167#[derive(Debug, Clone, Copy)]
168pub enum AlignItems {
169 FlexStart,
170 FlexEnd,
171 Center,
172 Stretch,
173}
174
175impl FlexLayout {
176 pub fn new() -> Self {
178 Self {
179 direction: FlexDirection::Column,
180 justify_content: JustifyContent::FlexStart,
181 align_items: AlignItems::Stretch,
182 wrap: false,
183 }
184 }
185
186 pub fn direction(mut self, direction: FlexDirection) -> Self {
188 self.direction = direction;
189 self
190 }
191
192 pub fn justify_content(mut self, justify: JustifyContent) -> Self {
194 self.justify_content = justify;
195 self
196 }
197
198 pub fn align_items(mut self, align: AlignItems) -> Self {
200 self.align_items = align;
201 self
202 }
203
204 pub fn wrap(mut self, wrap: bool) -> Self {
206 self.wrap = wrap;
207 self
208 }
209
210 pub fn apply(&self, items: &[FlexItem]) -> Vec<Constraint> {
212 let mut constraints = Vec::new();
213
214 for item in items {
215 match item.flex {
216 FlexBasis::Fixed(size) => constraints.push(Constraint::Length(size)),
217 FlexBasis::Percentage(percent) => constraints.push(Constraint::Percentage(percent)),
218 FlexBasis::Flex(flex) => {
219 if flex == 1 {
220 constraints.push(Constraint::Min(1));
221 } else {
222 constraints.push(Constraint::Ratio(flex as u32, items.len() as u32));
223 }
224 }
225 FlexBasis::Auto => constraints.push(Constraint::Min(1)),
226 }
227 }
228
229 constraints
230 }
231}
232
233impl Default for FlexLayout {
234 fn default() -> Self {
235 Self::new()
236 }
237}
238
239#[derive(Debug, Clone)]
241pub struct FlexItem {
242 pub flex: FlexBasis,
243 pub margin: Margin,
244 pub padding: Padding,
245}
246
247impl FlexItem {
248 pub fn new(flex: FlexBasis) -> Self {
250 Self {
251 flex,
252 margin: Margin::default(),
253 padding: Padding::default(),
254 }
255 }
256
257 pub fn margin(mut self, margin: Margin) -> Self {
259 self.margin = margin;
260 self
261 }
262
263 pub fn padding(mut self, padding: Padding) -> Self {
265 self.padding = padding;
266 self
267 }
268}
269
270#[derive(Debug, Clone, Copy)]
272pub enum FlexBasis {
273 Fixed(u16),
275 Percentage(u16),
277 Flex(f32),
279 Auto,
281}
282
283#[derive(Debug, Clone, Copy, Default)]
285pub struct Margin {
286 pub top: u16,
287 pub right: u16,
288 pub bottom: u16,
289 pub left: u16,
290}
291
292impl Margin {
293 pub fn uniform(size: u16) -> Self {
295 Self {
296 top: size,
297 right: size,
298 bottom: size,
299 left: size,
300 }
301 }
302
303 pub fn vh(vertical: u16, horizontal: u16) -> Self {
305 Self {
306 top: vertical,
307 right: horizontal,
308 bottom: vertical,
309 left: horizontal,
310 }
311 }
312}
313
314#[derive(Debug, Clone, Copy, Default)]
316pub struct Padding {
317 pub top: u16,
318 pub right: u16,
319 pub bottom: u16,
320 pub left: u16,
321}
322
323impl Padding {
324 pub fn uniform(size: u16) -> Self {
326 Self {
327 top: size,
328 right: size,
329 bottom: size,
330 left: size,
331 }
332 }
333
334 pub fn vh(vertical: u16, horizontal: u16) -> Self {
336 Self {
337 top: vertical,
338 right: horizontal,
339 bottom: vertical,
340 left: horizontal,
341 }
342 }
343}
344
345#[derive(Debug, Clone)]
347pub struct GridLayout {
348 rows: Vec<GridTrack>,
349 columns: Vec<GridTrack>,
350 gap: u16,
351}
352
353#[derive(Debug, Clone)]
354pub enum GridTrack {
355 Fixed(u16),
356 Fraction(u16),
357 Auto,
358 MinMax(u16, u16),
359}
360
361impl GridLayout {
362 pub fn new() -> Self {
364 Self {
365 rows: Vec::new(),
366 columns: Vec::new(),
367 gap: 0,
368 }
369 }
370
371 pub fn rows(mut self, rows: Vec<GridTrack>) -> Self {
373 self.rows = rows;
374 self
375 }
376
377 pub fn columns(mut self, columns: Vec<GridTrack>) -> Self {
379 self.columns = columns;
380 self
381 }
382
383 pub fn gap(mut self, gap: u16) -> Self {
385 self.gap = gap;
386 self
387 }
388
389 pub fn areas(&self, container: Rect) -> Vec<Vec<Rect>> {
391 let mut areas = Vec::new();
394
395 let row_constraints: Vec<Constraint> = self.rows.iter().map(|track| {
396 match track {
397 GridTrack::Fixed(size) => Constraint::Length(*size),
398 GridTrack::Fraction(fr) => Constraint::Ratio(*fr as u32, self.rows.len() as u32),
399 GridTrack::Auto => Constraint::Min(1),
400 GridTrack::MinMax(min, _max) => Constraint::Min(*min),
401 }
402 }).collect();
403
404 let col_constraints: Vec<Constraint> = self.columns.iter().map(|track| {
405 match track {
406 GridTrack::Fixed(size) => Constraint::Length(*size),
407 GridTrack::Fraction(fr) => Constraint::Ratio(*fr as u32, self.columns.len() as u32),
408 GridTrack::Auto => Constraint::Min(1),
409 GridTrack::MinMax(min, _max) => Constraint::Min(*min),
410 }
411 }).collect();
412
413 let rows = Layout::default()
415 .direction(Direction::Vertical)
416 .constraints(row_constraints)
417 .split(container);
418
419 for row_area in rows {
421 let columns = Layout::default()
422 .direction(Direction::Horizontal)
423 .constraints(col_constraints.clone())
424 .split(row_area);
425 areas.push(columns);
426 }
427
428 areas
429 }
430}
431
432impl Default for GridLayout {
433 fn default() -> Self {
434 Self::new()
435 }
436}
437
438#[derive(Debug, Clone)]
440pub struct ResponsiveLayout {
441 breakpoints: Vec<Breakpoint>,
442}
443
444#[derive(Debug, Clone)]
445pub struct Breakpoint {
446 pub min_width: u16,
447 pub layout: ResponsiveLayoutType,
448}
449
450#[derive(Debug, Clone)]
451pub enum ResponsiveLayoutType {
452 SingleColumn,
453 TwoColumn { left_width: u16 },
454 ThreeColumn { left_width: u16, right_width: u16 },
455 Custom(Box<dyn Fn(Rect) -> Vec<Rect> + Send + Sync>),
456}
457
458impl ResponsiveLayout {
459 pub fn new() -> Self {
461 let mut layout = Self {
462 breakpoints: Vec::new(),
463 };
464
465 layout.add_breakpoint(0, ResponsiveLayoutType::SingleColumn);
467 layout.add_breakpoint(80, ResponsiveLayoutType::TwoColumn { left_width: 40 });
468 layout.add_breakpoint(120, ResponsiveLayoutType::ThreeColumn {
469 left_width: 30,
470 right_width: 30
471 });
472
473 layout
474 }
475
476 pub fn add_breakpoint(&mut self, min_width: u16, layout: ResponsiveLayoutType) {
478 self.breakpoints.push(Breakpoint { min_width, layout });
479 self.breakpoints.sort_by_key(|bp| bp.min_width);
480 }
481
482 pub fn get_layout(&self, width: u16) -> &ResponsiveLayoutType {
484 for breakpoint in self.breakpoints.iter().rev() {
485 if width >= breakpoint.min_width {
486 return &breakpoint.layout;
487 }
488 }
489 &self.breakpoints[0].layout
490 }
491
492 pub fn apply(&self, area: Rect) -> Vec<Rect> {
494 let layout_type = self.get_layout(area.width);
495
496 match layout_type {
497 ResponsiveLayoutType::SingleColumn => vec![area],
498 ResponsiveLayoutType::TwoColumn { left_width } => {
499 Layout::default()
500 .direction(Direction::Horizontal)
501 .constraints([
502 Constraint::Length(*left_width),
503 Constraint::Min(1),
504 ])
505 .split(area)
506 },
507 ResponsiveLayoutType::ThreeColumn { left_width, right_width } => {
508 Layout::default()
509 .direction(Direction::Horizontal)
510 .constraints([
511 Constraint::Length(*left_width),
512 Constraint::Min(1),
513 Constraint::Length(*right_width),
514 ])
515 .split(area)
516 },
517 ResponsiveLayoutType::Custom(_custom_fn) => {
518 vec![area]
521 },
522 }
523 }
524}
525
526impl Default for ResponsiveLayout {
527 fn default() -> Self {
528 Self::new()
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
537 fn test_layout_manager_creation() {
538 let area = Rect::new(0, 0, 100, 50);
539 let layout = LayoutManager::new(area);
540
541 assert_eq!(layout.terminal_area, area);
542 assert!(layout.main_area.height > 0);
543 assert!(layout.status_area.height == 1);
544 assert!(layout.input_area.height == 3);
545 }
546
547 #[test]
548 fn test_popup_centered() {
549 let area = Rect::new(0, 0, 100, 50);
550 let popup = PopupLayout::centered(area, 50, 20);
551
552 assert_eq!(popup.width, 50);
553 assert_eq!(popup.height, 20);
554 assert_eq!(popup.x, 25); assert_eq!(popup.y, 15); }
557
558 #[test]
559 fn test_flex_layout() {
560 let flex = FlexLayout::new()
561 .direction(FlexDirection::Row)
562 .justify_content(JustifyContent::SpaceBetween);
563
564 let items = vec![
565 FlexItem::new(FlexBasis::Fixed(20)),
566 FlexItem::new(FlexBasis::Flex(1.0)),
567 FlexItem::new(FlexBasis::Fixed(30)),
568 ];
569
570 let constraints = flex.apply(&items);
571 assert_eq!(constraints.len(), 3);
572 }
573
574 #[test]
575 fn test_responsive_layout() {
576 let layout = ResponsiveLayout::new();
577
578 let small_layout = layout.get_layout(50);
580 matches!(small_layout, ResponsiveLayoutType::SingleColumn);
581
582 let medium_layout = layout.get_layout(90);
584 matches!(medium_layout, ResponsiveLayoutType::TwoColumn { .. });
585
586 let large_layout = layout.get_layout(130);
588 matches!(large_layout, ResponsiveLayoutType::ThreeColumn { .. });
589 }
590}