Skip to main content

retroglyph_widgets/widget/
box_border.rs

1//! [`BoxBorder`]: a single-line box border.
2use retroglyph_core::{Color, Rect, Style};
3
4use super::Widget;
5use crate::Surface;
6use crate::Theme;
7use crate::draw::{BL, BR, H, TL, TR, V};
8
9/// A single-line box border drawn around a [`Rect`].
10///
11/// The interior of the rectangle is not touched. `area` must be at least
12/// 2×2, or [`Widget::render`] is a no-op. `style` defaults to
13/// [`Style::new()`]; set it with [`BoxBorder::style`].
14///
15/// # Examples
16///
17/// ```
18/// use retroglyph_core::{Grid, Rect};
19/// use retroglyph_widgets::{BoxBorder, Surface, Widget};
20///
21/// let area = Rect::new(0, 0, 10, 4);
22/// let mut grid = Grid::new(10, 4);
23/// BoxBorder::new().render(area, &mut Surface::new(&mut grid, area, 0));
24/// ```
25#[derive(Clone, Copy, Debug, Default)]
26pub struct BoxBorder {
27    style: Style,
28}
29
30impl BoxBorder {
31    /// A plain box border; see [`BoxBorder::style`] to color it.
32    #[must_use]
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    /// Set the border's style.
38    #[must_use]
39    pub const fn style(mut self, style: Style) -> Self {
40        self.style = style;
41        self
42    }
43
44    /// Sets `style` to `theme.border` on `theme.panel_bg`.
45    ///
46    /// The background is set explicitly rather than left at [`Style::new()`]'s default: an unset
47    /// background isn't "transparent" once a real backend draws it (a bare `Color::Default` cell
48    /// paints as solid black behind the glyph, not whatever was there before; see
49    /// `retroglyph-software`'s `DEFAULT_BG`), which would leave a visible black grid of border
50    /// cells on a light [`Theme`] rather than a border blending into its surroundings. That means
51    /// this widget has to assume *something* about what it's drawn over, even though (unlike
52    /// [`super::Panel`], which also owns and fills its own interior) a standalone `BoxBorder`
53    /// genuinely doesn't know: `theme.panel_bg` is the closest default, matching what a themed
54    /// [`super::Panel`]/[`super::Modal`] around it would use. Drawing this border directly on the
55    /// raw screen background instead needs a manual [`BoxBorder::style`] override afterwards.
56    ///
57    /// Call before any manual [`BoxBorder::style`] override you want to keep.
58    #[must_use]
59    pub fn theme(self, theme: Theme) -> Self {
60        self.theme_on(theme, theme.panel_bg)
61    }
62
63    /// Same as [`BoxBorder::theme`], but `style` is drawn on `bg` instead of `theme.panel_bg` --
64    /// for a border drawn directly on a backdrop other than a themed [`super::Panel`]/
65    /// [`super::Modal`]'s fill. [`BoxBorder::theme`] is exactly `theme_on(theme, theme.panel_bg)`.
66    #[must_use]
67    pub fn theme_on(mut self, theme: Theme, bg: Color) -> Self {
68        self.style = Style::new().fg(theme.border).bg(bg);
69        self
70    }
71}
72
73impl Widget for BoxBorder {
74    fn render(&self, area: Rect, surface: &mut Surface<'_>) {
75        if area.width() < 2 || area.height() < 2 {
76            return;
77        }
78
79        let x0 = area.left();
80        let y0 = area.top();
81        let x1 = area.right().saturating_sub(1);
82        let y1 = area.bottom().saturating_sub(1);
83
84        // Corners
85        surface.put((x0, y0), TL, self.style);
86        surface.put((x1, y0), TR, self.style);
87        surface.put((x0, y1), BL, self.style);
88        surface.put((x1, y1), BR, self.style);
89
90        // Horizontal edges
91        for x in (x0 + 1)..x1 {
92            surface.put((x, y0), H, self.style);
93            surface.put((x, y1), H, self.style);
94        }
95
96        // Vertical edges
97        for y in (y0 + 1)..y1 {
98            surface.put((x0, y), V, self.style);
99            surface.put((x1, y), V, self.style);
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use retroglyph_core::{Color, Grid, Pos};
107
108    use super::*;
109
110    #[test]
111    fn draws_corners_and_edges() {
112        let area = Rect::new(0, 0, 5, 3);
113        let mut grid = Grid::new(5, 3);
114        BoxBorder::new()
115            .style(Style::new().fg(Color::WHITE))
116            .render(area, &mut Surface::new(&mut grid, area, 0));
117
118        assert_eq!(grid[Pos::new(0, 0)].glyph(), TL);
119        assert_eq!(grid[Pos::new(4, 0)].glyph(), TR);
120        assert_eq!(grid[Pos::new(0, 2)].glyph(), BL);
121        assert_eq!(grid[Pos::new(4, 2)].glyph(), BR);
122        assert_eq!(grid[Pos::new(2, 0)].glyph(), H);
123        assert_eq!(grid[Pos::new(0, 1)].glyph(), V);
124        // Interior untouched.
125        assert_eq!(grid[Pos::new(2, 1)].glyph(), ' ');
126    }
127
128    #[test]
129    fn too_small_is_a_no_op() {
130        let area = Rect::new(0, 0, 1, 1);
131        let mut grid = Grid::new(1, 1);
132        BoxBorder::new().render(area, &mut Surface::new(&mut grid, area, 0));
133        assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
134    }
135
136    #[test]
137    fn theme_maps_border_role_onto_style() {
138        let area = Rect::new(0, 0, 5, 3);
139        let mut grid = Grid::new(5, 3);
140        BoxBorder::new()
141            .theme(Theme::DARK)
142            .render(area, &mut Surface::new(&mut grid, area, 0));
143
144        assert_eq!(
145            grid[Pos::new(0, 0)].style().foreground(),
146            Theme::DARK.border
147        );
148        assert_eq!(
149            grid[Pos::new(0, 0)].style().background(),
150            Theme::DARK.panel_bg
151        );
152    }
153
154    #[test]
155    fn theme_on_uses_the_given_backdrop_instead_of_panel_bg() {
156        let area = Rect::new(0, 0, 5, 3);
157        let mut grid = Grid::new(5, 3);
158        BoxBorder::new()
159            .theme_on(Theme::DARK, Color::Default)
160            .render(area, &mut Surface::new(&mut grid, area, 0));
161
162        assert_eq!(
163            grid[Pos::new(0, 0)].style().foreground(),
164            Theme::DARK.border
165        );
166        assert_eq!(grid[Pos::new(0, 0)].style().background(), Color::Default);
167    }
168}