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