Skip to main content

retroglyph_widgets/widget/
box_border.rs

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