ascii_forge/widgets/
border.rs

1use std::ops::{Deref, DerefMut};
2
3use crate::prelude::*;
4
5/// A basic border type.
6/// Rendering this will put the next content inside of the function
7/// Borders will skip rendering if their size is under a 3x3
8pub struct Border {
9    pub size: Vec2,
10    pub horizontal: &'static str,
11    pub vertical: &'static str,
12    pub top_left: &'static str,
13    pub top_right: &'static str,
14    pub bottom_left: &'static str,
15    pub bottom_right: &'static str,
16
17    pub title: Option<Buffer>,
18
19    pub style: ContentStyle,
20}
21
22impl Deref for Border {
23    type Target = ContentStyle;
24    fn deref(&self) -> &Self::Target {
25        &self.style
26    }
27}
28
29impl DerefMut for Border {
30    fn deref_mut(&mut self) -> &mut Self::Target {
31        &mut self.style
32    }
33}
34
35impl Border {
36    pub const fn square(width: u16, height: u16) -> Border {
37        Border {
38            size: vec2(width, height),
39            horizontal: "─",
40            vertical: "│",
41            top_right: "┐",
42            top_left: "┌",
43            bottom_left: "└",
44            bottom_right: "┘",
45
46            title: None,
47
48            style: ContentStyle {
49                foreground_color: None,
50                background_color: None,
51                underline_color: None,
52                attributes: Attributes::none(),
53            },
54        }
55    }
56
57    pub const fn rounded(width: u16, height: u16) -> Border {
58        Border {
59            size: vec2(width, height),
60            horizontal: "─",
61            vertical: "│",
62            top_right: "╮",
63            top_left: "╭",
64            bottom_left: "╰",
65            bottom_right: "╯",
66
67            title: None,
68
69            style: ContentStyle {
70                foreground_color: None,
71                background_color: None,
72                underline_color: None,
73                attributes: Attributes::none(),
74            },
75        }
76    }
77
78    pub const fn thick(width: u16, height: u16) -> Border {
79        Border {
80            size: vec2(width, height),
81            horizontal: "━",
82            vertical: "┃",
83            top_right: "┓",
84            top_left: "┏",
85            bottom_left: "┗",
86            bottom_right: "┛",
87
88            title: None,
89
90            style: ContentStyle {
91                foreground_color: None,
92                background_color: None,
93                underline_color: None,
94                attributes: Attributes::none(),
95            },
96        }
97    }
98
99    pub const fn double(width: u16, height: u16) -> Border {
100        Border {
101            size: vec2(width, height),
102            horizontal: "═",
103            vertical: "║",
104            top_right: "╗",
105            top_left: "╔",
106            bottom_left: "╚",
107            bottom_right: "╝",
108
109            title: None,
110
111            style: ContentStyle {
112                foreground_color: None,
113                background_color: None,
114                underline_color: None,
115                attributes: Attributes::none(),
116            },
117        }
118    }
119
120    pub fn with_title(mut self, title: impl Render) -> Border {
121        let title_buf = Buffer::sized_element(title);
122        self.title = Some(title_buf);
123
124        self
125    }
126}
127
128impl Render for Border {
129    fn render(&self, loc: Vec2, buffer: &mut Buffer) -> Vec2 {
130        if self.size.x < 3 || self.size.y < 3 {
131            return loc;
132        }
133
134        // Fill the interior with spaces
135        for y in (loc.y + 1)..(loc.y + self.size.y.saturating_sub(1)) {
136            for x in (loc.x + 1)..(loc.x + self.size.x.saturating_sub(1)) {
137                buffer.set(vec2(x, y), " ");
138            }
139        }
140
141        // Render vertical sides with style
142        for y in (loc.y + 1)..(loc.y + self.size.y.saturating_sub(1)) {
143            buffer.set(
144                vec2(loc.x, y),
145                StyledContent::new(self.style, self.vertical),
146            );
147            buffer.set(
148                vec2(loc.x + self.size.x.saturating_sub(1), y),
149                StyledContent::new(self.style, self.vertical),
150            );
151        }
152
153        // Render top and bottom borders with style
154        let horizontal_repeat = self
155            .horizontal
156            .repeat(self.size.x.saturating_sub(2) as usize);
157        render!(buffer,
158            loc => [
159                StyledContent::new(self.style, self.top_left),
160                StyledContent::new(self.style, horizontal_repeat.as_str()),
161                StyledContent::new(self.style, self.top_right)
162            ],
163            vec2(loc.x, loc.y + self.size.y.saturating_sub(1)) => [
164                StyledContent::new(self.style, self.bottom_left),
165                StyledContent::new(self.style, self.horizontal.repeat(self.size.x.saturating_sub(2) as usize).as_str()),
166                StyledContent::new(self.style, self.bottom_right)
167            ]
168        );
169
170        // Render title with clipping to fit within the border width
171        if let Some(title) = &self.title {
172            let max_title_width = self.size.x.saturating_sub(2); // Account for corners
173            title.render_clipped(loc + vec2(1, 0), vec2(max_title_width, 1), buffer);
174        }
175
176        vec2(loc.x + 1, loc.y + 1)
177    }
178
179    fn size(&self) -> Vec2 {
180        self.size
181    }
182}
183
184#[cfg(test)]
185mod test {
186    use crate::{
187        math::Vec2,
188        render,
189        widgets::border::Border,
190        window::{Buffer, Render},
191    };
192
193    #[test]
194    fn render_small() {
195        let border = Border::square(0, 0);
196        // Ensure no panics
197        let _ = Buffer::sized_element(border);
198    }
199
200    #[test]
201    fn check_size() {
202        let border = Border::square(16, 16);
203        let mut buf = Buffer::new((80, 80));
204        render!(buf, (0, 0) => [ border ]);
205        buf.shrink();
206        assert_eq!(buf.size(), border.size())
207    }
208}