Skip to main content

rust_pixel/ui/components/
checkbox.rs

1// RustPixel UI Framework - Checkbox Component
2// copyright zipxing@hotmail.com 2022~2026
3
4//! Checkbox component - character-cell checkbox with label.
5
6use crate::context::Context;
7use crate::render::Buffer;
8use crate::render::style::{Style, Color};
9use crate::util::Rect;
10use crate::ui::{
11    Widget, BaseWidget, WidgetId, WidgetState, UIEvent, UIResult,
12    next_widget_id,
13};
14use crate::impl_widget_base;
15
16/// Checkbox widget: a toggleable checkbox with optional label.
17pub struct Checkbox {
18    base: BaseWidget,
19    checked: bool,
20    label: String,
21    style: Style,
22    checked_style: Style,
23    on_change: Option<Box<dyn Fn(bool) + 'static>>,
24}
25
26impl Checkbox {
27    pub fn new(label: &str) -> Self {
28        let id = next_widget_id();
29        Self {
30            base: BaseWidget::new(id),
31            checked: false,
32            label: label.to_string(),
33            style: Style::default().fg(Color::White).bg(Color::Black),
34            checked_style: Style::default().fg(Color::Green).bg(Color::Black),
35            on_change: None,
36        }
37    }
38
39    pub fn with_checked(mut self, checked: bool) -> Self {
40        self.checked = checked;
41        self
42    }
43
44    pub fn with_style(mut self, style: Style) -> Self {
45        self.style = style;
46        self
47    }
48
49    pub fn with_checked_style(mut self, style: Style) -> Self {
50        self.checked_style = style;
51        self
52    }
53
54    pub fn on_change<F>(mut self, callback: F) -> Self
55    where
56        F: Fn(bool) + 'static,
57    {
58        self.on_change = Some(Box::new(callback));
59        self
60    }
61
62    pub fn set_checked(&mut self, checked: bool) {
63        if self.checked != checked {
64            self.checked = checked;
65            self.mark_dirty();
66            if let Some(ref callback) = self.on_change {
67                callback(checked);
68            }
69        }
70    }
71
72    pub fn is_checked(&self) -> bool {
73        self.checked
74    }
75
76    pub fn toggle(&mut self) {
77        self.set_checked(!self.checked);
78    }
79}
80
81impl Widget for Checkbox {
82    impl_widget_base!(Checkbox, base);
83
84    fn render(&self, buffer: &mut Buffer, _ctx: &Context) -> UIResult<()> {
85        if !self.state().visible { return Ok(()); }
86        let b = self.bounds();
87        if b.width == 0 || b.height == 0 { return Ok(()); }
88
89        // Check if position is within buffer bounds
90        let buffer_area = *buffer.area();
91        if b.y >= buffer_area.y + buffer_area.height || b.x >= buffer_area.x + buffer_area.width {
92            return Ok(());
93        }
94
95        // Render checkbox symbol
96        let checkbox_symbol = if self.checked { "[✓]" } else { "[ ]" };
97        let checkbox_style = if self.checked { self.checked_style } else { self.style };
98        
99        // Ensure we don't exceed buffer width
100        if b.x + 3 < buffer_area.x + buffer_area.width {
101            buffer.set_string(b.x, b.y, checkbox_symbol, checkbox_style);
102        }
103
104        // Render label if there's space
105        if b.width > 4 && !self.label.is_empty() {
106            let label_x = b.x + 4;
107            if label_x < buffer_area.x + buffer_area.width {
108                let max_len = (b.width.saturating_sub(4)).min(buffer_area.width.saturating_sub(label_x - buffer_area.x)) as usize;
109                let label_text = if self.label.len() > max_len {
110                    &self.label[..max_len]
111                } else {
112                    &self.label
113                };
114                buffer.set_string(label_x, b.y, label_text, self.style);
115            }
116        }
117
118        Ok(())
119    }
120
121    fn handle_event(&mut self, event: &UIEvent, _ctx: &mut Context) -> UIResult<bool> {
122        if !self.state().visible { return Ok(false); }
123
124        // Handle mouse click
125        if let UIEvent::Input(crate::event::Event::Mouse(mouse_event)) = event {
126            if self.hit_test(mouse_event.column, mouse_event.row) {
127                if let crate::event::MouseEventKind::Down(crate::event::MouseButton::Left) = mouse_event.kind {
128                    self.toggle();
129                    return Ok(true);
130                }
131            }
132        }
133
134        // Handle space key
135        if let UIEvent::Input(crate::event::Event::Key(key)) = event {
136            if key.code == crate::event::KeyCode::Char(' ') {
137                self.toggle();
138                return Ok(true);
139            }
140        }
141
142        Ok(false)
143    }
144
145    fn preferred_size(&self, available: Rect) -> Rect {
146        // Prefer a single row, width based on label length
147        let width = (4 + self.label.len() as u16).min(available.width);
148        Rect::new(available.x, available.y, width, 1)
149    }
150}
151