code_mesh_tui/
layout.rs

1use ratatui::{
2    layout::{Constraint, Direction, Layout, Rect},
3};
4
5/// Layout manager for organizing UI components
6#[derive(Debug, Clone)]
7pub struct LayoutManager {
8    /// Main content area
9    pub main_area: Rect,
10    /// Side panel area (optional)
11    pub side_panel: Option<Rect>,
12    /// Status bar area
13    pub status_area: Rect,
14    /// Input area
15    pub input_area: Rect,
16    /// Full terminal area
17    pub terminal_area: Rect,
18}
19
20impl LayoutManager {
21    /// Create a new layout manager for the given terminal area
22    pub fn new(area: Rect) -> Self {
23        let main_layout = Layout::default()
24            .direction(Direction::Vertical)
25            .constraints([
26                Constraint::Min(1),      // Main content
27                Constraint::Length(1),   // Status bar
28            ])
29            .split(area);
30
31        let content_layout = Layout::default()
32            .direction(Direction::Vertical)
33            .constraints([
34                Constraint::Min(1),      // Messages/content
35                Constraint::Length(3),   // Input area
36            ])
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    /// Create layout with side panel
49    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),      // Main content
54                Constraint::Length(1),   // Status bar
55            ])
56            .split(area);
57
58        let horizontal_layout = Layout::default()
59            .direction(Direction::Horizontal)
60            .constraints([
61                Constraint::Min(1),              // Main content
62                Constraint::Length(panel_width), // Side panel
63            ])
64            .split(main_layout[0]);
65
66        let content_layout = Layout::default()
67            .direction(Direction::Vertical)
68            .constraints([
69                Constraint::Min(1),      // Messages/content
70                Constraint::Length(3),   // Input area
71            ])
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    /// Update layout when terminal size changes
84    pub fn resize(&mut self, new_area: Rect) {
85        *self = if self.side_panel.is_some() {
86            // Preserve side panel if it existed
87            Self::with_side_panel(new_area, 40) // Default width
88        } else {
89            Self::new(new_area)
90        };
91    }
92}
93
94/// Popup layout for modals and dialogs
95#[derive(Debug, Clone)]
96pub struct PopupLayout;
97
98impl PopupLayout {
99    /// Create a centered popup area
100    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    /// Create a popup that takes a percentage of the screen
121    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/// Flexible layout system similar to CSS Flexbox
143#[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    /// Create a new flex layout
177    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    /// Set the flex direction
187    pub fn direction(mut self, direction: FlexDirection) -> Self {
188        self.direction = direction;
189        self
190    }
191
192    /// Set justify content
193    pub fn justify_content(mut self, justify: JustifyContent) -> Self {
194        self.justify_content = justify;
195        self
196    }
197
198    /// Set align items
199    pub fn align_items(mut self, align: AlignItems) -> Self {
200        self.align_items = align;
201        self
202    }
203
204    /// Enable/disable wrapping
205    pub fn wrap(mut self, wrap: bool) -> Self {
206        self.wrap = wrap;
207        self
208    }
209
210    /// Apply the flex layout to create ratatui constraints
211    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/// Flex item definition
240#[derive(Debug, Clone)]
241pub struct FlexItem {
242    pub flex: FlexBasis,
243    pub margin: Margin,
244    pub padding: Padding,
245}
246
247impl FlexItem {
248    /// Create a new flex item
249    pub fn new(flex: FlexBasis) -> Self {
250        Self {
251            flex,
252            margin: Margin::default(),
253            padding: Padding::default(),
254        }
255    }
256
257    /// Set margin
258    pub fn margin(mut self, margin: Margin) -> Self {
259        self.margin = margin;
260        self
261    }
262
263    /// Set padding
264    pub fn padding(mut self, padding: Padding) -> Self {
265        self.padding = padding;
266        self
267    }
268}
269
270/// Flex basis for sizing items
271#[derive(Debug, Clone, Copy)]
272pub enum FlexBasis {
273    /// Fixed size in characters
274    Fixed(u16),
275    /// Percentage of container
276    Percentage(u16),
277    /// Flex grow factor
278    Flex(f32),
279    /// Auto size based on content
280    Auto,
281}
282
283/// Margin definition
284#[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    /// Create uniform margin
294    pub fn uniform(size: u16) -> Self {
295        Self {
296            top: size,
297            right: size,
298            bottom: size,
299            left: size,
300        }
301    }
302
303    /// Create vertical/horizontal margin
304    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/// Padding definition
315#[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    /// Create uniform padding
325    pub fn uniform(size: u16) -> Self {
326        Self {
327            top: size,
328            right: size,
329            bottom: size,
330            left: size,
331        }
332    }
333
334    /// Create vertical/horizontal padding
335    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/// Grid layout system
346#[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    /// Create a new grid layout
363    pub fn new() -> Self {
364        Self {
365            rows: Vec::new(),
366            columns: Vec::new(),
367            gap: 0,
368        }
369    }
370
371    /// Set grid template rows
372    pub fn rows(mut self, rows: Vec<GridTrack>) -> Self {
373        self.rows = rows;
374        self
375    }
376
377    /// Set grid template columns
378    pub fn columns(mut self, columns: Vec<GridTrack>) -> Self {
379        self.columns = columns;
380        self
381    }
382
383    /// Set grid gap
384    pub fn gap(mut self, gap: u16) -> Self {
385        self.gap = gap;
386        self
387    }
388
389    /// Create the grid areas
390    pub fn areas(&self, container: Rect) -> Vec<Vec<Rect>> {
391        // This is a simplified grid implementation
392        // In a full implementation, this would handle complex grid logic
393        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        // Create row layout
414        let rows = Layout::default()
415            .direction(Direction::Vertical)
416            .constraints(row_constraints)
417            .split(container);
418
419        // Create column layouts for each row
420        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/// Responsive layout system that adapts to screen size
439#[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    /// Create a new responsive layout
460    pub fn new() -> Self {
461        let mut layout = Self {
462            breakpoints: Vec::new(),
463        };
464        
465        // Default breakpoints
466        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    /// Add a breakpoint
477    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    /// Get the appropriate layout for the given width
483    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    /// Apply the responsive layout
493    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                // For now, fallback to single column
519                // In a full implementation, we'd call the custom function
520                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); // Centered horizontally
555        assert_eq!(popup.y, 15); // Centered vertically
556    }
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        // Test small screen
579        let small_layout = layout.get_layout(50);
580        matches!(small_layout, ResponsiveLayoutType::SingleColumn);
581        
582        // Test medium screen
583        let medium_layout = layout.get_layout(90);
584        matches!(medium_layout, ResponsiveLayoutType::TwoColumn { .. });
585        
586        // Test large screen
587        let large_layout = layout.get_layout(130);
588        matches!(large_layout, ResponsiveLayoutType::ThreeColumn { .. });
589    }
590}