Skip to main content

boxmux_lib/components/
border.rs

1use crate::components::ComponentDimensions;
2use crate::model::muxbox::MuxBox;
3use crate::pty_manager::PtyManager;
4use crate::{Bounds, Cell, ScreenBuffer};
5
6/// Border component for rendering box borders with various styles and states
7pub struct Border {
8    pub style: BorderStyle,
9    pub color: Option<u8>,
10    pub bg_color: Option<u8>,
11    pub resize_enabled: bool,
12    pub pty_enabled: bool,
13    pub error_state: bool,
14    pub dead_state: bool,
15}
16
17#[derive(Debug, Clone, PartialEq, Default)]
18pub enum BorderStyle {
19    #[default]
20    Single,
21    Double,
22    Thick,
23    Rounded,
24    Custom(BorderCharSet),
25}
26
27#[derive(Debug, Clone, PartialEq)]
28pub struct BorderCharSet {
29    pub top_left: char,
30    pub top_right: char,
31    pub bottom_left: char,
32    pub bottom_right: char,
33    pub horizontal: char,
34    pub vertical: char,
35    pub resize_knob: char,
36}
37
38impl BorderStyle {
39    pub fn get_charset(&self) -> BorderCharSet {
40        match self {
41            BorderStyle::Single => BorderCharSet {
42                top_left: '┌',
43                top_right: '┐',
44                bottom_left: '└',
45                bottom_right: '┘',
46                horizontal: '─',
47                vertical: '│',
48                resize_knob: '⋱',
49            },
50            BorderStyle::Double => BorderCharSet {
51                top_left: '╔',
52                top_right: '╗',
53                bottom_left: '╚',
54                bottom_right: '╝',
55                horizontal: '═',
56                vertical: '║',
57                resize_knob: '⋱',
58            },
59            BorderStyle::Thick => BorderCharSet {
60                top_left: '┏',
61                top_right: '┓',
62                bottom_left: '┗',
63                bottom_right: '┛',
64                horizontal: '━',
65                vertical: '┃',
66                resize_knob: '⋱',
67            },
68            BorderStyle::Rounded => BorderCharSet {
69                top_left: '╭',
70                top_right: '╮',
71                bottom_left: '╰',
72                bottom_right: '╯',
73                horizontal: '─',
74                vertical: '│',
75                resize_knob: '⋱',
76            },
77            BorderStyle::Custom(charset) => charset.clone(),
78        }
79    }
80}
81
82impl Border {
83    /// Create a new Border component from MuxBox configuration
84    pub fn from_muxbox(muxbox: &MuxBox, pty_manager: &PtyManager, locked: bool) -> Self {
85        let error_state = pty_manager.is_pty_in_error_state(&muxbox.id);
86        let dead_state = pty_manager.is_pty_dead(&muxbox.id);
87
88        // Parse color strings to u8 values if present
89        let border_color_u8 = muxbox
90            .border_color
91            .as_ref()
92            .and_then(|s| s.parse::<u8>().ok());
93        let bg_color_u8 = muxbox.bg_color.as_ref().and_then(|s| s.parse::<u8>().ok());
94
95        Self {
96            style: BorderStyle::Single, // TODO: Make configurable from muxbox
97            color: border_color_u8,
98            bg_color: bg_color_u8,
99            resize_enabled: !locked,
100            pty_enabled: matches!(muxbox.execution_mode, crate::ExecutionMode::Pty),
101            error_state,
102            dead_state,
103        }
104    }
105
106    /// Create a Border with custom configuration
107    pub fn new(style: BorderStyle, color: Option<u8>, bg_color: Option<u8>) -> Self {
108        Self {
109            style,
110            color,
111            bg_color,
112            resize_enabled: false,
113            pty_enabled: false,
114            error_state: false,
115            dead_state: false,
116        }
117    }
118
119    /// Set resize knob visibility
120    pub fn with_resize_enabled(mut self, enabled: bool) -> Self {
121        self.resize_enabled = enabled;
122        self
123    }
124
125    /// Set PTY state for color determination
126    pub fn with_pty_state(
127        mut self,
128        pty_enabled: bool,
129        error_state: bool,
130        dead_state: bool,
131    ) -> Self {
132        self.pty_enabled = pty_enabled;
133        self.error_state = error_state;
134        self.dead_state = dead_state;
135        self
136    }
137
138    /// Draw the border to the screen buffer
139    pub fn draw(&self, bounds: &Bounds, buffer: &mut ScreenBuffer) {
140        let charset = self.style.get_charset();
141        let border_color = self.calculate_border_color();
142        let bg_color = self
143            .bg_color
144            .map_or_else(|| "0".to_string(), |c| c.to_string());
145
146        // Draw corners
147        buffer.update(
148            bounds.left(),
149            bounds.top(),
150            Cell {
151                ch: charset.top_left,
152                fg_color: border_color.clone(),
153                bg_color: bg_color.clone(),
154            },
155        );
156
157        buffer.update(
158            bounds.right(),
159            bounds.top(),
160            Cell {
161                ch: charset.top_right,
162                fg_color: border_color.clone(),
163                bg_color: bg_color.clone(),
164            },
165        );
166
167        buffer.update(
168            bounds.left(),
169            bounds.bottom(),
170            Cell {
171                ch: charset.bottom_left,
172                fg_color: border_color.clone(),
173                bg_color: bg_color.clone(),
174            },
175        );
176
177        // Bottom right corner - resize knob or normal corner
178        buffer.update(
179            bounds.right(),
180            bounds.bottom(),
181            Cell {
182                ch: if self.resize_enabled {
183                    charset.resize_knob
184                } else {
185                    charset.bottom_right
186                },
187                fg_color: border_color.clone(),
188                bg_color: bg_color.clone(),
189            },
190        );
191
192        // Draw horizontal edges
193        let component_dims = ComponentDimensions::new(*bounds);
194        let inside_border = component_dims.inside_border_bounds();
195        for x in inside_border.left()..bounds.right() {
196            buffer.update(
197                x,
198                bounds.top(),
199                Cell {
200                    ch: charset.horizontal,
201                    fg_color: border_color.clone(),
202                    bg_color: bg_color.clone(),
203                },
204            );
205            buffer.update(
206                x,
207                bounds.bottom(),
208                Cell {
209                    ch: charset.horizontal,
210                    fg_color: border_color.clone(),
211                    bg_color: bg_color.clone(),
212                },
213            );
214        }
215
216        // Draw vertical edges
217        for y in inside_border.top()..bounds.bottom() {
218            buffer.update(
219                bounds.left(),
220                y,
221                Cell {
222                    ch: charset.vertical,
223                    fg_color: border_color.clone(),
224                    bg_color: bg_color.clone(),
225                },
226            );
227            buffer.update(
228                bounds.right(),
229                y,
230                Cell {
231                    ch: charset.vertical,
232                    fg_color: border_color.clone(),
233                    bg_color: bg_color.clone(),
234                },
235            );
236        }
237    }
238
239    /// Calculate border color based on state - returns color string
240    pub fn calculate_border_color(&self) -> String {
241        if self.pty_enabled {
242            "14".to_string() // Bright Cyan for PTY
243        } else if self.dead_state || self.error_state {
244            "9".to_string() // Bright Red for errors
245        } else {
246            self.color
247                .map_or_else(|| "7".to_string(), |c| c.to_string()) // Default or configured color
248        }
249    }
250
251    /// Check if coordinates are within the resize knob area
252    pub fn is_resize_knob_area(&self, bounds: &Bounds, x: u16, y: u16) -> bool {
253        if !self.resize_enabled {
254            return false;
255        }
256
257        // Resize knob is at bottom-right corner
258        x as usize == bounds.right() && y as usize == bounds.bottom()
259    }
260
261    /// Check if coordinates are on the border
262    pub fn is_border_area(&self, bounds: &Bounds, x: u16, y: u16) -> bool {
263        let on_top = y as usize == bounds.top()
264            && x as usize >= bounds.left()
265            && x as usize <= bounds.right();
266        let on_bottom = y as usize == bounds.bottom()
267            && x as usize >= bounds.left()
268            && x as usize <= bounds.right();
269        let on_left = x as usize == bounds.left()
270            && y as usize >= bounds.top()
271            && y as usize <= bounds.bottom();
272        let on_right = x as usize == bounds.right()
273            && y as usize >= bounds.top()
274            && y as usize <= bounds.bottom();
275
276        on_top || on_bottom || on_left || on_right
277    }
278
279    /// Check if coordinates are on the top border (for dragging)
280    pub fn is_top_border_area(&self, bounds: &Bounds, x: u16, y: u16) -> bool {
281        y as usize == bounds.top() && x as usize >= bounds.left() && x as usize <= bounds.right()
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    // Tests use direct crate references
289
290    #[test]
291    fn test_border_creation() {
292        let border = Border::new(BorderStyle::Single, Some(7), Some(0));
293        assert_eq!(border.style, BorderStyle::Single);
294        assert_eq!(border.color, Some(7));
295        assert_eq!(border.bg_color, Some(0));
296    }
297
298    #[test]
299    fn test_border_style_charset() {
300        let single = BorderStyle::Single.get_charset();
301        assert_eq!(single.top_left, '┌');
302        assert_eq!(single.horizontal, '─');
303
304        let double = BorderStyle::Double.get_charset();
305        assert_eq!(double.top_left, '╔');
306        assert_eq!(double.horizontal, '═');
307    }
308
309    #[test]
310    fn test_resize_knob_detection() {
311        let border = Border::new(BorderStyle::Single, None, None).with_resize_enabled(true);
312
313        let bounds = Bounds::new(0, 0, 10, 5);
314
315        assert!(border.is_resize_knob_area(&bounds, 10, 5));
316        assert!(!border.is_resize_knob_area(&bounds, 0, 0));
317        assert!(!border.is_resize_knob_area(&bounds, 5, 2));
318    }
319
320    #[test]
321    fn test_border_area_detection() {
322        let border = Border::new(BorderStyle::Single, None, None);
323        let bounds = Bounds::new(2, 1, 8, 4);
324
325        // Corners
326        assert!(border.is_border_area(&bounds, 2, 1)); // top-left
327        assert!(border.is_border_area(&bounds, 8, 1)); // top-right
328        assert!(border.is_border_area(&bounds, 2, 4)); // bottom-left
329        assert!(border.is_border_area(&bounds, 8, 4)); // bottom-right
330
331        // Edges
332        assert!(border.is_border_area(&bounds, 5, 1)); // top edge
333        assert!(border.is_border_area(&bounds, 5, 4)); // bottom edge
334        assert!(border.is_border_area(&bounds, 2, 2)); // left edge
335        assert!(border.is_border_area(&bounds, 8, 2)); // right edge
336
337        // Interior (should not be border)
338        assert!(!border.is_border_area(&bounds, 5, 2));
339    }
340}