ratatui_form/field/
checkbox.rs1use crossterm::event::{KeyCode, KeyEvent};
4use ratatui::buffer::Buffer;
5use ratatui::layout::Rect;
6use ratatui::text::{Line, Span};
7use ratatui::widgets::Widget;
8use serde_json::Value;
9use unicode_width::UnicodeWidthStr;
10
11use crate::field::Field;
12use crate::style::FormStyle;
13use crate::validation::ValidationError;
14
15pub struct Checkbox {
17 id: String,
18 label: String,
19 checked: bool,
20 required: bool,
21}
22
23impl Checkbox {
24 pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
26 Self {
27 id: id.into(),
28 label: label.into(),
29 checked: false,
30 required: false,
31 }
32 }
33
34 pub fn checked(mut self, checked: bool) -> Self {
36 self.checked = checked;
37 self
38 }
39
40 pub fn required(mut self) -> Self {
42 self.required = true;
43 self
44 }
45
46 fn toggle(&mut self) {
47 self.checked = !self.checked;
48 }
49}
50
51impl Field for Checkbox {
52 fn id(&self) -> &str {
53 &self.id
54 }
55
56 fn label(&self) -> &str {
57 &self.label
58 }
59
60 fn render(&self, area: Rect, buf: &mut Buffer, focused: bool, style: &FormStyle) {
61 if area.height < 1 || area.width < 4 {
62 return;
63 }
64
65 let checkbox_style = if focused {
66 style.input_focused
67 } else {
68 style.input
69 };
70
71 let label_style = if focused {
72 style.label_focused
73 } else {
74 style.label
75 };
76
77 let checkbox_char = if self.checked { "[✓]" } else { "[ ]" };
79 for (i, c) in checkbox_char.chars().enumerate() {
80 if area.x + (i as u16) < area.x + area.width {
81 buf[(area.x + i as u16, area.y)].set_char(c);
82 buf[(area.x + i as u16, area.y)].set_style(checkbox_style);
83 }
84 }
85
86 let required_marker = if self.required { "*" } else { "" };
88 let label_text = format!(" {}{}", self.label, required_marker);
89 let label_x = area.x + 3;
90 let remaining_width = area.width.saturating_sub(3);
91
92 if remaining_width > 0 {
93 let label_span = Span::styled(&label_text, label_style);
94 let label_line = Line::from(label_span);
95 let label_area = Rect {
96 x: label_x,
97 y: area.y,
98 width: remaining_width.min(label_text.width() as u16),
99 height: 1,
100 };
101 label_line.render(label_area, buf);
102 }
103 }
104
105 fn handle_input(&mut self, event: &KeyEvent) -> bool {
106 match event.code {
107 KeyCode::Enter | KeyCode::Char(' ') => {
108 self.toggle();
109 true
110 }
111 _ => false,
112 }
113 }
114
115 fn value(&self) -> Value {
116 Value::Bool(self.checked)
117 }
118
119 fn validate(&self) -> Result<(), Vec<ValidationError>> {
120 if self.required && !self.checked {
121 Err(vec![ValidationError {
122 field_id: self.id.clone(),
123 message: format!("{} must be checked", self.label),
124 }])
125 } else {
126 Ok(())
127 }
128 }
129
130 fn height(&self) -> u16 {
131 1
132 }
133
134 fn is_required(&self) -> bool {
135 self.required
136 }
137}