rust_pixel/ui/components/
checkbox.rs1use 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
16pub 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 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 let checkbox_symbol = if self.checked { "[✓]" } else { "[ ]" };
97 let checkbox_style = if self.checked { self.checked_style } else { self.style };
98
99 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 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 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 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 let width = (4 + self.label.len() as u16).min(available.width);
148 Rect::new(available.x, available.y, width, 1)
149 }
150}
151