Skip to main content

presentar_terminal/widgets/
border.rs

1//! Box/Border widget for framing content.
2//!
3//! Provides Unicode box-drawing borders around content areas.
4
5use presentar_core::{
6    Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
7    LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
8};
9use std::any::Any;
10use std::time::Duration;
11
12/// Border style using Unicode box-drawing characters.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum BorderStyle {
15    /// Single line: ┌─┐│└─┘
16    #[default]
17    Single,
18    /// Double line: ╔═╗║╚═╝
19    Double,
20    /// Rounded corners: ╭─╮│╰─╯
21    Rounded,
22    /// Heavy/thick: ┏━┓┃┗━┛
23    Heavy,
24    /// ASCII only: +-+|+-+
25    Ascii,
26    /// No border
27    None,
28}
29
30impl BorderStyle {
31    /// Get border characters: (`top_left`, top, `top_right`, left, right, `bottom_left`, bottom, `bottom_right`)
32    #[must_use]
33    pub const fn chars(&self) -> (char, char, char, char, char, char, char, char) {
34        match self {
35            Self::Single => ('┌', '─', '┐', '│', '│', '└', '─', '┘'),
36            Self::Double => ('╔', '═', '╗', '║', '║', '╚', '═', '╝'),
37            Self::Rounded => ('╭', '─', '╮', '│', '│', '╰', '─', '╯'),
38            Self::Heavy => ('┏', '━', '┓', '┃', '┃', '┗', '━', '┛'),
39            Self::Ascii => ('+', '-', '+', '|', '|', '+', '-', '+'),
40            Self::None => (' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '),
41        }
42    }
43}
44
45/// A bordered box widget.
46pub struct Border {
47    /// Title displayed at top.
48    title: Option<String>,
49    /// Border style.
50    style: BorderStyle,
51    /// Border color.
52    color: Color,
53    /// Title color.
54    title_color: Color,
55    /// Fill background.
56    fill: bool,
57    /// Background color (if fill is true).
58    background: Color,
59    /// Cached bounds.
60    bounds: Rect,
61    /// Left-align title (ttop style) instead of centering.
62    title_left_aligned: bool,
63    /// Child widget (for composition).
64    child: Option<Box<dyn Widget>>,
65}
66
67impl std::fmt::Debug for Border {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        f.debug_struct("Border")
70            .field("title", &self.title)
71            .field("style", &self.style)
72            .field("color", &self.color)
73            .field("title_color", &self.title_color)
74            .field("fill", &self.fill)
75            .field("background", &self.background)
76            .field("bounds", &self.bounds)
77            .field("title_left_aligned", &self.title_left_aligned)
78            .field("child", &self.child.as_ref().map(|_| ".."))
79            .finish()
80    }
81}
82
83impl Default for Border {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl Border {
90    /// Create a new border.
91    #[must_use]
92    pub fn new() -> Self {
93        Self {
94            title: None,
95            style: BorderStyle::default(),
96            color: Color::new(0.4, 0.5, 0.6, 1.0),
97            title_color: Color::new(0.8, 0.9, 1.0, 1.0),
98            fill: false,
99            background: Color::new(0.1, 0.1, 0.1, 1.0),
100            bounds: Rect::default(),
101            title_left_aligned: false,
102            child: None,
103        }
104    }
105
106    /// Create a border with rounded corners and a title.
107    #[must_use]
108    pub fn rounded(title: impl Into<String>) -> Self {
109        Self::new()
110            .with_style(BorderStyle::Rounded)
111            .with_title(title)
112            .with_title_left_aligned()
113    }
114
115    /// Set a child widget to render inside the border.
116    #[must_use]
117    pub fn child(mut self, widget: impl Widget + 'static) -> Self {
118        self.child = Some(Box::new(widget));
119        self
120    }
121
122    /// Enable left-aligned title (ttop style) instead of centered.
123    #[must_use]
124    pub fn with_title_left_aligned(mut self) -> Self {
125        self.title_left_aligned = true;
126        self
127    }
128
129    /// Set the title.
130    #[must_use]
131    pub fn with_title(mut self, title: impl Into<String>) -> Self {
132        self.title = Some(title.into());
133        self
134    }
135
136    /// Set the border style.
137    #[must_use]
138    pub fn with_style(mut self, style: BorderStyle) -> Self {
139        self.style = style;
140        self
141    }
142
143    /// Set the border color.
144    #[must_use]
145    pub fn with_color(mut self, color: Color) -> Self {
146        self.color = color;
147        self
148    }
149
150    /// Set the title color.
151    #[must_use]
152    pub fn with_title_color(mut self, color: Color) -> Self {
153        self.title_color = color;
154        self
155    }
156
157    /// Enable background fill.
158    #[must_use]
159    pub fn with_fill(mut self, fill: bool) -> Self {
160        self.fill = fill;
161        self
162    }
163
164    /// Set the background color.
165    #[must_use]
166    pub fn with_background(mut self, color: Color) -> Self {
167        self.background = color;
168        self
169    }
170
171    /// Get the inner content area (excluding border).
172    #[must_use]
173    pub fn inner_rect(&self) -> Rect {
174        if matches!(self.style, BorderStyle::None) {
175            self.bounds
176        } else {
177            Rect::new(
178                self.bounds.x + 1.0,
179                self.bounds.y + 1.0,
180                (self.bounds.width - 2.0).max(0.0),
181                (self.bounds.height - 2.0).max(0.0),
182            )
183        }
184    }
185
186    /// Smart truncation: prefer section boundaries (│) to avoid mid-word cuts.
187    fn truncate_title_smart(title: &str, max_len: usize) -> std::borrow::Cow<'_, str> {
188        let title_len = title.chars().count();
189        if title_len <= max_len {
190            return std::borrow::Cow::Borrowed(title);
191        }
192        let truncate_to = max_len.saturating_sub(1);
193        let chars_vec: Vec<char> = title.chars().collect();
194        // Find section boundaries (│)
195        let mut section_ends: Vec<usize> = vec![0];
196        for (i, &ch) in chars_vec.iter().enumerate() {
197            if ch == '│' {
198                let mut end = i;
199                while end > 0 && chars_vec[end - 1] == ' ' {
200                    end -= 1;
201                }
202                if end > 0 {
203                    section_ends.push(end);
204                }
205            }
206        }
207        // Find best split point
208        let mut best_split = truncate_to;
209        for &end in section_ends.iter().rev() {
210            if end <= truncate_to && end > 0 {
211                best_split = end;
212                break;
213            }
214        }
215        // Fall back to word boundary
216        if best_split == truncate_to || best_split == 0 {
217            let search_start = truncate_to.saturating_sub(truncate_to / 3);
218            for i in (search_start..truncate_to).rev() {
219                if i < chars_vec.len() && chars_vec[i] == ' ' {
220                    best_split = i;
221                    break;
222                }
223            }
224        }
225        let truncated: String = chars_vec.iter().take(best_split).collect();
226        std::borrow::Cow::Owned(format!("{}…", truncated.trim_end()))
227    }
228
229    /// Draw top border with optional title.
230    fn draw_top_border(
231        &self,
232        canvas: &mut dyn Canvas,
233        width: usize,
234        chars: (char, char, char, char, char, char, char, char),
235        style: &TextStyle,
236    ) {
237        let (tl, top, tr, _, _, _, _, _) = chars;
238        let mut top_line = String::with_capacity(width);
239        top_line.push(tl);
240
241        if let Some(ref title) = self.title {
242            let ttop_available = width.saturating_sub(3);
243            let display_title = Self::truncate_title_smart(title, ttop_available);
244            let display_len = display_title.chars().count();
245            if display_len > 0 && ttop_available > 0 {
246                let title_style = TextStyle {
247                    color: self.title_color,
248                    ..Default::default()
249                };
250                if self.title_left_aligned {
251                    self.draw_left_aligned_title(
252                        canvas,
253                        &display_title,
254                        display_len,
255                        width,
256                        top,
257                        tr,
258                        style,
259                        &title_style,
260                    );
261                } else {
262                    self.draw_centered_title(
263                        canvas,
264                        &display_title,
265                        display_len,
266                        width,
267                        top,
268                        tr,
269                        style,
270                        &title_style,
271                    );
272                }
273                return;
274            }
275        }
276        // No title or too small
277        for _ in 0..(width - 2) {
278            top_line.push(top);
279        }
280        top_line.push(tr);
281        canvas.draw_text(&top_line, Point::new(self.bounds.x, self.bounds.y), style);
282    }
283
284    /// Draw left-aligned title.
285    #[allow(clippy::too_many_arguments)]
286    fn draw_left_aligned_title(
287        &self,
288        canvas: &mut dyn Canvas,
289        title: &str,
290        title_len: usize,
291        width: usize,
292        top: char,
293        tr: char,
294        style: &TextStyle,
295        title_style: &TextStyle,
296    ) {
297        let (tl, _, _, _, _, _, _, _) = self.style.chars();
298        canvas.draw_text(
299            &tl.to_string(),
300            Point::new(self.bounds.x, self.bounds.y),
301            style,
302        );
303        canvas.draw_text(
304            &format!(" {title}"),
305            Point::new(self.bounds.x + 1.0, self.bounds.y),
306            title_style,
307        );
308        let after_title = 1 + title_len + 1;
309        let remaining = width.saturating_sub(after_title + 1);
310        let mut rest = String::new();
311        for _ in 0..remaining {
312            rest.push(top);
313        }
314        rest.push(tr);
315        canvas.draw_text(
316            &rest,
317            Point::new(self.bounds.x + after_title as f32, self.bounds.y),
318            style,
319        );
320    }
321
322    /// Draw centered title.
323    #[allow(clippy::too_many_arguments)]
324    fn draw_centered_title(
325        &self,
326        canvas: &mut dyn Canvas,
327        title: &str,
328        title_len: usize,
329        width: usize,
330        top: char,
331        tr: char,
332        style: &TextStyle,
333        title_style: &TextStyle,
334    ) {
335        let (tl, _, _, _, _, _, _, _) = self.style.chars();
336        let available = width.saturating_sub(4);
337        let padding = (available.saturating_sub(title_len)) / 2;
338        let mut top_line = String::new();
339        top_line.push(tl);
340        for _ in 0..padding {
341            top_line.push(top);
342        }
343        canvas.draw_text(&top_line, Point::new(self.bounds.x, self.bounds.y), style);
344        canvas.draw_text(
345            &format!(" {title} "),
346            Point::new(self.bounds.x + 1.0 + padding as f32, self.bounds.y),
347            title_style,
348        );
349        let after_title = padding + title_len + 2;
350        let remaining = width.saturating_sub(after_title + 1);
351        let mut rest = String::new();
352        for _ in 0..remaining {
353            rest.push(top);
354        }
355        rest.push(tr);
356        canvas.draw_text(
357            &rest,
358            Point::new(self.bounds.x + after_title as f32, self.bounds.y),
359            style,
360        );
361    }
362}
363
364impl Brick for Border {
365    fn brick_name(&self) -> &'static str {
366        "border"
367    }
368
369    fn assertions(&self) -> &[BrickAssertion] {
370        static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
371        ASSERTIONS
372    }
373
374    fn budget(&self) -> BrickBudget {
375        BrickBudget::uniform(16)
376    }
377
378    fn verify(&self) -> BrickVerification {
379        BrickVerification {
380            passed: self.assertions().to_vec(),
381            failed: vec![],
382            verification_time: Duration::from_micros(10),
383        }
384    }
385
386    fn to_html(&self) -> String {
387        String::new()
388    }
389
390    fn to_css(&self) -> String {
391        String::new()
392    }
393}
394
395impl Widget for Border {
396    fn type_id(&self) -> TypeId {
397        TypeId::of::<Self>()
398    }
399
400    fn measure(&self, constraints: Constraints) -> Size {
401        constraints.constrain(Size::new(
402            constraints.max_width.min(20.0),
403            constraints.max_height.min(5.0),
404        ))
405    }
406
407    fn layout(&mut self, bounds: Rect) -> LayoutResult {
408        self.bounds = bounds;
409
410        // Layout child in inner rect (calculate inner rect first to avoid borrow conflict)
411        let inner = self.inner_rect();
412        if let Some(ref mut child) = self.child {
413            child.layout(inner);
414        }
415
416        LayoutResult {
417            size: Size::new(bounds.width, bounds.height),
418        }
419    }
420
421    fn paint(&self, canvas: &mut dyn Canvas) {
422        let width = self.bounds.width as usize;
423        let height = self.bounds.height as usize;
424        if width < 2 || height < 2 {
425            return;
426        }
427
428        // Fill background if enabled
429        if self.fill {
430            canvas.fill_rect(self.bounds, self.background);
431        }
432        if matches!(self.style, BorderStyle::None) {
433            return;
434        }
435
436        let chars = self.style.chars();
437        let (_, _, _, left, right, bl, bottom, br) = chars;
438        let style = TextStyle {
439            color: self.color,
440            ..Default::default()
441        };
442
443        // Top border with title
444        self.draw_top_border(canvas, width, chars, &style);
445
446        // Side borders
447        for y in 1..(height - 1) {
448            canvas.draw_text(
449                &left.to_string(),
450                Point::new(self.bounds.x, self.bounds.y + y as f32),
451                &style,
452            );
453            canvas.draw_text(
454                &right.to_string(),
455                Point::new(self.bounds.x + (width - 1) as f32, self.bounds.y + y as f32),
456                &style,
457            );
458        }
459
460        // Bottom border
461        let mut bottom_line = String::with_capacity(width);
462        bottom_line.push(bl);
463        for _ in 0..(width - 2) {
464            bottom_line.push(bottom);
465        }
466        bottom_line.push(br);
467        canvas.draw_text(
468            &bottom_line,
469            Point::new(self.bounds.x, self.bounds.y + (height - 1) as f32),
470            &style,
471        );
472
473        // Paint child widget
474        if let Some(ref child) = self.child {
475            let inner = self.inner_rect();
476            canvas.push_clip(inner);
477            child.paint(canvas);
478            canvas.pop_clip();
479        }
480    }
481
482    fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
483        // Propagate events to child
484        if let Some(ref mut child) = self.child {
485            if let Some(result) = child.event(event) {
486                return Some(result);
487            }
488        }
489        None
490    }
491
492    fn children(&self) -> &[Box<dyn Widget>] {
493        // Can't return reference to Option contents
494        &[]
495    }
496
497    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
498        &mut []
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505
506    struct MockCanvas {
507        texts: Vec<(String, Point)>,
508        rects: Vec<Rect>,
509    }
510
511    impl MockCanvas {
512        fn new() -> Self {
513            Self {
514                texts: vec![],
515                rects: vec![],
516            }
517        }
518    }
519
520    impl Canvas for MockCanvas {
521        fn fill_rect(&mut self, rect: Rect, _color: Color) {
522            self.rects.push(rect);
523        }
524        fn stroke_rect(&mut self, _rect: Rect, _color: Color, _width: f32) {}
525        fn draw_text(&mut self, text: &str, position: Point, _style: &TextStyle) {
526            self.texts.push((text.to_string(), position));
527        }
528        fn draw_line(&mut self, _from: Point, _to: Point, _color: Color, _width: f32) {}
529        fn fill_circle(&mut self, _center: Point, _radius: f32, _color: Color) {}
530        fn stroke_circle(&mut self, _center: Point, _radius: f32, _color: Color, _width: f32) {}
531        fn fill_arc(&mut self, _c: Point, _r: f32, _s: f32, _e: f32, _color: Color) {}
532        fn draw_path(&mut self, _points: &[Point], _color: Color, _width: f32) {}
533        fn fill_polygon(&mut self, _points: &[Point], _color: Color) {}
534        fn push_clip(&mut self, _rect: Rect) {}
535        fn pop_clip(&mut self) {}
536        fn push_transform(&mut self, _transform: presentar_core::Transform2D) {}
537        fn pop_transform(&mut self) {}
538    }
539
540    #[test]
541    fn test_border_creation() {
542        let border = Border::new();
543        assert!(border.title.is_none());
544        assert_eq!(border.style, BorderStyle::Single);
545    }
546
547    #[test]
548    fn test_border_with_title() {
549        let border = Border::new().with_title("Test");
550        assert_eq!(border.title, Some("Test".to_string()));
551    }
552
553    #[test]
554    fn test_border_with_style() {
555        let border = Border::new().with_style(BorderStyle::Double);
556        assert_eq!(border.style, BorderStyle::Double);
557    }
558
559    #[test]
560    fn test_border_with_color() {
561        let border = Border::new().with_color(Color::RED);
562        assert_eq!(border.color, Color::RED);
563    }
564
565    #[test]
566    fn test_border_with_fill() {
567        let border = Border::new().with_fill(true);
568        assert!(border.fill);
569    }
570
571    #[test]
572    fn test_border_style_chars() {
573        let (tl, _, tr, _, _, bl, _, br) = BorderStyle::Single.chars();
574        assert_eq!(tl, '┌');
575        assert_eq!(tr, '┐');
576        assert_eq!(bl, '└');
577        assert_eq!(br, '┘');
578    }
579
580    #[test]
581    fn test_border_style_rounded() {
582        let (tl, _, tr, _, _, bl, _, br) = BorderStyle::Rounded.chars();
583        assert_eq!(tl, '╭');
584        assert_eq!(tr, '╮');
585        assert_eq!(bl, '╰');
586        assert_eq!(br, '╯');
587    }
588
589    #[test]
590    fn test_border_style_double() {
591        let (tl, _, _, _, _, _, _, _) = BorderStyle::Double.chars();
592        assert_eq!(tl, '╔');
593    }
594
595    #[test]
596    fn test_border_style_heavy() {
597        let (tl, _, _, _, _, _, _, _) = BorderStyle::Heavy.chars();
598        assert_eq!(tl, '┏');
599    }
600
601    #[test]
602    fn test_border_style_ascii() {
603        let (tl, _, _, _, _, _, _, _) = BorderStyle::Ascii.chars();
604        assert_eq!(tl, '+');
605    }
606
607    #[test]
608    fn test_border_inner_rect() {
609        let mut border = Border::new();
610        border.bounds = Rect::new(0.0, 0.0, 20.0, 10.0);
611        let inner = border.inner_rect();
612        assert_eq!(inner.x, 1.0);
613        assert_eq!(inner.y, 1.0);
614        assert_eq!(inner.width, 18.0);
615        assert_eq!(inner.height, 8.0);
616    }
617
618    #[test]
619    fn test_border_inner_rect_no_border() {
620        let mut border = Border::new().with_style(BorderStyle::None);
621        border.bounds = Rect::new(5.0, 5.0, 20.0, 10.0);
622        let inner = border.inner_rect();
623        assert_eq!(inner, border.bounds);
624    }
625
626    #[test]
627    fn test_border_paint() {
628        let mut border = Border::new();
629        border.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
630        let mut canvas = MockCanvas::new();
631        border.paint(&mut canvas);
632        assert!(!canvas.texts.is_empty());
633    }
634
635    #[test]
636    fn test_border_paint_with_title() {
637        let mut border = Border::new().with_title("CPU");
638        border.bounds = Rect::new(0.0, 0.0, 20.0, 5.0);
639        let mut canvas = MockCanvas::new();
640        border.paint(&mut canvas);
641        assert!(canvas.texts.iter().any(|(t, _)| t.contains("CPU")));
642    }
643
644    #[test]
645    fn test_border_paint_with_fill() {
646        let mut border = Border::new().with_fill(true);
647        border.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
648        let mut canvas = MockCanvas::new();
649        border.paint(&mut canvas);
650        assert!(!canvas.rects.is_empty());
651    }
652
653    #[test]
654    fn test_border_paint_no_style() {
655        let mut border = Border::new().with_style(BorderStyle::None);
656        border.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
657        let mut canvas = MockCanvas::new();
658        border.paint(&mut canvas);
659        // Should not draw border characters
660    }
661
662    #[test]
663    fn test_border_paint_small() {
664        let mut border = Border::new();
665        border.bounds = Rect::new(0.0, 0.0, 1.0, 1.0);
666        let mut canvas = MockCanvas::new();
667        border.paint(&mut canvas);
668        // Should early return for small bounds
669        assert!(canvas.texts.is_empty());
670    }
671
672    #[test]
673    fn test_border_assertions() {
674        let border = Border::new();
675        assert!(!border.assertions().is_empty());
676    }
677
678    #[test]
679    fn test_border_verify() {
680        let border = Border::new();
681        assert!(border.verify().is_valid());
682    }
683
684    #[test]
685    fn test_border_brick_name() {
686        let border = Border::new();
687        assert_eq!(border.brick_name(), "border");
688    }
689
690    #[test]
691    fn test_border_type_id() {
692        let border = Border::new();
693        assert_eq!(Widget::type_id(&border), TypeId::of::<Border>());
694    }
695
696    #[test]
697    fn test_border_measure() {
698        let border = Border::new();
699        let size = border.measure(Constraints::loose(Size::new(100.0, 100.0)));
700        assert!(size.width > 0.0);
701        assert!(size.height > 0.0);
702    }
703
704    #[test]
705    fn test_border_layout() {
706        let mut border = Border::new();
707        let bounds = Rect::new(5.0, 10.0, 30.0, 15.0);
708        let result = border.layout(bounds);
709        assert_eq!(result.size.width, 30.0);
710        assert_eq!(border.bounds, bounds);
711    }
712
713    #[test]
714    fn test_border_children() {
715        let border = Border::new();
716        assert!(border.children().is_empty());
717    }
718
719    #[test]
720    fn test_border_children_mut() {
721        let mut border = Border::new();
722        assert!(border.children_mut().is_empty());
723    }
724
725    #[test]
726    fn test_border_event() {
727        let mut border = Border::new();
728        let event = Event::key_down(presentar_core::Key::Enter);
729        assert!(border.event(&event).is_none());
730    }
731
732    #[test]
733    fn test_border_default() {
734        let border = Border::default();
735        assert!(border.title.is_none());
736    }
737
738    #[test]
739    fn test_border_to_html() {
740        let border = Border::new();
741        assert!(border.to_html().is_empty());
742    }
743
744    #[test]
745    fn test_border_to_css() {
746        let border = Border::new();
747        assert!(border.to_css().is_empty());
748    }
749
750    #[test]
751    fn test_border_budget() {
752        let border = Border::new();
753        let budget = border.budget();
754        assert!(budget.paint_ms > 0);
755    }
756
757    #[test]
758    fn test_border_title_too_long() {
759        let mut border = Border::new().with_title("This is a very long title that won't fit");
760        border.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
761        let mut canvas = MockCanvas::new();
762        border.paint(&mut canvas);
763        // Should draw plain border without title
764        assert!(!canvas.texts.is_empty());
765    }
766
767    #[test]
768    fn test_border_with_title_color() {
769        let border = Border::new().with_title_color(Color::GREEN);
770        assert_eq!(border.title_color, Color::GREEN);
771    }
772
773    #[test]
774    fn test_border_with_background() {
775        let border = Border::new().with_background(Color::BLUE);
776        assert_eq!(border.background, Color::BLUE);
777    }
778
779    #[test]
780    fn test_border_rounded_helper() {
781        let border = Border::rounded("CPU Panel");
782        assert_eq!(border.style, BorderStyle::Rounded);
783        assert_eq!(border.title, Some("CPU Panel".to_string()));
784        assert!(border.title_left_aligned);
785    }
786
787    #[test]
788    fn test_border_style_none() {
789        let (tl, top, tr, left, right, bl, bottom, br) = BorderStyle::None.chars();
790        assert_eq!(tl, ' ');
791        assert_eq!(top, ' ');
792        assert_eq!(tr, ' ');
793        assert_eq!(left, ' ');
794        assert_eq!(right, ' ');
795        assert_eq!(bl, ' ');
796        assert_eq!(bottom, ' ');
797        assert_eq!(br, ' ');
798    }
799
800    #[test]
801    fn test_border_style_default() {
802        let style = BorderStyle::default();
803        assert_eq!(style, BorderStyle::Single);
804    }
805
806    #[test]
807    fn test_border_paint_with_left_aligned_title() {
808        let mut border = Border::new().with_title("CPU").with_title_left_aligned();
809        border.bounds = Rect::new(0.0, 0.0, 40.0, 5.0);
810        let mut canvas = MockCanvas::new();
811        border.paint(&mut canvas);
812        assert!(canvas.texts.iter().any(|(t, _)| t.contains("CPU")));
813    }
814
815    #[test]
816    fn test_border_paint_centered_title() {
817        let mut border = Border::new().with_title("Memory");
818        // Not left aligned (default)
819        assert!(!border.title_left_aligned);
820        border.bounds = Rect::new(0.0, 0.0, 50.0, 5.0);
821        let mut canvas = MockCanvas::new();
822        border.paint(&mut canvas);
823        assert!(canvas.texts.iter().any(|(t, _)| t.contains("Memory")));
824    }
825
826    #[test]
827    fn test_border_paint_all_styles() {
828        for style in [
829            BorderStyle::Single,
830            BorderStyle::Double,
831            BorderStyle::Rounded,
832            BorderStyle::Heavy,
833            BorderStyle::Ascii,
834        ] {
835            let mut border = Border::new().with_style(style);
836            border.bounds = Rect::new(0.0, 0.0, 20.0, 5.0);
837            let mut canvas = MockCanvas::new();
838            border.paint(&mut canvas);
839            assert!(!canvas.texts.is_empty());
840        }
841    }
842
843    #[test]
844    fn test_border_paint_with_fill_and_title() {
845        let mut border = Border::new()
846            .with_title("Test")
847            .with_fill(true)
848            .with_background(Color::new(0.1, 0.1, 0.1, 1.0));
849        border.bounds = Rect::new(0.0, 0.0, 30.0, 10.0);
850        let mut canvas = MockCanvas::new();
851        border.paint(&mut canvas);
852        assert!(!canvas.texts.is_empty());
853        assert!(!canvas.rects.is_empty());
854    }
855
856    #[test]
857    fn test_border_title_truncation() {
858        // Very long title that needs truncation
859        let mut border = Border::new().with_title(
860            "This is a very long title that will need to be truncated | section2 | section3",
861        );
862        border.bounds = Rect::new(0.0, 0.0, 30.0, 5.0);
863        let mut canvas = MockCanvas::new();
864        border.paint(&mut canvas);
865        // Should handle truncation gracefully
866        assert!(!canvas.texts.is_empty());
867    }
868
869    #[test]
870    fn test_border_title_with_sections() {
871        // Title with section separators
872        let mut border = Border::new().with_title("CPU 45% │ 8 cores │ 3.6GHz");
873        border.bounds = Rect::new(0.0, 0.0, 40.0, 5.0);
874        let mut canvas = MockCanvas::new();
875        border.paint(&mut canvas);
876        assert!(!canvas.texts.is_empty());
877    }
878
879    #[test]
880    fn test_border_inner_rect_minimum_size() {
881        let mut border = Border::new();
882        border.bounds = Rect::new(0.0, 0.0, 2.0, 2.0);
883        let inner = border.inner_rect();
884        // With 2x2 bounds and 1 pixel border, inner should be 0x0
885        assert_eq!(inner.width, 0.0);
886        assert_eq!(inner.height, 0.0);
887    }
888
889    #[test]
890    fn test_border_paint_narrow_width() {
891        let mut border = Border::new().with_title("Test");
892        border.bounds = Rect::new(0.0, 0.0, 5.0, 5.0);
893        let mut canvas = MockCanvas::new();
894        border.paint(&mut canvas);
895        // Should draw something even with narrow width
896    }
897
898    #[test]
899    fn test_border_all_chars_heavy() {
900        let (tl, top, tr, left, right, bl, bottom, br) = BorderStyle::Heavy.chars();
901        assert_eq!(tl, '┏');
902        assert_eq!(top, '━');
903        assert_eq!(tr, '┓');
904        assert_eq!(left, '┃');
905        assert_eq!(right, '┃');
906        assert_eq!(bl, '┗');
907        assert_eq!(bottom, '━');
908        assert_eq!(br, '┛');
909    }
910
911    #[test]
912    fn test_border_all_chars_double() {
913        let (tl, top, tr, left, right, bl, bottom, br) = BorderStyle::Double.chars();
914        assert_eq!(tl, '╔');
915        assert_eq!(top, '═');
916        assert_eq!(tr, '╗');
917        assert_eq!(left, '║');
918        assert_eq!(right, '║');
919        assert_eq!(bl, '╚');
920        assert_eq!(bottom, '═');
921        assert_eq!(br, '╝');
922    }
923
924    #[test]
925    fn test_border_with_child() {
926        use crate::widgets::Text;
927        let border = Border::new().child(Text::new("Hello"));
928        assert!(border.child.is_some());
929    }
930
931    #[test]
932    fn test_border_child_paint() {
933        use crate::widgets::Text;
934        let mut border = Border::new().child(Text::new("Hello"));
935        border.bounds = Rect::new(0.0, 0.0, 20.0, 10.0);
936        border.layout(border.bounds);
937        let mut canvas = MockCanvas::new();
938        border.paint(&mut canvas);
939        // Child should be painted inside border
940    }
941}