Skip to main content

retroglyph_ui/widget/
box_border.rs

1//! [`BoxBorder`]: a single-line box border.
2use retroglyph_core::color::{Color, Style};
3
4use super::{BorderType, Widget};
5use crate::Surface;
6use crate::theme::Theme;
7
8/// A single-line box border drawn around a [`Rect`](retroglyph_core::grid::Rect).
9///
10/// The interior of the rectangle is not touched. `area` must be at least
11/// 2×2, or [`Widget::render`] is a no-op. `style` defaults to [`Theme::DARK`] (as if
12/// [`BoxBorder::theme`] had been called); set it with [`BoxBorder::style`].
13///
14/// # Examples
15///
16/// ```
17/// use retroglyph_core::grid::{Grid, Rect};
18/// use retroglyph_ui::widget::{BoxBorder, Widget};
19/// use retroglyph_ui::Surface;
20///
21/// let area = Rect::new(0, 0, 10, 4);
22/// let mut grid = Grid::new(10, 4);
23/// BoxBorder::new().render(&mut Surface::new(&mut grid, area, 0));
24/// ```
25#[derive(Clone, Copy, Debug, Default)]
26pub struct BoxBorder {
27    style: Style,
28    border_type: BorderType,
29}
30
31impl BoxBorder {
32    /// A box border styled from [`Theme::DARK`] (as if [`BoxBorder::theme`] had been called); see
33    /// [`BoxBorder::style`] to override it.
34    #[must_use]
35    pub fn new() -> Self {
36        Self::default().theme(Theme::DARK)
37    }
38
39    /// Set the border's style.
40    #[must_use]
41    pub const fn style(mut self, style: Style) -> Self {
42        self.style = style;
43        self
44    }
45
46    /// Set which box-drawing glyphs the border is drawn with. Defaults to
47    /// [`BorderType::Plain`].
48    #[must_use]
49    pub const fn border_type(mut self, border_type: BorderType) -> Self {
50        self.border_type = border_type;
51        self
52    }
53
54    /// Sets `style` to `theme.border` on `theme.panel_bg`.
55    ///
56    /// The background is set explicitly for the same reason, and with the same caveat, as
57    /// [`super::Gauge::theme`]; see its doc comment for the full explanation. Unlike
58    /// [`super::Panel`], which also owns and fills its own interior, a standalone `BoxBorder`
59    /// genuinely doesn't know what it's drawn over: `theme.panel_bg` is the closest default,
60    /// matching what a themed [`super::Panel`]/[`super::Modal`] around it would use. Drawing this
61    /// border directly on the raw screen background instead needs a manual [`BoxBorder::style`]
62    /// override afterwards.
63    ///
64    /// Call before any manual [`BoxBorder::style`] override you want to keep.
65    #[must_use]
66    pub fn theme(self, theme: Theme) -> Self {
67        self.theme_on(theme, theme.panel_bg)
68    }
69
70    /// Same as [`BoxBorder::theme`], but `style` is drawn on `bg` instead of `theme.panel_bg` --
71    /// for a border drawn directly on a backdrop other than a themed [`super::Panel`]/
72    /// [`super::Modal`]'s fill. [`BoxBorder::theme`] is exactly `theme_on(theme, theme.panel_bg)`.
73    #[must_use]
74    pub fn theme_on(mut self, theme: Theme, bg: Color) -> Self {
75        self.style = Style::new().fg(theme.border).bg(bg);
76        self
77    }
78}
79
80impl Widget for BoxBorder {
81    fn render(&self, surface: &mut Surface<'_>) {
82        let (w, h) = (surface.width(), surface.height());
83        if w < 2 || h < 2 {
84            return;
85        }
86
87        let x1 = w - 1;
88        let y1 = h - 1;
89        let glyphs = self.border_type.glyphs();
90        let mut surface = surface.with_style(self.style);
91
92        // Corners
93        surface.put((0, 0), glyphs.top_left);
94        surface.put((x1, 0), glyphs.top_right);
95        surface.put((0, y1), glyphs.bottom_left);
96        surface.put((x1, y1), glyphs.bottom_right);
97
98        // Horizontal edges
99        for x in 1..x1 {
100            surface.put((x, 0), glyphs.horizontal);
101            surface.put((x, y1), glyphs.horizontal);
102        }
103
104        // Vertical edges
105        for y in 1..y1 {
106            surface.put((0, y), glyphs.vertical);
107            surface.put((x1, y), glyphs.vertical);
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use retroglyph_core::color::Color;
115    use retroglyph_core::grid::{Grid, Pos, Rect};
116    use retroglyph_core::symbols::border::PLAIN;
117
118    use super::*;
119
120    #[test]
121    fn draws_corners_and_edges() {
122        let area = Rect::new(0, 0, 5, 3);
123        let mut grid = Grid::new(5, 3);
124        BoxBorder::new()
125            .style(Style::new().fg(Color::WHITE))
126            .render(&mut Surface::new(&mut grid, area, 0));
127
128        assert_eq!(grid[Pos::new(0, 0)].glyph(), PLAIN.top_left);
129        assert_eq!(grid[Pos::new(4, 0)].glyph(), PLAIN.top_right);
130        assert_eq!(grid[Pos::new(0, 2)].glyph(), PLAIN.bottom_left);
131        assert_eq!(grid[Pos::new(4, 2)].glyph(), PLAIN.bottom_right);
132        assert_eq!(grid[Pos::new(2, 0)].glyph(), PLAIN.horizontal);
133        assert_eq!(grid[Pos::new(0, 1)].glyph(), PLAIN.vertical);
134        // Interior untouched.
135        assert_eq!(grid[Pos::new(2, 1)].glyph(), ' ');
136    }
137
138    #[test]
139    fn border_type_defaults_to_plain() {
140        let area = Rect::new(0, 0, 5, 3);
141        let mut grid = Grid::new(5, 3);
142        BoxBorder::new().render(&mut Surface::new(&mut grid, area, 0));
143
144        assert_eq!(grid[Pos::new(0, 0)].glyph(), PLAIN.top_left);
145    }
146
147    #[test]
148    fn border_type_selects_the_glyph_set() {
149        let area = Rect::new(0, 0, 5, 3);
150        let mut grid = Grid::new(5, 3);
151        BoxBorder::new()
152            .border_type(BorderType::Rounded)
153            .render(&mut Surface::new(&mut grid, area, 0));
154
155        assert_eq!(grid[Pos::new(0, 0)].glyph(), '╭');
156        assert_eq!(grid[Pos::new(4, 0)].glyph(), '╮');
157        assert_eq!(grid[Pos::new(0, 2)].glyph(), '╰');
158        assert_eq!(grid[Pos::new(4, 2)].glyph(), '╯');
159        assert_eq!(grid[Pos::new(2, 0)].glyph(), '─');
160        assert_eq!(grid[Pos::new(0, 1)].glyph(), '│');
161    }
162
163    #[test]
164    fn border_type_double() {
165        let area = Rect::new(0, 0, 5, 3);
166        let mut grid = Grid::new(5, 3);
167        BoxBorder::new()
168            .border_type(BorderType::Double)
169            .render(&mut Surface::new(&mut grid, area, 0));
170
171        assert_eq!(grid[Pos::new(0, 0)].glyph(), '╔');
172        assert_eq!(grid[Pos::new(4, 0)].glyph(), '╗');
173        assert_eq!(grid[Pos::new(0, 2)].glyph(), '╚');
174        assert_eq!(grid[Pos::new(4, 2)].glyph(), '╝');
175        assert_eq!(grid[Pos::new(2, 0)].glyph(), '═');
176        assert_eq!(grid[Pos::new(0, 1)].glyph(), '║');
177    }
178
179    #[test]
180    fn border_type_thick() {
181        let area = Rect::new(0, 0, 5, 3);
182        let mut grid = Grid::new(5, 3);
183        BoxBorder::new()
184            .border_type(BorderType::Thick)
185            .render(&mut Surface::new(&mut grid, area, 0));
186
187        assert_eq!(grid[Pos::new(0, 0)].glyph(), '┏');
188        assert_eq!(grid[Pos::new(4, 0)].glyph(), '┓');
189        assert_eq!(grid[Pos::new(0, 2)].glyph(), '┗');
190        assert_eq!(grid[Pos::new(4, 2)].glyph(), '┛');
191        assert_eq!(grid[Pos::new(2, 0)].glyph(), '━');
192        assert_eq!(grid[Pos::new(0, 1)].glyph(), '┃');
193    }
194
195    #[test]
196    fn too_small_is_a_no_op() {
197        let area = Rect::new(0, 0, 1, 1);
198        let mut grid = Grid::new(1, 1);
199        BoxBorder::new().render(&mut Surface::new(&mut grid, area, 0));
200        assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
201    }
202
203    #[test]
204    fn theme_maps_border_role_onto_style() {
205        let area = Rect::new(0, 0, 5, 3);
206        let mut grid = Grid::new(5, 3);
207        BoxBorder::new()
208            .theme(Theme::DARK)
209            .render(&mut Surface::new(&mut grid, area, 0));
210
211        assert_eq!(
212            grid[Pos::new(0, 0)].style().foreground(),
213            Theme::DARK.border
214        );
215        assert_eq!(
216            grid[Pos::new(0, 0)].style().background(),
217            Theme::DARK.panel_bg
218        );
219    }
220
221    #[test]
222    fn theme_on_uses_the_given_backdrop_instead_of_panel_bg() {
223        let area = Rect::new(0, 0, 5, 3);
224        let mut grid = Grid::new(5, 3);
225        BoxBorder::new()
226            .theme_on(Theme::DARK, Color::Default)
227            .render(&mut Surface::new(&mut grid, area, 0));
228
229        assert_eq!(
230            grid[Pos::new(0, 0)].style().foreground(),
231            Theme::DARK.border
232        );
233        assert_eq!(grid[Pos::new(0, 0)].style().background(), Color::Default);
234    }
235}