Skip to main content

revue/widget/layout/
border.rs

1//! Border/Frame widget for surrounding content
2
3use crate::layout::Rect;
4use crate::render::Cell;
5use crate::style::Color;
6use crate::utils::border::BorderChars;
7use crate::widget::traits::{RenderContext, View, WidgetProps};
8use crate::{impl_props_builders, impl_styled_view};
9
10/// Border style characters
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub enum BorderType {
13    /// No border
14    None,
15    /// Single line: ┌─┐│└┘
16    #[default]
17    Single,
18    /// Double line: ╔═╗║╚╝
19    Double,
20    /// Rounded: ╭─╮│╰╯
21    Rounded,
22    /// Thick: ┏━┓┃┗┛
23    Thick,
24    /// ASCII: +-+|
25    Ascii,
26}
27
28/// Empty border chars (for BorderType::None)
29const NONE_CHARS: BorderChars = BorderChars {
30    top_left: ' ',
31    top_right: ' ',
32    bottom_left: ' ',
33    bottom_right: ' ',
34    horizontal: ' ',
35    vertical: ' ',
36};
37
38impl BorderType {
39    /// Get the character set for this border type
40    pub fn chars(&self) -> BorderChars {
41        match self {
42            BorderType::None => NONE_CHARS,
43            BorderType::Single => BorderChars::SINGLE,
44            BorderType::Double => BorderChars::DOUBLE,
45            BorderType::Rounded => BorderChars::ROUNDED,
46            BorderType::Thick => BorderChars::BOLD,
47            BorderType::Ascii => BorderChars::ASCII,
48        }
49    }
50}
51
52/// A border widget that wraps content
53pub struct Border {
54    child: Option<Box<dyn View>>,
55    border_type: BorderType,
56    title: Option<String>,
57    fg: Option<Color>,
58    bg: Option<Color>,
59    props: WidgetProps,
60}
61
62impl Border {
63    /// Create a new border
64    pub fn new() -> Self {
65        Self {
66            child: None,
67            border_type: BorderType::Single,
68            title: None,
69            fg: None,
70            bg: None,
71            props: WidgetProps::new(),
72        }
73    }
74
75    /// Set the child widget
76    pub fn child(mut self, child: impl View + 'static) -> Self {
77        self.child = Some(Box::new(child));
78        self
79    }
80
81    /// Set border type
82    pub fn border_type(mut self, border_type: BorderType) -> Self {
83        self.border_type = border_type;
84        self
85    }
86
87    /// Set title (displayed in top border)
88    pub fn title(mut self, title: impl Into<String>) -> Self {
89        self.title = Some(title.into());
90        self
91    }
92
93    /// Set border color
94    pub fn fg(mut self, color: Color) -> Self {
95        self.fg = Some(color);
96        self
97    }
98
99    /// Set background color
100    pub fn bg(mut self, color: Color) -> Self {
101        self.bg = Some(color);
102        self
103    }
104
105    // ─────────────────────────────────────────────────────────────────────────
106    // Preset builders
107    // ─────────────────────────────────────────────────────────────────────────
108
109    /// Create single border
110    pub fn single() -> Self {
111        Self::new().border_type(BorderType::Single)
112    }
113
114    /// Create double border
115    pub fn double() -> Self {
116        Self::new().border_type(BorderType::Double)
117    }
118
119    /// Create rounded border
120    pub fn rounded() -> Self {
121        Self::new().border_type(BorderType::Rounded)
122    }
123
124    /// Create thick border
125    pub fn thick() -> Self {
126        Self::new().border_type(BorderType::Thick)
127    }
128
129    /// Create ASCII border (for basic terminals)
130    pub fn ascii() -> Self {
131        Self::new().border_type(BorderType::Ascii)
132    }
133
134    /// Create a panel (double border with cyan color)
135    pub fn panel() -> Self {
136        Self::new().border_type(BorderType::Double).fg(Color::CYAN)
137    }
138
139    /// Create a card (rounded border with white color)
140    pub fn card() -> Self {
141        Self::new()
142            .border_type(BorderType::Rounded)
143            .fg(Color::WHITE)
144    }
145
146    /// Create an error box (single border with red color)
147    pub fn error_box() -> Self {
148        Self::new().border_type(BorderType::Single).fg(Color::RED)
149    }
150
151    /// Create a success box (single border with green color)
152    pub fn success_box() -> Self {
153        Self::new().border_type(BorderType::Single).fg(Color::GREEN)
154    }
155}
156
157impl Default for Border {
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163impl View for Border {
164    fn render(&self, ctx: &mut RenderContext) {
165        let area = ctx.area;
166        if area.width < 2 || area.height < 2 {
167            return;
168        }
169
170        let chars = self.border_type.chars();
171
172        // Top border
173        let mut cell = Cell::new(chars.top_left);
174        cell.fg = self.fg;
175        cell.bg = self.bg;
176        ctx.buffer.set(area.x, area.y, cell);
177
178        // Top horizontal line with optional title
179        let title_start = if let Some(ref title) = self.title {
180            let max_title_len = (area.width as usize).saturating_sub(4);
181            let display_title: String = title.chars().take(max_title_len).collect();
182            let title_len = display_title.len();
183
184            // Draw horizontal before title
185            for x in 1..2 {
186                let mut c = Cell::new(chars.horizontal);
187                c.fg = self.fg;
188                c.bg = self.bg;
189                ctx.buffer.set(area.x + x, area.y, c);
190            }
191
192            // Draw title
193            for (i, ch) in display_title.chars().enumerate() {
194                let mut c = Cell::new(ch);
195                c.fg = self.fg;
196                c.bg = self.bg;
197                ctx.buffer.set(area.x + 2 + i as u16, area.y, c);
198            }
199
200            2 + title_len as u16
201        } else {
202            1
203        };
204
205        // Rest of top horizontal
206        for x in title_start..(area.width - 1) {
207            let mut c = Cell::new(chars.horizontal);
208            c.fg = self.fg;
209            c.bg = self.bg;
210            ctx.buffer.set(area.x + x, area.y, c);
211        }
212
213        // Top right corner
214        let mut cell = Cell::new(chars.top_right);
215        cell.fg = self.fg;
216        cell.bg = self.bg;
217        ctx.buffer.set(area.x + area.width - 1, area.y, cell);
218
219        // Left and right borders
220        for y in 1..(area.height - 1) {
221            let mut left = Cell::new(chars.vertical);
222            left.fg = self.fg;
223            left.bg = self.bg;
224            ctx.buffer.set(area.x, area.y + y, left);
225
226            let mut right = Cell::new(chars.vertical);
227            right.fg = self.fg;
228            right.bg = self.bg;
229            ctx.buffer.set(area.x + area.width - 1, area.y + y, right);
230        }
231
232        // Bottom border
233        let mut cell = Cell::new(chars.bottom_left);
234        cell.fg = self.fg;
235        cell.bg = self.bg;
236        ctx.buffer.set(area.x, area.y + area.height - 1, cell);
237
238        for x in 1..(area.width - 1) {
239            let mut c = Cell::new(chars.horizontal);
240            c.fg = self.fg;
241            c.bg = self.bg;
242            ctx.buffer.set(area.x + x, area.y + area.height - 1, c);
243        }
244
245        let mut cell = Cell::new(chars.bottom_right);
246        cell.fg = self.fg;
247        cell.bg = self.bg;
248        ctx.buffer
249            .set(area.x + area.width - 1, area.y + area.height - 1, cell);
250
251        // Render child in inner area
252        if let Some(ref child) = self.child {
253            let inner = Rect::new(
254                area.x + 1,
255                area.y + 1,
256                area.width.saturating_sub(2),
257                area.height.saturating_sub(2),
258            );
259            let mut child_ctx = RenderContext::new(ctx.buffer, inner);
260            child.render(&mut child_ctx);
261        }
262    }
263
264    crate::impl_view_meta!("Border");
265}
266
267/// Helper function to create a border
268pub fn border() -> Border {
269    Border::new()
270}
271
272impl_styled_view!(Border);
273impl_props_builders!(Border);
274
275/// Draw a border on a buffer
276///
277/// This is a utility function that can be used by other widgets to draw borders
278/// without needing to create a full Border widget.
279///
280/// # Arguments
281/// * `buffer` - The buffer to draw on
282/// * `area` - The area to draw the border in
283/// * `border_type` - The type of border to draw
284/// * `fg` - Optional foreground color
285/// * `bg` - Optional background color
286pub fn draw_border(
287    buffer: &mut crate::render::Buffer,
288    area: Rect,
289    border_type: BorderType,
290    fg: Option<Color>,
291    bg: Option<Color>,
292) {
293    if area.width < 2 || area.height < 2 || border_type == BorderType::None {
294        return;
295    }
296
297    let chars = border_type.chars();
298
299    // Top-left corner
300    let mut cell = Cell::new(chars.top_left);
301    cell.fg = fg;
302    cell.bg = bg;
303    buffer.set(area.x, area.y, cell);
304
305    // Top border
306    for x in 1..(area.width - 1) {
307        let mut c = Cell::new(chars.horizontal);
308        c.fg = fg;
309        c.bg = bg;
310        buffer.set(area.x + x, area.y, c);
311    }
312
313    // Top-right corner
314    let mut cell = Cell::new(chars.top_right);
315    cell.fg = fg;
316    cell.bg = bg;
317    buffer.set(area.x + area.width - 1, area.y, cell);
318
319    // Left and right borders
320    for y in 1..(area.height - 1) {
321        let mut left = Cell::new(chars.vertical);
322        left.fg = fg;
323        left.bg = bg;
324        buffer.set(area.x, area.y + y, left);
325
326        let mut right = Cell::new(chars.vertical);
327        right.fg = fg;
328        right.bg = bg;
329        buffer.set(area.x + area.width - 1, area.y + y, right);
330    }
331
332    // Bottom-left corner
333    let mut cell = Cell::new(chars.bottom_left);
334    cell.fg = fg;
335    cell.bg = bg;
336    buffer.set(area.x, area.y + area.height - 1, cell);
337
338    // Bottom border
339    for x in 1..(area.width - 1) {
340        let mut c = Cell::new(chars.horizontal);
341        c.fg = fg;
342        c.bg = bg;
343        buffer.set(area.x + x, area.y + area.height - 1, c);
344    }
345
346    // Bottom-right corner
347    let mut cell = Cell::new(chars.bottom_right);
348    cell.fg = fg;
349    cell.bg = bg;
350    buffer.set(area.x + area.width - 1, area.y + area.height - 1, cell);
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356    use crate::render::Buffer;
357    use crate::widget::Text;
358
359    #[test]
360    fn test_border_new() {
361        let b = Border::new();
362        assert_eq!(b.border_type, BorderType::Single);
363        assert!(b.title.is_none());
364    }
365
366    #[test]
367    fn test_border_types() {
368        assert_eq!(Border::single().border_type, BorderType::Single);
369        assert_eq!(Border::double().border_type, BorderType::Double);
370        assert_eq!(Border::rounded().border_type, BorderType::Rounded);
371    }
372
373    #[test]
374    fn test_border_render_single() {
375        let mut buffer = Buffer::new(10, 5);
376        let area = Rect::new(0, 0, 10, 5);
377        let mut ctx = RenderContext::new(&mut buffer, area);
378
379        let b = Border::single();
380        b.render(&mut ctx);
381
382        assert_eq!(buffer.get(0, 0).unwrap().symbol, '┌');
383        assert_eq!(buffer.get(9, 0).unwrap().symbol, '┐');
384        assert_eq!(buffer.get(0, 4).unwrap().symbol, '└');
385        assert_eq!(buffer.get(9, 4).unwrap().symbol, '┘');
386        assert_eq!(buffer.get(0, 2).unwrap().symbol, '│');
387        assert_eq!(buffer.get(5, 0).unwrap().symbol, '─');
388    }
389
390    #[test]
391    fn test_border_render_double() {
392        let mut buffer = Buffer::new(10, 5);
393        let area = Rect::new(0, 0, 10, 5);
394        let mut ctx = RenderContext::new(&mut buffer, area);
395
396        let b = Border::double();
397        b.render(&mut ctx);
398
399        assert_eq!(buffer.get(0, 0).unwrap().symbol, '╔');
400        assert_eq!(buffer.get(9, 0).unwrap().symbol, '╗');
401    }
402
403    #[test]
404    fn test_border_with_title() {
405        let mut buffer = Buffer::new(20, 5);
406        let area = Rect::new(0, 0, 20, 5);
407        let mut ctx = RenderContext::new(&mut buffer, area);
408
409        let b = Border::single().title("Test");
410        b.render(&mut ctx);
411
412        assert_eq!(buffer.get(2, 0).unwrap().symbol, 'T');
413        assert_eq!(buffer.get(3, 0).unwrap().symbol, 'e');
414        assert_eq!(buffer.get(4, 0).unwrap().symbol, 's');
415        assert_eq!(buffer.get(5, 0).unwrap().symbol, 't');
416    }
417
418    #[test]
419    fn test_border_with_child() {
420        let mut buffer = Buffer::new(10, 5);
421        let area = Rect::new(0, 0, 10, 5);
422        let mut ctx = RenderContext::new(&mut buffer, area);
423
424        let b = Border::single().child(Text::new("Hi"));
425        b.render(&mut ctx);
426
427        // Child rendered at (1, 1) inside border
428        assert_eq!(buffer.get(1, 1).unwrap().symbol, 'H');
429        assert_eq!(buffer.get(2, 1).unwrap().symbol, 'i');
430    }
431
432    #[test]
433    fn test_border_rounded() {
434        let mut buffer = Buffer::new(10, 5);
435        let area = Rect::new(0, 0, 10, 5);
436        let mut ctx = RenderContext::new(&mut buffer, area);
437
438        let b = Border::rounded();
439        b.render(&mut ctx);
440
441        assert_eq!(buffer.get(0, 0).unwrap().symbol, '╭');
442        assert_eq!(buffer.get(9, 0).unwrap().symbol, '╮');
443        assert_eq!(buffer.get(0, 4).unwrap().symbol, '╰');
444        assert_eq!(buffer.get(9, 4).unwrap().symbol, '╯');
445    }
446
447    #[test]
448    fn test_border_type_chars() {
449        // Test that BorderType::chars() returns correct consolidated BorderChars
450        let none = BorderType::None.chars();
451        assert_eq!(none.top_left, ' ');
452        assert_eq!(none.horizontal, ' ');
453
454        let single = BorderType::Single.chars();
455        assert_eq!(single.top_left, '┌');
456        assert_eq!(single.horizontal, '─');
457
458        let double = BorderType::Double.chars();
459        assert_eq!(double.top_left, '╔');
460        assert_eq!(double.horizontal, '═');
461
462        let rounded = BorderType::Rounded.chars();
463        assert_eq!(rounded.top_left, '╭');
464        assert_eq!(rounded.bottom_right, '╯');
465
466        let thick = BorderType::Thick.chars();
467        assert_eq!(thick.top_left, '┏');
468        assert_eq!(thick.horizontal, '━');
469
470        let ascii = BorderType::Ascii.chars();
471        assert_eq!(ascii.top_left, '+');
472        assert_eq!(ascii.horizontal, '-');
473    }
474
475    #[test]
476    fn test_draw_border_utility() {
477        // Test the draw_border utility function
478        let mut buffer = Buffer::new(10, 5);
479        let area = Rect::new(0, 0, 10, 5);
480
481        draw_border(&mut buffer, area, BorderType::Single, None, None);
482
483        assert_eq!(buffer.get(0, 0).unwrap().symbol, '┌');
484        assert_eq!(buffer.get(9, 0).unwrap().symbol, '┐');
485        assert_eq!(buffer.get(0, 4).unwrap().symbol, '└');
486        assert_eq!(buffer.get(9, 4).unwrap().symbol, '┘');
487    }
488
489    #[test]
490    fn test_draw_border_none_type() {
491        // Test that BorderType::None draws nothing
492        let mut buffer = Buffer::new(10, 5);
493        let area = Rect::new(0, 0, 10, 5);
494
495        draw_border(&mut buffer, area, BorderType::None, None, None);
496
497        // Border should not be drawn
498        assert_eq!(buffer.get(0, 0).unwrap().symbol, ' ');
499    }
500}