1use crate::{
8 layout::Rect,
9 renderer::Rendered,
10 theme::{
11 ColorMode,
12 Style,
13 },
14};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct Border {
19 pub left: char,
21 pub right: char,
23 pub top: char,
25 pub bottom: char,
27 pub top_left: char,
29 pub top_right: char,
31 pub bottom_left: char,
33 pub bottom_right: char,
35}
36
37impl Border {
38 pub const DOUBLE: Self = Self {
40 left: '║',
41 right: '║',
42 top: '═',
43 bottom: '═',
44 top_left: '╔',
45 top_right: '╗',
46 bottom_left: '╚',
47 bottom_right: '╝',
48 };
49 pub const LEFT: Self = Self {
51 left: '▐',
52 right: ' ',
53 top: ' ',
54 bottom: ' ',
55 top_left: '▐',
56 top_right: ' ',
57 bottom_left: '▐',
58 bottom_right: ' ',
59 };
60 pub const NONE: Self = Self {
62 left: ' ',
63 right: ' ',
64 top: ' ',
65 bottom: ' ',
66 top_left: ' ',
67 top_right: ' ',
68 bottom_left: ' ',
69 bottom_right: ' ',
70 };
71 pub const ROUNDED: Self = Self {
73 left: '│',
74 right: '│',
75 top: '─',
76 bottom: '─',
77 top_left: '╭',
78 top_right: '╮',
79 bottom_left: '╰',
80 bottom_right: '╯',
81 };
82 pub const THICK: Self = Self {
84 left: '┃',
85 right: '┃',
86 top: '━',
87 bottom: '━',
88 top_left: '┏',
89 top_right: '┓',
90 bottom_left: '┗',
91 bottom_right: '┛',
92 };
93 pub const THIN: Self = Self {
95 left: '│',
96 right: '│',
97 top: '─',
98 bottom: '─',
99 top_left: '┌',
100 top_right: '┐',
101 bottom_left: '└',
102 bottom_right: '┘',
103 };
104
105 pub fn size(&self) -> (u16, u16) {
109 let h = if self.left != ' ' || self.right != ' ' {
110 1
111 } else {
112 0
113 };
114 let v = if self.top != ' ' || self.bottom != ' ' {
115 1
116 } else {
117 0
118 };
119 (h, v)
120 }
121
122 pub fn inner(&self, rect: Rect) -> Rect {
124 let (h, v) = self.size();
125 Rect::new(
126 rect.x + h,
127 rect.y + v,
128 rect.width.saturating_sub(h * 2),
129 rect.height.saturating_sub(v * 2),
130 )
131 }
132}
133
134impl Default for Border {
135 fn default() -> Self {
136 Self::THIN
137 }
138}
139
140pub fn draw_border(target: &mut Rendered, rect: Rect, border: &Border, style: &Style) {
150 if rect.width == 0 || rect.height == 0 {
151 return;
152 }
153
154 let mode = ColorMode::detect();
155 let prefix = style.prefix(mode);
156 let suffix = Style::suffix();
157
158 let needed_rows = rect.y as usize + rect.height as usize;
159 while target.lines.len() < needed_rows {
160 target.lines.push(String::new());
161 }
162
163 let top = rect.y as usize;
164 let bottom = (rect.y + rect.height - 1) as usize;
165 let inner_width = rect.width.saturating_sub(2) as usize;
166 let interior = " ".repeat(inner_width);
167
168 let build_edge = |l: char, edge: char, r: char| -> String {
170 let mut line = String::new();
171 if l != ' ' {
172 line.push_str(&prefix);
173 line.push(l);
174 line.push_str(suffix);
175 }
176 if edge != ' ' && inner_width > 0 {
177 line.push_str(&prefix);
178 for _ in 0..inner_width {
179 line.push(edge);
180 }
181 line.push_str(suffix);
182 } else if inner_width > 0 {
183 line.push_str(&interior);
184 }
185 if r != ' ' && rect.width > 1 {
186 line.push_str(&prefix);
187 line.push(r);
188 line.push_str(suffix);
189 }
190 line
191 };
192
193 let build_side = |l: char, r: char| -> String {
195 let mut line = String::new();
196 if l != ' ' {
197 line.push_str(&prefix);
198 line.push(l);
199 line.push_str(suffix);
200 }
201 if inner_width > 0 {
202 line.push_str(&interior);
203 }
204 if r != ' ' && rect.width > 1 {
205 line.push_str(&prefix);
206 line.push(r);
207 line.push_str(suffix);
208 }
209 line
210 };
211
212 if rect.height >= 1 {
214 target.lines[top] = build_edge(border.top_left, border.top, border.top_right);
215 }
216
217 for y in (top + 1)..bottom {
219 target.lines[y] = build_side(border.left, border.right);
220 }
221
222 if rect.height >= 2 {
224 target.lines[bottom] = build_edge(border.bottom_left, border.bottom, border.bottom_right);
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::theme::{
232 Color,
233 Theme,
234 };
235
236 fn empty_rendered(_width: u16, height: u16) -> Rendered {
237 Rendered {
238 lines: vec![String::new(); height as usize],
239 cursor: None,
240 images: Vec::new(),
241 }
242 }
243
244 #[test]
245 fn thin_border_3x3() {
246 Theme::with(Theme::Light, || {
247 let mut target = empty_rendered(3, 3);
248 let rect = Rect::new(0, 0, 3, 3);
249 let style = Style::new().fg(Color::SUNBEAM_ORANGE);
250 draw_border(&mut target, rect, &Border::THIN, &style);
251
252 assert!(target.lines[0].contains('┌'));
253 assert!(target.lines[0].contains('┐'));
254 assert!(target.lines[1].contains('│'));
255 assert!(target.lines[2].contains('└'));
256 assert!(target.lines[2].contains('┘'));
257 });
258 }
259
260 #[test]
261 fn thin_border_5x3_with_title_space() {
262 Theme::with(Theme::Light, || {
263 let mut target = empty_rendered(5, 3);
264 let rect = Rect::new(0, 0, 5, 3);
265 let style = Style::new().fg(Color::WHITE);
266 draw_border(&mut target, rect, &Border::THIN, &style);
267
268 assert!(target.lines[0].contains('┌'));
270 assert!(target.lines[0].contains('┐'));
271 assert!(target.lines[1].contains('│'));
273 assert!(target.lines[2].contains('└'));
275 assert!(target.lines[2].contains('┘'));
276 });
277 }
278
279 #[test]
280 fn border_includes_ansi_codes() {
281 Theme::with(Theme::Light, || {
282 let mut target = empty_rendered(3, 3);
283 let rect = Rect::new(0, 0, 3, 3);
284 let style = Style::new().fg(Color::SUNBEAM_ORANGE);
285 draw_border(&mut target, rect, &Border::THIN, &style);
286
287 assert!(target.lines[0].starts_with('\x1b'));
289 assert!(target.lines[0].contains("\x1b[0m"));
290 });
291 }
292
293 #[test]
294 fn left_border_only() {
295 Theme::with(Theme::Light, || {
296 let mut target = empty_rendered(3, 3);
297 let rect = Rect::new(0, 0, 3, 3);
298 let style = Style::new().fg(Color::SUNBEAM_ORANGE);
299 draw_border(&mut target, rect, &Border::LEFT, &style);
300
301 assert!(target.lines[0].contains('▐'));
302 assert!(target.lines[1].contains('▐'));
303 assert!(target.lines[2].contains('▐'));
304 assert!(!target.lines[1].contains('│'));
306 });
307 }
308
309 #[test]
310 fn border_inner_rect() {
311 let border = Border::THIN;
312 let outer = Rect::new(0, 0, 10, 5);
313 let inner = border.inner(outer);
314 assert_eq!(inner.x, 1);
315 assert_eq!(inner.y, 1);
316 assert_eq!(inner.width, 8);
317 assert_eq!(inner.height, 3);
318 }
319
320 #[test]
321 fn empty_border_inner_is_full() {
322 let border = Border::NONE;
323 let outer = Rect::new(0, 0, 10, 5);
324 let inner = border.inner(outer);
325 assert_eq!(inner, outer);
326 }
327}