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