Skip to main content

ratatui_form/field/
checkbox.rs

1//! Checkbox field.
2
3use 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
15/// A checkbox field.
16pub struct Checkbox {
17    id: String,
18    label: String,
19    checked: bool,
20    required: bool,
21}
22
23impl Checkbox {
24    /// Creates a new checkbox field.
25    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    /// Sets the initial checked state.
35    pub fn checked(mut self, checked: bool) -> Self {
36        self.checked = checked;
37        self
38    }
39
40    /// Marks this field as required (must be checked).
41    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        // Render checkbox
78        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        // Render label
87        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}