ricecoder_tui/
layout.rs

1//! Terminal layout management
2
3/// Layout constraints
4#[derive(Debug, Clone, Copy)]
5pub struct Constraint {
6    /// Percentage of available space
7    pub percentage: u16,
8}
9
10impl Constraint {
11    /// Create a constraint with a percentage
12    pub fn percentage(percentage: u16) -> Self {
13        Self { percentage }
14    }
15
16    /// Create a constraint for fixed size
17    pub fn fixed(size: u16) -> Self {
18        Self { percentage: size }
19    }
20
21    /// Create a constraint for minimum size
22    pub fn min(size: u16) -> Self {
23        Self { percentage: size }
24    }
25}
26
27/// Layout direction
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum Direction {
30    /// Horizontal layout
31    Horizontal,
32    /// Vertical layout
33    Vertical,
34}
35
36/// Rect represents a rectangular area
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct Rect {
39    /// X coordinate
40    pub x: u16,
41    /// Y coordinate
42    pub y: u16,
43    /// Width
44    pub width: u16,
45    /// Height
46    pub height: u16,
47}
48
49impl Rect {
50    /// Create a new rect
51    pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
52        Self {
53            x,
54            y,
55            width,
56            height,
57        }
58    }
59
60    /// Get the right edge
61    pub const fn right(&self) -> u16 {
62        self.x.saturating_add(self.width)
63    }
64
65    /// Get the bottom edge
66    pub const fn bottom(&self) -> u16 {
67        self.y.saturating_add(self.height)
68    }
69
70    /// Check if rect is empty
71    pub const fn is_empty(&self) -> bool {
72        self.width == 0 || self.height == 0
73    }
74}
75
76/// Layout manager
77pub struct Layout {
78    /// Terminal width
79    pub width: u16,
80    /// Terminal height
81    pub height: u16,
82}
83
84impl Layout {
85    /// Create a new layout
86    pub fn new(width: u16, height: u16) -> Self {
87        Self { width, height }
88    }
89
90    /// Check if terminal size is valid (minimum 80x24)
91    pub fn is_valid(&self) -> bool {
92        self.width >= 80 && self.height >= 24
93    }
94
95    /// Get the main content area
96    pub fn content_area(&self) -> Rect {
97        Rect::new(0, 0, self.width, self.height.saturating_sub(3))
98    }
99
100    /// Get the input area (bottom 3 lines)
101    pub fn input_area(&self) -> Rect {
102        let input_height = 3;
103        let y = self.height.saturating_sub(input_height);
104        Rect::new(0, y, self.width, input_height)
105    }
106
107    /// Split a rect vertically
108    pub fn split_vertical(&self, rect: Rect, constraints: &[Constraint]) -> Vec<Rect> {
109        if constraints.is_empty() {
110            return vec![rect];
111        }
112
113        let mut rects = Vec::new();
114        let mut y = rect.y;
115
116        for constraint in constraints {
117            let height = (rect.height as u32 * constraint.percentage as u32 / 100) as u16;
118            rects.push(Rect::new(rect.x, y, rect.width, height));
119            y = y.saturating_add(height);
120        }
121
122        rects
123    }
124
125    /// Split a rect horizontally
126    pub fn split_horizontal(&self, rect: Rect, constraints: &[Constraint]) -> Vec<Rect> {
127        if constraints.is_empty() {
128            return vec![rect];
129        }
130
131        let mut rects = Vec::new();
132        let mut x = rect.x;
133
134        for constraint in constraints {
135            let width = (rect.width as u32 * constraint.percentage as u32 / 100) as u16;
136            rects.push(Rect::new(x, rect.y, width, rect.height));
137            x = x.saturating_add(width);
138        }
139
140        rects
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_rect_creation() {
150        let rect = Rect::new(0, 0, 80, 24);
151        assert_eq!(rect.x, 0);
152        assert_eq!(rect.y, 0);
153        assert_eq!(rect.width, 80);
154        assert_eq!(rect.height, 24);
155    }
156
157    #[test]
158    fn test_rect_edges() {
159        let rect = Rect::new(10, 5, 20, 15);
160        assert_eq!(rect.right(), 30);
161        assert_eq!(rect.bottom(), 20);
162    }
163
164    #[test]
165    fn test_layout_valid() {
166        let layout = Layout::new(80, 24);
167        assert!(layout.is_valid());
168
169        let layout = Layout::new(79, 24);
170        assert!(!layout.is_valid());
171
172        let layout = Layout::new(80, 23);
173        assert!(!layout.is_valid());
174    }
175
176    #[test]
177    fn test_layout_areas() {
178        let layout = Layout::new(80, 24);
179        let content = layout.content_area();
180        assert_eq!(content.height, 21);
181
182        let input = layout.input_area();
183        assert_eq!(input.height, 3);
184        assert_eq!(input.y, 21);
185    }
186
187    #[test]
188    fn test_split_vertical() {
189        let layout = Layout::new(80, 24);
190        let rect = Rect::new(0, 0, 80, 20);
191        let constraints = vec![Constraint::percentage(50), Constraint::percentage(50)];
192        let rects = layout.split_vertical(rect, &constraints);
193
194        assert_eq!(rects.len(), 2);
195        assert_eq!(rects[0].height, 10);
196        assert_eq!(rects[1].height, 10);
197    }
198
199    #[test]
200    fn test_split_horizontal() {
201        let layout = Layout::new(80, 24);
202        let rect = Rect::new(0, 0, 80, 20);
203        let constraints = vec![Constraint::percentage(30), Constraint::percentage(70)];
204        let rects = layout.split_horizontal(rect, &constraints);
205
206        assert_eq!(rects.len(), 2);
207        assert_eq!(rects[0].width, 24);
208        assert_eq!(rects[1].width, 56);
209    }
210}