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 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 #[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 #[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 #[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 #[must_use]
124 pub fn with_title_left_aligned(mut self) -> Self {
125 self.title_left_aligned = true;
126 self
127 }
128
129 #[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 #[must_use]
138 pub fn with_style(mut self, style: BorderStyle) -> Self {
139 self.style = style;
140 self
141 }
142
143 #[must_use]
145 pub fn with_color(mut self, color: Color) -> Self {
146 self.color = color;
147 self
148 }
149
150 #[must_use]
152 pub fn with_title_color(mut self, color: Color) -> Self {
153 self.title_color = color;
154 self
155 }
156
157 #[must_use]
159 pub fn with_fill(mut self, fill: bool) -> Self {
160 self.fill = fill;
161 self
162 }
163
164 #[must_use]
166 pub fn with_background(mut self, color: Color) -> Self {
167 self.background = color;
168 self
169 }
170
171 #[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 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 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 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 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 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 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 #[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 #[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 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 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 self.draw_top_border(canvas, width, chars, &style);
445
446 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 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 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 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 &[]
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 }
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 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 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 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 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 assert!(!canvas.texts.is_empty());
867 }
868
869 #[test]
870 fn test_border_title_with_sections() {
871 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 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 }
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 }
941}