1#![warn(missing_docs)]
47#![warn(clippy::pedantic)]
48#![allow(clippy::cast_possible_truncation)] use std::borrow::Cow;
51
52use ratatui::buffer::Buffer;
53use ratatui::layout::Rect;
54use ratatui::style::{Style, Styled};
55use ratatui::text::{Line, Span};
56use ratatui::widgets::{Block, Widget};
57
58pub mod symbols;
59
60#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
62pub enum LabelPosition {
63 #[default]
65 Right,
66 Left,
68 Top,
70 Bottom,
72}
73
74#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
76pub enum HorizontalAlignment {
77 #[default]
79 Left,
80 Center,
82 Right,
84}
85
86impl HorizontalAlignment {
87 const fn offset(self, available: u16, content: u16) -> u16 {
89 match self {
90 Self::Left => 0,
91 Self::Center => available.saturating_sub(content) / 2,
92 Self::Right => available.saturating_sub(content),
93 }
94 }
95}
96
97#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
99pub enum VerticalAlignment {
100 #[default]
102 Top,
103 Center,
105 Bottom,
107}
108
109impl VerticalAlignment {
110 const fn offset(self, available: u16, content: u16) -> u16 {
112 match self {
113 Self::Top => 0,
114 Self::Center => available.saturating_sub(content) / 2,
115 Self::Bottom => available.saturating_sub(content),
116 }
117 }
118}
119
120fn to_owned_line(line: &Line<'_>) -> Line<'static> {
122 Line::from(
123 line.spans
124 .iter()
125 .map(|s| Span::styled(s.content.to_string(), s.style))
126 .collect::<Vec<_>>(),
127 )
128}
129
130#[expect(clippy::struct_field_names)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
162pub struct Checkbox<'a> {
163 label: Line<'a>,
165 checked: bool,
167 block: Option<Block<'a>>,
169 style: Style,
171 checkbox_style: Style,
173 label_style: Style,
175 checked_symbol: Cow<'a, str>,
177 unchecked_symbol: Cow<'a, str>,
179 label_position: LabelPosition,
181 horizontal_alignment: HorizontalAlignment,
183 vertical_alignment: VerticalAlignment,
185 min_width: Option<u16>,
187 max_width: Option<u16>,
189 wrap_label: bool,
191}
192
193impl Default for Checkbox<'_> {
194 fn default() -> Self {
215 Self {
216 label: Line::default(),
217 checked: false,
218 block: None,
219 style: Style::default(),
220 checkbox_style: Style::default(),
221 label_style: Style::default(),
222 checked_symbol: Cow::Borrowed(symbols::CHECKED),
223 unchecked_symbol: Cow::Borrowed(symbols::UNCHECKED),
224 label_position: LabelPosition::default(),
225 horizontal_alignment: HorizontalAlignment::default(),
226 vertical_alignment: VerticalAlignment::default(),
227 min_width: None,
228 max_width: None,
229 wrap_label: false,
230 }
231 }
232}
233
234impl<'a> Checkbox<'a> {
235 pub fn new<T>(label: T, checked: bool) -> Self
253 where
254 T: Into<Line<'a>>,
255 {
256 Self {
257 label: label.into(),
258 checked,
259 ..Default::default()
260 }
261 }
262
263 #[must_use = "method moves the value of self and returns the modified value"]
275 pub fn label<T>(mut self, label: T) -> Self
276 where
277 T: Into<Line<'a>>,
278 {
279 self.label = label.into();
280 self
281 }
282
283 #[must_use = "method moves the value of self and returns the modified value"]
293 pub const fn checked(mut self, checked: bool) -> Self {
294 self.checked = checked;
295 self
296 }
297
298 #[must_use = "method moves the value of self and returns the modified value"]
309 pub fn block(mut self, block: Block<'a>) -> Self {
310 self.block = Some(block);
311 self
312 }
313
314 #[must_use = "method moves the value of self and returns the modified value"]
333 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
334 self.style = style.into();
335 self
336 }
337
338 #[must_use = "method moves the value of self and returns the modified value"]
356 pub fn checkbox_style<S: Into<Style>>(mut self, style: S) -> Self {
357 self.checkbox_style = style.into();
358 self
359 }
360
361 #[must_use = "method moves the value of self and returns the modified value"]
379 pub fn label_style<S: Into<Style>>(mut self, style: S) -> Self {
380 self.label_style = style.into();
381 self
382 }
383
384 #[must_use = "method moves the value of self and returns the modified value"]
396 pub fn checked_symbol<T>(mut self, symbol: T) -> Self
397 where
398 T: Into<Cow<'a, str>>,
399 {
400 self.checked_symbol = symbol.into();
401 self
402 }
403
404 #[must_use = "method moves the value of self and returns the modified value"]
416 pub fn unchecked_symbol<T>(mut self, symbol: T) -> Self
417 where
418 T: Into<Cow<'a, str>>,
419 {
420 self.unchecked_symbol = symbol.into();
421 self
422 }
423
424 #[must_use = "method moves the value of self and returns the modified value"]
436 pub const fn label_position(mut self, position: LabelPosition) -> Self {
437 self.label_position = position;
438 self
439 }
440
441 #[must_use = "method moves the value of self and returns the modified value"]
454 pub const fn horizontal_alignment(mut self, alignment: HorizontalAlignment) -> Self {
455 self.horizontal_alignment = alignment;
456 self
457 }
458
459 #[must_use = "method moves the value of self and returns the modified value"]
472 pub const fn vertical_alignment(mut self, alignment: VerticalAlignment) -> Self {
473 self.vertical_alignment = alignment;
474 self
475 }
476
477 #[must_use = "method moves the value of self and returns the modified value"]
489 pub const fn min_width(mut self, width: u16) -> Self {
490 self.min_width = Some(width);
491 self
492 }
493
494 #[must_use = "method moves the value of self and returns the modified value"]
506 pub const fn max_width(mut self, width: u16) -> Self {
507 self.max_width = Some(width);
508 self
509 }
510
511 #[must_use = "method moves the value of self and returns the modified value"]
526 pub const fn wrap_label(mut self, wrap: bool) -> Self {
527 self.wrap_label = wrap;
528 self
529 }
530}
531
532impl Styled for Checkbox<'_> {
533 type Item = Self;
534
535 fn style(&self) -> Style {
536 self.style
537 }
538
539 fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
540 self.style = style.into();
541 self
542 }
543}
544
545impl Widget for Checkbox<'_> {
546 fn render(self, area: Rect, buf: &mut Buffer) {
547 Widget::render(&self, area, buf);
548 }
549}
550
551impl Widget for &Checkbox<'_> {
552 fn render(self, area: Rect, buf: &mut Buffer) {
553 buf.set_style(area, self.style);
554 let inner = if let Some(ref block) = self.block {
555 let inner_area = block.inner(area);
556 block.render(area, buf);
557 inner_area
558 } else {
559 area
560 };
561 self.render_checkbox(inner, buf);
562 }
563}
564
565impl Checkbox<'_> {
566 fn render_checkbox(&self, area: Rect, buf: &mut Buffer) {
567 if area.is_empty() {
568 return;
569 }
570
571 let symbol = if self.checked {
573 &self.checked_symbol
574 } else {
575 &self.unchecked_symbol
576 };
577
578 let checkbox_style = self.style.patch(self.checkbox_style);
580 let label_style = self.style.patch(self.label_style);
581
582 let mut render_area = area;
584 if let Some(min_width) = self.min_width {
585 render_area.width = render_area.width.max(min_width);
586 }
587 if let Some(max_width) = self.max_width {
588 render_area.width = render_area.width.min(max_width);
589 }
590
591 render_area.width = render_area.width.min(area.width);
593
594 let checkbox_span = Span::styled(symbol.as_ref(), checkbox_style);
596 let styled_label = self.label.clone().patch_style(label_style);
597 let owned_label = to_owned_line(&styled_label);
598
599 match self.label_position {
601 LabelPosition::Right | LabelPosition::Left => {
602 self.render_horizontal(render_area, buf, checkbox_span, owned_label);
603 }
604 LabelPosition::Top | LabelPosition::Bottom => {
605 self.render_vertical(render_area, buf, checkbox_span, owned_label);
606 }
607 }
608 }
609
610 fn render_horizontal(
611 &self,
612 area: Rect,
613 buf: &mut Buffer,
614 checkbox_span: Span<'_>,
615 label: Line<'static>,
616 ) {
617 if area.height == 0 || area.width == 0 {
618 return;
619 }
620
621 let checkbox_width = checkbox_span.width() as u16;
622 let space_width = 1u16;
623
624 let label_lines = if self.wrap_label {
626 let available_width = area.width.saturating_sub(checkbox_width + space_width);
627 Self::wrap_text(&label, available_width)
628 } else {
629 vec![label]
630 };
631
632 let total_width = if label_lines.is_empty() {
633 checkbox_width
634 } else {
635 checkbox_width
636 + space_width
637 + label_lines
638 .iter()
639 .map(|l| l.width() as u16)
640 .max()
641 .unwrap_or(0)
642 };
643
644 let x_offset = self.horizontal_alignment.offset(area.width, total_width);
646
647 let content_height = label_lines.len() as u16;
649 let y_offset = self.vertical_alignment.offset(area.height, content_height);
650
651 match self.label_position {
653 LabelPosition::Right if x_offset < area.width && y_offset < area.height => {
654 let checkbox_area = Rect {
656 x: area.x + x_offset,
657 y: area.y + y_offset,
658 width: checkbox_width.min(area.width.saturating_sub(x_offset)),
659 height: 1,
660 };
661 Line::from(vec![checkbox_span]).render(checkbox_area, buf);
662
663 for (i, label_line) in label_lines.iter().enumerate() {
665 let label_x = area.x + x_offset + checkbox_width + space_width;
666 let label_y = area.y + y_offset + i as u16;
667 if label_y < area.y + area.height && label_x < area.x + area.width {
668 let label_area = Rect {
669 x: label_x,
670 y: label_y,
671 width: area
672 .width
673 .saturating_sub(x_offset + checkbox_width + space_width),
674 height: 1,
675 };
676 label_line.clone().render(label_area, buf);
677 }
678 }
679 }
680 LabelPosition::Left => {
681 let max_label_width = label_lines
683 .iter()
684 .map(|l| l.width() as u16)
685 .max()
686 .unwrap_or(0);
687
688 for (i, label_line) in label_lines.iter().enumerate() {
690 let label_y = area.y + y_offset + i as u16;
691 if label_y < area.y + area.height && x_offset < area.width {
692 let label_area = Rect {
693 x: area.x + x_offset,
694 y: label_y,
695 width: max_label_width.min(area.width.saturating_sub(x_offset)),
696 height: 1,
697 };
698 label_line.clone().render(label_area, buf);
699 }
700 }
701
702 let checkbox_x = area.x + x_offset + max_label_width + space_width;
704 if checkbox_x < area.x + area.width && y_offset < area.height {
705 let checkbox_area = Rect {
706 x: checkbox_x,
707 y: area.y + y_offset,
708 width: checkbox_width.min(
709 area.width
710 .saturating_sub(x_offset + max_label_width + space_width),
711 ),
712 height: 1,
713 };
714 Line::from(vec![checkbox_span]).render(checkbox_area, buf);
715 }
716 }
717 _ => {}
718 }
719 }
720
721 fn render_vertical(
722 &self,
723 area: Rect,
724 buf: &mut Buffer,
725 checkbox_span: Span<'_>,
726 label: Line<'static>,
727 ) {
728 if area.height == 0 || area.width == 0 {
729 return;
730 }
731
732 let label_lines = if self.wrap_label {
734 Self::wrap_text(&label, area.width)
735 } else {
736 vec![label]
737 };
738
739 let checkbox_width = checkbox_span.width() as u16;
740 let label_height = label_lines.len() as u16;
741 let total_height = 1 + label_height; let y_offset = self.vertical_alignment.offset(area.height, total_height);
745
746 match self.label_position {
747 LabelPosition::Top => {
748 for (i, label_line) in label_lines.iter().enumerate() {
750 let label_y = area.y + y_offset + i as u16;
751 if label_y < area.y + area.height {
752 let x_offset = self
753 .horizontal_alignment
754 .offset(area.width, label_line.width() as u16);
755 let label_area = Rect {
756 x: area.x + x_offset,
757 y: label_y,
758 width: area.width.saturating_sub(x_offset),
759 height: 1,
760 };
761 label_line.clone().render(label_area, buf);
762 }
763 }
764
765 let checkbox_y = area.y + y_offset + label_height;
767 if checkbox_y < area.y + area.height {
768 let x_offset = self.horizontal_alignment.offset(area.width, checkbox_width);
769 let checkbox_area = Rect {
770 x: area.x + x_offset,
771 y: checkbox_y,
772 width: checkbox_width.min(area.width.saturating_sub(x_offset)),
773 height: 1,
774 };
775 Line::from(vec![checkbox_span]).render(checkbox_area, buf);
776 }
777 }
778 LabelPosition::Bottom => {
779 let x_offset = self.horizontal_alignment.offset(area.width, checkbox_width);
781 let checkbox_area = Rect {
782 x: area.x + x_offset,
783 y: area.y + y_offset,
784 width: checkbox_width.min(area.width.saturating_sub(x_offset)),
785 height: 1,
786 };
787 Line::from(vec![checkbox_span]).render(checkbox_area, buf);
788
789 for (i, label_line) in label_lines.iter().enumerate() {
791 let label_y = area.y + y_offset + 1 + i as u16;
792 if label_y < area.y + area.height {
793 let x_offset = self
794 .horizontal_alignment
795 .offset(area.width, label_line.width() as u16);
796 let label_area = Rect {
797 x: area.x + x_offset,
798 y: label_y,
799 width: area.width.saturating_sub(x_offset),
800 height: 1,
801 };
802 label_line.clone().render(label_area, buf);
803 }
804 }
805 }
806 _ => {}
807 }
808 }
809
810 fn wrap_text(line: &Line<'_>, max_width: u16) -> Vec<Line<'static>> {
811 if max_width == 0 {
812 return vec![to_owned_line(line)];
813 }
814
815 let mut result = Vec::new();
816 let mut current_line = Vec::new();
817 let mut current_width = 0u16;
818
819 for span in &line.spans {
820 let text = span.content.as_ref();
821 let words: Vec<&str> = text.split(' ').collect();
822
823 for (i, word) in words.iter().enumerate() {
824 let word_width = word.chars().count() as u16;
825 let space_width = u16::from(i > 0 || !current_line.is_empty());
826
827 if current_width + space_width + word_width > max_width && !current_line.is_empty()
828 {
829 result.push(Line::from(current_line.clone()));
830 current_line.clear();
831 current_width = 0;
832 }
833
834 if i > 0 {
835 current_line.push(Span::styled(String::from(" "), span.style));
836 current_width += 1;
837 }
838
839 current_line.push(Span::styled(String::from(*word), span.style));
840 current_width += word_width;
841 }
842 }
843
844 if !current_line.is_empty() {
845 result.push(Line::from(current_line));
846 }
847
848 if result.is_empty() {
849 result.push(to_owned_line(line));
850 }
851
852 result
853 }
854}
855
856#[cfg(test)]
857mod tests {
858 use ratatui::style::{Color, Modifier, Stylize};
859
860 use super::*;
861
862 #[test]
863 fn checkbox_new() {
864 let checkbox = Checkbox::new("Test", true);
865 assert_eq!(checkbox.label, Line::from("Test"));
866 assert!(checkbox.checked);
867 }
868
869 #[test]
870 fn checkbox_default() {
871 let checkbox = Checkbox::default();
872 assert_eq!(checkbox.label, Line::default());
873 assert!(!checkbox.checked);
874 }
875
876 #[test]
877 fn checkbox_label() {
878 let checkbox = Checkbox::default().label("New label");
879 assert_eq!(checkbox.label, Line::from("New label"));
880 }
881
882 #[test]
883 fn checkbox_checked() {
884 let checkbox = Checkbox::default().checked(true);
885 assert!(checkbox.checked);
886 }
887
888 #[test]
889 fn checkbox_style() {
890 let style = Style::default().fg(Color::Red);
891 let checkbox = Checkbox::default().style(style);
892 assert_eq!(checkbox.style, style);
893 }
894
895 #[test]
896 fn checkbox_checkbox_style() {
897 let style = Style::default().fg(Color::Green);
898 let checkbox = Checkbox::default().checkbox_style(style);
899 assert_eq!(checkbox.checkbox_style, style);
900 }
901
902 #[test]
903 fn checkbox_label_style() {
904 let style = Style::default().fg(Color::Blue);
905 let checkbox = Checkbox::default().label_style(style);
906 assert_eq!(checkbox.label_style, style);
907 }
908
909 #[test]
910 fn checkbox_checked_symbol() {
911 let checkbox = Checkbox::default().checked_symbol("[X]");
912 assert_eq!(checkbox.checked_symbol, "[X]");
913 }
914
915 #[test]
916 fn checkbox_unchecked_symbol() {
917 let checkbox = Checkbox::default().unchecked_symbol("[ ]");
918 assert_eq!(checkbox.unchecked_symbol, "[ ]");
919 }
920
921 #[test]
922 fn checkbox_styled_trait() {
923 let checkbox = Checkbox::default().red();
924 assert_eq!(checkbox.style, Style::default().fg(Color::Red));
925 }
926
927 #[test]
928 fn checkbox_render_unchecked() {
929 let checkbox = Checkbox::new("Test", false);
930 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
931 checkbox.render(buffer.area, &mut buffer);
932
933 assert!(buffer
935 .cell(buffer.area.as_position())
936 .unwrap()
937 .symbol()
938 .starts_with('☐'));
939 }
940
941 #[test]
942 fn checkbox_render_checked() {
943 let checkbox = Checkbox::new("Test", true);
944 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
945 checkbox.render(buffer.area, &mut buffer);
946
947 assert!(buffer
949 .cell(buffer.area.as_position())
950 .unwrap()
951 .symbol()
952 .starts_with('☑'));
953 }
954
955 #[test]
956 fn checkbox_render_empty_area() {
957 let checkbox = Checkbox::new("Test", true);
958 let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 0));
959
960 checkbox.render(buffer.area, &mut buffer);
962 }
963
964 #[test]
965 fn checkbox_render_with_block() {
966 let checkbox = Checkbox::new("Test", true).block(Block::bordered());
967 let mut buffer = Buffer::empty(Rect::new(0, 0, 12, 3));
968
969 checkbox.render(buffer.area, &mut buffer);
971 }
972
973 #[test]
974 fn checkbox_render_with_custom_symbols() {
975 let checkbox = Checkbox::new("Test", true)
976 .checked_symbol("[X]")
977 .unchecked_symbol("[ ]");
978
979 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
980 checkbox.render(buffer.area, &mut buffer);
981
982 assert!(buffer
983 .cell(buffer.area.as_position())
984 .unwrap()
985 .symbol()
986 .starts_with('['));
987 }
988
989 #[test]
990 fn checkbox_with_styled_label() {
991 let checkbox = Checkbox::new("Test".blue(), true);
992 assert_eq!(checkbox.label.spans[0].style.fg, Some(Color::Blue));
993 }
994
995 #[test]
996 fn checkbox_complex_styling() {
997 let checkbox = Checkbox::new("Feature", true)
998 .style(Style::default().fg(Color::White))
999 .checkbox_style(
1000 Style::default()
1001 .fg(Color::Green)
1002 .add_modifier(Modifier::BOLD),
1003 )
1004 .label_style(Style::default().fg(Color::Gray));
1005
1006 assert_eq!(checkbox.style.fg, Some(Color::White));
1007 assert_eq!(checkbox.checkbox_style.fg, Some(Color::Green));
1008 assert_eq!(checkbox.label_style.fg, Some(Color::Gray));
1009 }
1010
1011 macro_rules! symbol_tests {
1013 ($($name:ident: $checked:expr, $unchecked:expr;)*) => {
1014 $(
1015 #[test]
1016 fn $name() {
1017 let checkbox = Checkbox::new("Test", true)
1018 .checked_symbol($checked)
1019 .unchecked_symbol($unchecked);
1020 assert_eq!(checkbox.checked_symbol, $checked);
1021 assert_eq!(checkbox.unchecked_symbol, $unchecked);
1022 }
1023 )*
1024 };
1025 }
1026
1027 symbol_tests! {
1028 checkbox_emoji_symbols: "✅ ", "⬜ ";
1029 checkbox_unicode_symbols: "● ", "○ ";
1030 checkbox_arrow_symbols: "▶ ", "▷ ";
1031 checkbox_parenthesis_symbols: "(X)", "(O)";
1032 checkbox_minus_symbols: "[+]", "[-]";
1033 checkbox_predefined_symbols: symbols::CHECKED_X, symbols::UNCHECKED_SPACE;
1034 }
1035
1036 #[test]
1037 fn checkbox_predefined_minus_symbol() {
1038 use crate::symbols;
1039 let checkbox = Checkbox::new("Test", false).unchecked_symbol(symbols::UNCHECKED_MINUS);
1040
1041 assert_eq!(checkbox.unchecked_symbol, "[-]");
1042 }
1043
1044 #[test]
1045 fn checkbox_predefined_parenthesis_symbols() {
1046 use crate::symbols;
1047 let checkbox = Checkbox::new("Test", true)
1048 .checked_symbol(symbols::CHECKED_PARENTHESIS_X)
1049 .unchecked_symbol(symbols::UNCHECKED_PARENTHESIS_O);
1050
1051 assert_eq!(checkbox.checked_symbol, "(X)");
1052 assert_eq!(checkbox.unchecked_symbol, "(O)");
1053 }
1054
1055 #[test]
1056 fn checkbox_render_emoji() {
1057 let checkbox = Checkbox::new("Emoji", true)
1058 .checked_symbol("✅ ")
1059 .unchecked_symbol("⬜ ");
1060
1061 let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
1062 checkbox.render(buffer.area, &mut buffer);
1063
1064 assert!(buffer.area.area() > 0);
1066 }
1067
1068 #[test]
1069 fn checkbox_label_style_overrides() {
1070 let checkbox = Checkbox::new("Test", true)
1071 .style(Style::default().fg(Color::White))
1072 .label_style(Style::default().fg(Color::Blue));
1073
1074 assert_eq!(checkbox.style.fg, Some(Color::White));
1075 assert_eq!(checkbox.label_style.fg, Some(Color::Blue));
1076 }
1077
1078 #[test]
1079 fn horizontal_alignment_offset() {
1080 assert_eq!(HorizontalAlignment::Left.offset(20, 6), 0);
1081 assert_eq!(HorizontalAlignment::Center.offset(20, 6), 7);
1082 assert_eq!(HorizontalAlignment::Right.offset(20, 6), 14);
1083 assert_eq!(HorizontalAlignment::Center.offset(4, 10), 0);
1085 assert_eq!(HorizontalAlignment::Right.offset(4, 10), 0);
1086 }
1087
1088 #[test]
1089 fn vertical_alignment_offset() {
1090 assert_eq!(VerticalAlignment::Top.offset(10, 3), 0);
1091 assert_eq!(VerticalAlignment::Center.offset(10, 3), 3);
1092 assert_eq!(VerticalAlignment::Bottom.offset(10, 3), 7);
1093 assert_eq!(VerticalAlignment::Bottom.offset(2, 5), 0);
1094 }
1095
1096 #[test]
1097 fn to_owned_line_clones_content_and_style() {
1098 let line = Line::from(vec![Span::styled("hi", Style::default().fg(Color::Red))]);
1099 let owned = to_owned_line(&line);
1100 assert_eq!(owned.spans[0].content, "hi");
1101 assert_eq!(owned.spans[0].style.fg, Some(Color::Red));
1102 }
1103
1104 #[test]
1105 fn checkbox_builder_defaults() {
1106 let checkbox = Checkbox::default();
1107 assert_eq!(checkbox.label_position, LabelPosition::Right);
1108 assert_eq!(checkbox.horizontal_alignment, HorizontalAlignment::Left);
1109 assert_eq!(checkbox.vertical_alignment, VerticalAlignment::Top);
1110 assert_eq!(checkbox.min_width, None);
1111 assert_eq!(checkbox.max_width, None);
1112 assert!(!checkbox.wrap_label);
1113 }
1114
1115 #[test]
1116 fn checkbox_layout_builders() {
1117 let checkbox = Checkbox::new("Test", true)
1118 .label_position(LabelPosition::Left)
1119 .horizontal_alignment(HorizontalAlignment::Center)
1120 .vertical_alignment(VerticalAlignment::Bottom)
1121 .min_width(10)
1122 .max_width(40)
1123 .wrap_label(true);
1124
1125 assert_eq!(checkbox.label_position, LabelPosition::Left);
1126 assert_eq!(checkbox.horizontal_alignment, HorizontalAlignment::Center);
1127 assert_eq!(checkbox.vertical_alignment, VerticalAlignment::Bottom);
1128 assert_eq!(checkbox.min_width, Some(10));
1129 assert_eq!(checkbox.max_width, Some(40));
1130 assert!(checkbox.wrap_label);
1131 }
1132
1133 #[test]
1134 fn checkbox_render_label_left() {
1135 let checkbox = Checkbox::new("Hi", true).label_position(LabelPosition::Left);
1136 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
1137 checkbox.render(buffer.area, &mut buffer);
1138 assert!(buffer
1140 .cell(buffer.area.as_position())
1141 .unwrap()
1142 .symbol()
1143 .starts_with('H'));
1144 }
1145
1146 #[test]
1147 fn checkbox_render_label_top_and_bottom() {
1148 for position in [LabelPosition::Top, LabelPosition::Bottom] {
1149 let checkbox = Checkbox::new("Hi", true).label_position(position);
1150 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1151 checkbox.render(buffer.area, &mut buffer);
1153 }
1154 }
1155
1156 #[test]
1157 fn checkbox_render_wrapped_label() {
1158 let checkbox = Checkbox::new("one two three four five", false)
1159 .wrap_label(true)
1160 .max_width(12);
1161 let mut buffer = Buffer::empty(Rect::new(0, 0, 12, 4));
1162 checkbox.render(buffer.area, &mut buffer);
1164 }
1165
1166 #[test]
1167 fn checkbox_min_width_expands_and_max_width_limits() {
1168 let checkbox = Checkbox::new("Test", true).min_width(50);
1170 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
1171 checkbox.render(buffer.area, &mut buffer);
1172
1173 let checkbox = Checkbox::new("A long label here", false).max_width(5);
1175 let mut buffer = Buffer::empty(Rect::new(0, 0, 30, 1));
1176 checkbox.render(buffer.area, &mut buffer);
1177 }
1178
1179 #[test]
1180 fn wrap_text_zero_width_returns_single_line() {
1181 let line = Line::from("hello world");
1182 let wrapped = Checkbox::wrap_text(&line, 0);
1183 assert_eq!(wrapped.len(), 1);
1184 assert_eq!(wrapped[0].width(), "hello world".len());
1185 }
1186
1187 #[test]
1188 fn wrap_text_splits_on_width() {
1189 let line = Line::from("aaa bbb ccc");
1190 let wrapped = Checkbox::wrap_text(&line, 4);
1191 assert_eq!(wrapped.len(), 3);
1192 }
1193
1194 #[test]
1195 fn checkbox_render_alignment_variants() {
1196 for h in [
1197 HorizontalAlignment::Left,
1198 HorizontalAlignment::Center,
1199 HorizontalAlignment::Right,
1200 ] {
1201 for v in [
1202 VerticalAlignment::Top,
1203 VerticalAlignment::Center,
1204 VerticalAlignment::Bottom,
1205 ] {
1206 let checkbox = Checkbox::new("Test", true)
1207 .horizontal_alignment(h)
1208 .vertical_alignment(v);
1209 let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 3));
1210 checkbox.render(buffer.area, &mut buffer);
1211 }
1212 }
1213 }
1214}