revue/widget/input_widgets/
checkbox.rs1use crate::event::{Key, KeyEvent};
4use crate::render::Cell;
5use crate::style::Color;
6use crate::widget::traits::{
7 EventResult, Interactive, RenderContext, View, WidgetProps, WidgetState,
8};
9use crate::{impl_styled_view, impl_widget_builders};
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
13pub enum CheckboxStyle {
14 #[default]
16 Square,
17 Unicode,
19 Filled,
21 Circle,
23}
24
25impl CheckboxStyle {
26 fn chars(&self) -> (char, char) {
28 match self {
29 CheckboxStyle::Square => ('x', ' '),
30 CheckboxStyle::Unicode => ('☑', '☐'),
31 CheckboxStyle::Filled => ('■', '□'),
32 CheckboxStyle::Circle => ('●', '○'),
33 }
34 }
35
36 fn brackets(&self) -> Option<(char, char)> {
38 match self {
39 CheckboxStyle::Square => Some(('[', ']')),
40 _ => None,
41 }
42 }
43}
44
45#[derive(Clone, Debug)]
47pub struct Checkbox {
48 label: String,
49 checked: bool,
50 state: WidgetState,
52 props: WidgetProps,
54 style: CheckboxStyle,
55 check_fg: Option<Color>,
57}
58
59impl Checkbox {
60 pub fn new(label: impl Into<String>) -> Self {
62 Self {
63 label: label.into(),
64 checked: false,
65 state: WidgetState::new(),
66 props: WidgetProps::new(),
67 style: CheckboxStyle::default(),
68 check_fg: None,
69 }
70 }
71
72 pub fn checked(mut self, checked: bool) -> Self {
74 self.checked = checked;
75 self
76 }
77
78 pub fn style(mut self, style: CheckboxStyle) -> Self {
80 self.style = style;
81 self
82 }
83
84 pub fn check_fg(mut self, color: Color) -> Self {
86 self.check_fg = Some(color);
87 self
88 }
89
90 pub fn is_checked(&self) -> bool {
92 self.checked
93 }
94
95 pub fn set_checked(&mut self, checked: bool) {
97 self.checked = checked;
98 }
99
100 pub fn toggle(&mut self) {
102 if !self.state.disabled {
103 self.checked = !self.checked;
104 }
105 }
106
107 pub fn handle_key(&mut self, key: &Key) -> bool {
109 if self.state.disabled {
110 return false;
111 }
112
113 if matches!(key, Key::Enter | Key::Char(' ')) {
114 self.toggle();
115 true
116 } else {
117 false
118 }
119 }
120}
121
122impl Default for Checkbox {
123 fn default() -> Self {
124 Self::new("")
125 }
126}
127
128impl View for Checkbox {
129 fn render(&self, ctx: &mut RenderContext) {
130 let area = ctx.area;
131 if area.width == 0 || area.height == 0 {
132 return;
133 }
134
135 let (checked_char, unchecked_char) = self.style.chars();
136 let brackets = self.style.brackets();
137
138 let mut x = area.x;
139
140 let label_fg = self.state.resolve_fg(ctx.style, Color::WHITE);
142
143 let check_fg = if self.state.disabled {
144 Color::rgb(100, 100, 100)
145 } else if self.checked {
146 self.check_fg.unwrap_or(Color::GREEN)
147 } else {
148 self.state.fg.unwrap_or(Color::rgb(150, 150, 150))
149 };
150
151 if self.state.focused && !self.state.disabled {
153 let mut cell = Cell::new('>');
154 cell.fg = Some(Color::CYAN);
155 ctx.buffer.set(x, area.y, cell);
156 x += 1;
157
158 let space = Cell::new(' ');
159 ctx.buffer.set(x, area.y, space);
160 x += 1;
161 }
162
163 if let Some((left, right)) = brackets {
165 let mut left_cell = Cell::new(left);
167 left_cell.fg = Some(label_fg);
168 ctx.buffer.set(x, area.y, left_cell);
169 x += 1;
170
171 let check_char = if self.checked {
172 checked_char
173 } else {
174 unchecked_char
175 };
176 let mut check_cell = Cell::new(check_char);
177 check_cell.fg = Some(check_fg);
178 ctx.buffer.set(x, area.y, check_cell);
179 x += 1;
180
181 let mut right_cell = Cell::new(right);
182 right_cell.fg = Some(label_fg);
183 ctx.buffer.set(x, area.y, right_cell);
184 x += 1;
185 } else {
186 let check_char = if self.checked {
188 checked_char
189 } else {
190 unchecked_char
191 };
192 let mut check_cell = Cell::new(check_char);
193 check_cell.fg = Some(check_fg);
194 ctx.buffer.set(x, area.y, check_cell);
195 x += 1;
196 }
197
198 ctx.buffer.set(x, area.y, Cell::new(' '));
200 x += 1;
201
202 for ch in self.label.chars() {
204 if x >= area.x + area.width {
205 break;
206 }
207 let mut cell = Cell::new(ch);
208 cell.fg = Some(label_fg);
209 if self.state.focused && !self.state.disabled {
210 cell.modifier = crate::render::Modifier::BOLD;
211 }
212 ctx.buffer.set(x, area.y, cell);
213 x += 1;
214 }
215 }
216
217 crate::impl_view_meta!("Checkbox");
218}
219
220impl Interactive for Checkbox {
221 fn handle_key(&mut self, event: &KeyEvent) -> EventResult {
222 if self.state.disabled {
223 return EventResult::Ignored;
224 }
225
226 match event.key {
227 Key::Enter | Key::Char(' ') => {
228 self.checked = !self.checked;
229 EventResult::ConsumedAndRender
230 }
231 _ => EventResult::Ignored,
232 }
233 }
234
235 fn focusable(&self) -> bool {
236 !self.state.disabled
237 }
238
239 fn on_focus(&mut self) {
240 self.state.focused = true;
241 }
242
243 fn on_blur(&mut self) {
244 self.state.focused = false;
245 }
246}
247
248pub fn checkbox(label: impl Into<String>) -> Checkbox {
250 Checkbox::new(label)
251}
252
253impl_styled_view!(Checkbox);
254impl_widget_builders!(Checkbox);
255
256#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn test_checkbox_new() {
265 let cb = Checkbox::new("Accept terms");
266 assert_eq!(cb.label, "Accept terms");
267 assert!(!cb.is_checked());
268 assert!(!cb.is_focused());
269 assert!(!cb.is_disabled());
270 }
271
272 #[test]
273 fn test_checkbox_builder() {
274 let cb = Checkbox::new("Option")
275 .checked(true)
276 .focused(true)
277 .disabled(false)
278 .style(CheckboxStyle::Unicode);
279
280 assert!(cb.is_checked());
281 assert!(cb.is_focused());
282 assert!(!cb.is_disabled());
283 assert_eq!(cb.style, CheckboxStyle::Unicode);
284 }
285
286 #[test]
287 fn test_checkbox_styles() {
288 let square = CheckboxStyle::Square.chars();
289 assert_eq!(square, ('x', ' '));
290
291 let unicode = CheckboxStyle::Unicode.chars();
292 assert_eq!(unicode, ('☑', '☐'));
293
294 let filled = CheckboxStyle::Filled.chars();
295 assert_eq!(filled, ('■', '□'));
296
297 let circle = CheckboxStyle::Circle.chars();
298 assert_eq!(circle, ('●', '○'));
299 }
300
301 #[test]
302 fn test_checkbox_helper() {
303 let cb = checkbox("Helper");
304 assert_eq!(cb.label, "Helper");
305 }
306
307 #[test]
308 fn test_checkbox_custom_colors() {
309 let cb = Checkbox::new("Colored")
310 .fg(Color::WHITE)
311 .check_fg(Color::GREEN);
312
313 assert_eq!(cb.state.fg, Some(Color::WHITE));
314 assert_eq!(cb.check_fg, Some(Color::GREEN));
315 }
316}