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