Skip to main content

retroglyph_widgets/widget/
box_border.rs

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