ratatui_interact/components/
checkbox.rs1use ratatui::{
21 buffer::Buffer,
22 layout::Rect,
23 style::{Color, Modifier, Style},
24 text::{Line, Span},
25 widgets::{Paragraph, Widget},
26};
27
28use crate::traits::{ClickRegion, FocusId};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum CheckBoxAction {
33 Toggle,
35}
36
37#[derive(Debug, Clone)]
39pub struct CheckBoxState {
40 pub checked: bool,
42 pub focused: bool,
44 pub enabled: bool,
46}
47
48impl Default for CheckBoxState {
49 fn default() -> Self {
50 Self {
51 checked: false,
52 focused: false,
53 enabled: true,
54 }
55 }
56}
57
58impl CheckBoxState {
59 pub fn new(checked: bool) -> Self {
65 Self {
66 checked,
67 ..Default::default()
68 }
69 }
70
71 pub fn toggle(&mut self) {
75 if self.enabled {
76 self.checked = !self.checked;
77 }
78 }
79
80 pub fn set_checked(&mut self, checked: bool) {
82 if self.enabled {
83 self.checked = checked;
84 }
85 }
86
87 pub fn set_focused(&mut self, focused: bool) {
89 self.focused = focused;
90 }
91
92 pub fn set_enabled(&mut self, enabled: bool) {
94 self.enabled = enabled;
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct CheckBoxStyle {
101 pub checked_symbol: &'static str,
103 pub unchecked_symbol: &'static str,
105 pub focused_fg: Color,
107 pub unfocused_fg: Color,
109 pub disabled_fg: Color,
111 pub checked_fg: Color,
113}
114
115impl Default for CheckBoxStyle {
116 fn default() -> Self {
117 Self {
118 checked_symbol: "[x]",
119 unchecked_symbol: "[ ]",
120 focused_fg: Color::Yellow,
121 unfocused_fg: Color::White,
122 disabled_fg: Color::DarkGray,
123 checked_fg: Color::Green,
124 }
125 }
126}
127
128impl From<&crate::theme::Theme> for CheckBoxStyle {
129 fn from(theme: &crate::theme::Theme) -> Self {
130 let p = &theme.palette;
131 Self {
132 checked_symbol: "[x]",
133 unchecked_symbol: "[ ]",
134 focused_fg: p.primary,
135 unfocused_fg: p.text,
136 disabled_fg: p.text_disabled,
137 checked_fg: p.success,
138 }
139 }
140}
141
142impl CheckBoxStyle {
143 pub fn ascii() -> Self {
145 Self::default()
146 }
147
148 pub fn unicode() -> Self {
150 Self {
151 checked_symbol: "☑",
152 unchecked_symbol: "☐",
153 ..Default::default()
154 }
155 }
156
157 pub fn checkmark() -> Self {
159 Self {
160 checked_symbol: "✓",
161 unchecked_symbol: "○",
162 ..Default::default()
163 }
164 }
165
166 pub fn custom(checked: &'static str, unchecked: &'static str) -> Self {
168 Self {
169 checked_symbol: checked,
170 unchecked_symbol: unchecked,
171 ..Default::default()
172 }
173 }
174
175 pub fn focused_fg(mut self, color: Color) -> Self {
177 self.focused_fg = color;
178 self
179 }
180
181 pub fn unfocused_fg(mut self, color: Color) -> Self {
183 self.unfocused_fg = color;
184 self
185 }
186
187 pub fn disabled_fg(mut self, color: Color) -> Self {
189 self.disabled_fg = color;
190 self
191 }
192
193 pub fn checked_fg(mut self, color: Color) -> Self {
195 self.checked_fg = color;
196 self
197 }
198}
199
200pub struct CheckBox<'a> {
205 label: &'a str,
206 state: &'a CheckBoxState,
207 style: CheckBoxStyle,
208 focus_id: FocusId,
209}
210
211impl<'a> CheckBox<'a> {
212 pub fn new(label: &'a str, state: &'a CheckBoxState) -> Self {
219 Self {
220 label,
221 state,
222 style: CheckBoxStyle::default(),
223 focus_id: FocusId::default(),
224 }
225 }
226
227 pub fn style(mut self, style: CheckBoxStyle) -> Self {
229 self.style = style;
230 self
231 }
232
233 pub fn theme(self, theme: &crate::theme::Theme) -> Self {
235 self.style(CheckBoxStyle::from(theme))
236 }
237
238 pub fn focus_id(mut self, id: FocusId) -> Self {
240 self.focus_id = id;
241 self
242 }
243
244 fn build_line(&self) -> Line<'a> {
246 let symbol = if self.state.checked {
247 self.style.checked_symbol
248 } else {
249 self.style.unchecked_symbol
250 };
251
252 let fg_color = if !self.state.enabled {
253 self.style.disabled_fg
254 } else if self.state.focused {
255 self.style.focused_fg
256 } else if self.state.checked {
257 self.style.checked_fg
258 } else {
259 self.style.unfocused_fg
260 };
261
262 let mut style = Style::default().fg(fg_color);
263 if self.state.focused && self.state.enabled {
264 style = style.add_modifier(Modifier::BOLD);
265 }
266
267 Line::from(vec![
268 Span::styled(symbol, style),
269 Span::styled(" ", style),
270 Span::styled(self.label, style),
271 ])
272 }
273
274 pub fn width(&self) -> u16 {
276 let symbol_len = if self.state.checked {
277 self.style.checked_symbol.chars().count()
278 } else {
279 self.style.unchecked_symbol.chars().count()
280 };
281 (symbol_len + 1 + self.label.chars().count()) as u16
282 }
283
284 pub fn render_stateful(self, area: Rect, buf: &mut Buffer) -> ClickRegion<CheckBoxAction> {
288 let width = self.width().min(area.width);
289 let click_area = Rect::new(area.x, area.y, width, 1);
290
291 let line = self.build_line();
292 let paragraph = Paragraph::new(line);
293 paragraph.render(area, buf);
294
295 ClickRegion::new(click_area, CheckBoxAction::Toggle)
296 }
297}
298
299impl Widget for CheckBox<'_> {
300 fn render(self, area: Rect, buf: &mut Buffer) {
301 let line = self.build_line();
302 let paragraph = Paragraph::new(line);
303 paragraph.render(area, buf);
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_state_default() {
313 let state = CheckBoxState::default();
314 assert!(!state.checked);
315 assert!(!state.focused);
316 assert!(state.enabled);
317 }
318
319 #[test]
320 fn test_state_new() {
321 let state = CheckBoxState::new(true);
322 assert!(state.checked);
323 assert!(!state.focused);
324 assert!(state.enabled);
325 }
326
327 #[test]
328 fn test_toggle() {
329 let mut state = CheckBoxState::new(false);
330 assert!(!state.checked);
331
332 state.toggle();
333 assert!(state.checked);
334
335 state.toggle();
336 assert!(!state.checked);
337 }
338
339 #[test]
340 fn test_toggle_disabled() {
341 let mut state = CheckBoxState::new(false);
342 state.enabled = false;
343
344 state.toggle();
345 assert!(!state.checked); }
347
348 #[test]
349 fn test_set_checked() {
350 let mut state = CheckBoxState::new(false);
351
352 state.set_checked(true);
353 assert!(state.checked);
354
355 state.set_checked(false);
356 assert!(!state.checked);
357 }
358
359 #[test]
360 fn test_set_checked_disabled() {
361 let mut state = CheckBoxState::new(false);
362 state.enabled = false;
363
364 state.set_checked(true);
365 assert!(!state.checked); }
367
368 #[test]
369 fn test_style_default() {
370 let style = CheckBoxStyle::default();
371 assert_eq!(style.checked_symbol, "[x]");
372 assert_eq!(style.unchecked_symbol, "[ ]");
373 }
374
375 #[test]
376 fn test_style_unicode() {
377 let style = CheckBoxStyle::unicode();
378 assert_eq!(style.checked_symbol, "☑");
379 assert_eq!(style.unchecked_symbol, "☐");
380 }
381
382 #[test]
383 fn test_style_checkmark() {
384 let style = CheckBoxStyle::checkmark();
385 assert_eq!(style.checked_symbol, "✓");
386 assert_eq!(style.unchecked_symbol, "○");
387 }
388
389 #[test]
390 fn test_style_custom() {
391 let style = CheckBoxStyle::custom("ON", "OFF");
392 assert_eq!(style.checked_symbol, "ON");
393 assert_eq!(style.unchecked_symbol, "OFF");
394 }
395
396 #[test]
397 fn test_checkbox_width() {
398 let state = CheckBoxState::new(false);
399 let checkbox = CheckBox::new("Test", &state);
400
401 assert_eq!(checkbox.width(), 8);
403 }
404
405 #[test]
406 fn test_checkbox_width_unicode() {
407 let state = CheckBoxState::new(true);
408 let checkbox = CheckBox::new("Test", &state).style(CheckBoxStyle::unicode());
409
410 assert_eq!(checkbox.width(), 6);
412 }
413
414 #[test]
415 fn test_render_basic() {
416 let state = CheckBoxState::new(true);
417 let checkbox = CheckBox::new("Test", &state);
418
419 let area = Rect::new(0, 0, 20, 1);
420 let mut buffer = Buffer::empty(area);
421
422 checkbox.render(area, &mut buffer);
423
424 let content: String = (0..8)
426 .map(|x| buffer[(x, 0)].symbol().to_string())
427 .collect();
428 assert!(content.contains("[x]"));
429 }
430
431 #[test]
432 fn test_render_stateful() {
433 let state = CheckBoxState::new(false);
434 let checkbox = CheckBox::new("Click me", &state);
435
436 let area = Rect::new(5, 3, 20, 1);
437 let mut buffer = Buffer::empty(Rect::new(0, 0, 30, 10));
438
439 let click_region = checkbox.render_stateful(area, &mut buffer);
440
441 assert_eq!(click_region.area.x, 5);
443 assert_eq!(click_region.area.y, 3);
444 assert_eq!(click_region.data, CheckBoxAction::Toggle);
445 }
446
447 #[test]
448 fn test_click_region_detection() {
449 let state = CheckBoxState::new(false);
450 let checkbox = CheckBox::new("Test", &state);
451
452 let area = Rect::new(10, 5, 20, 1);
453 let mut buffer = Buffer::empty(Rect::new(0, 0, 40, 10));
454
455 let click_region = checkbox.render_stateful(area, &mut buffer);
456
457 assert!(click_region.contains(10, 5));
459 assert!(click_region.contains(15, 5));
460
461 assert!(!click_region.contains(9, 5));
463 assert!(!click_region.contains(10, 4));
464 assert!(!click_region.contains(10, 6));
465 }
466}