1use crate::components::ComponentDimensions;
2use crate::model::muxbox::MuxBox;
3use crate::pty_manager::PtyManager;
4use crate::{Bounds, Cell, ScreenBuffer};
5
6pub 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 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 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, 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 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 pub fn with_resize_enabled(mut self, enabled: bool) -> Self {
121 self.resize_enabled = enabled;
122 self
123 }
124
125 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 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 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 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 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 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 pub fn calculate_border_color(&self) -> String {
241 if self.pty_enabled {
242 "14".to_string() } else if self.dead_state || self.error_state {
244 "9".to_string() } else {
246 self.color
247 .map_or_else(|| "7".to_string(), |c| c.to_string()) }
249 }
250
251 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 x as usize == bounds.right() && y as usize == bounds.bottom()
259 }
260
261 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 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 #[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 assert!(border.is_border_area(&bounds, 2, 1)); assert!(border.is_border_area(&bounds, 8, 1)); assert!(border.is_border_area(&bounds, 2, 4)); assert!(border.is_border_area(&bounds, 8, 4)); assert!(border.is_border_area(&bounds, 5, 1)); assert!(border.is_border_area(&bounds, 5, 4)); assert!(border.is_border_area(&bounds, 2, 2)); assert!(border.is_border_area(&bounds, 8, 2)); assert!(!border.is_border_area(&bounds, 5, 2));
339 }
340}