Skip to main content

tui/components/
checkbox.rs

1use crossterm::event::KeyCode;
2
3use crate::components::{Component, Event, ViewContext};
4use crate::line::Line;
5use crate::rendering::frame::Frame;
6use crate::style::Style;
7
8/// Boolean toggle rendered as `[x]` / `[ ]`, optionally with an inline label: `[x] Label`.
9pub struct Checkbox {
10    pub checked: bool,
11    label: Option<String>,
12}
13
14impl Checkbox {
15    pub fn new(checked: bool) -> Self {
16        Self { checked, label: None }
17    }
18
19    pub fn with_label(mut self, label: impl Into<String>) -> Self {
20        self.label = Some(label.into());
21        self
22    }
23
24    pub fn to_json(&self) -> serde_json::Value {
25        serde_json::Value::Bool(self.checked)
26    }
27}
28
29impl Component for Checkbox {
30    type Message = ();
31
32    async fn on_event(&mut self, event: &Event) -> Option<Vec<Self::Message>> {
33        let Event::Key(key) = event else {
34            return None;
35        };
36        match key.code {
37            KeyCode::Char(' ') => {
38                self.checked = !self.checked;
39                Some(vec![])
40            }
41            _ => None,
42        }
43    }
44
45    fn render(&mut self, context: &ViewContext) -> Frame {
46        Frame::new(self.render_field(context, true))
47    }
48}
49
50impl Checkbox {
51    pub fn render_field(&self, context: &ViewContext, focused: bool) -> Vec<Line> {
52        let marker = if self.checked { "[x]" } else { "[ ]" };
53        let marker_color = if focused { context.theme.primary() } else { context.theme.text_primary() };
54        let mut line = Line::styled(marker, marker_color);
55        if let Some(label) = &self.label {
56            line.push_with_style(format!(" {label}"), Style::fg(context.theme.text_primary()));
57        }
58        vec![line]
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use crossterm::event::{KeyEvent, KeyModifiers};
66
67    fn key(code: KeyCode) -> KeyEvent {
68        KeyEvent::new(code, KeyModifiers::NONE)
69    }
70
71    #[tokio::test]
72    async fn space_toggles() {
73        let mut cb = Checkbox::new(false);
74        cb.on_event(&Event::Key(key(KeyCode::Char(' ')))).await;
75        assert!(cb.checked);
76        cb.on_event(&Event::Key(key(KeyCode::Char(' ')))).await;
77        assert!(!cb.checked);
78    }
79
80    #[test]
81    fn to_json_returns_bool() {
82        assert_eq!(Checkbox::new(true).to_json(), serde_json::json!(true));
83        assert_eq!(Checkbox::new(false).to_json(), serde_json::json!(false));
84    }
85
86    #[tokio::test]
87    async fn other_keys_are_ignored() {
88        let mut cb = Checkbox::new(false);
89        let outcome = cb.on_event(&Event::Key(key(KeyCode::Char('a')))).await;
90        assert!(outcome.is_none());
91        assert!(!cb.checked);
92    }
93}