agg_gui/widgets/
checkbox.rs1use std::cell::Cell;
15use std::rc::Rc;
16use std::sync::Arc;
17
18use crate::color::Color;
19use crate::event::{Event, EventResult, Key, MouseButton};
20use crate::geometry::{Rect, Size};
21use crate::draw_ctx::DrawCtx;
22use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
23use crate::text::Font;
24use crate::widget::{Widget, paint_subtree};
25use crate::widgets::label::Label;
26
27const BOX_SIZE: f64 = 16.0;
28const GAP: f64 = 8.0;
29
30pub struct Checkbox {
32 bounds: Rect,
33 children: Vec<Box<dyn Widget>>, base: WidgetBase,
35 font: Arc<Font>,
36 font_size: f64,
37 label_color: Option<Color>,
39 checked: bool,
40 state_cell: Option<Rc<Cell<bool>>>,
44 hovered: bool,
45 focused: bool,
46 on_change: Option<Box<dyn FnMut(bool)>>,
47 label_widget: Label,
49}
50
51impl Checkbox {
52 pub fn new(label: impl Into<String>, font: Arc<Font>, checked: bool) -> Self {
53 let label_str: String = label.into();
54 let font_size = 14.0;
55 let label_widget = Label::new(&label_str, Arc::clone(&font))
56 .with_font_size(font_size);
57 Self {
58 bounds: Rect::default(),
59 children: Vec::new(),
60 base: WidgetBase::new(),
61 font,
62 font_size,
63 label_color: None,
64 checked,
65 state_cell: None,
66 hovered: false,
67 focused: false,
68 on_change: None,
69 label_widget,
70 }
71 }
72
73 pub fn with_font_size(mut self, size: f64) -> Self {
74 self.font_size = size;
75 self.label_widget = Label::new(
76 self.label_widget.text_str(),
77 Arc::clone(&self.font),
78 ).with_font_size(size);
79 self
80 }
81 pub fn with_label_color(mut self, c: Color) -> Self { self.label_color = Some(c); self }
82
83 pub fn with_state_cell(mut self, cell: Rc<Cell<bool>>) -> Self {
89 self.state_cell = Some(cell);
90 self
91 }
92
93 pub fn with_margin(mut self, m: Insets) -> Self { self.base.margin = m; self }
94 pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
95 pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
96 pub fn with_min_size(mut self, s: Size) -> Self { self.base.min_size = s; self }
97 pub fn with_max_size(mut self, s: Size) -> Self { self.base.max_size = s; self }
98
99 pub fn on_change(mut self, cb: impl FnMut(bool) + 'static) -> Self {
100 self.on_change = Some(Box::new(cb));
101 self
102 }
103
104 pub fn checked(&self) -> bool { self.checked }
105 pub fn set_checked(&mut self, v: bool) { self.checked = v; }
106
107 fn toggle(&mut self) {
108 let new_val = !self.effective_checked();
109 self.checked = new_val;
110 if let Some(ref cell) = self.state_cell { cell.set(new_val); }
111 if let Some(cb) = self.on_change.as_mut() { cb(new_val); }
112 }
113
114 #[inline]
117 fn effective_checked(&self) -> bool {
118 if let Some(ref cell) = self.state_cell { cell.get() } else { self.checked }
119 }
120}
121
122impl Widget for Checkbox {
123 fn type_name(&self) -> &'static str { "Checkbox" }
124 fn bounds(&self) -> Rect { self.bounds }
125 fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
126 fn children(&self) -> &[Box<dyn Widget>] { &self.children }
127 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
128
129 fn is_focusable(&self) -> bool { true }
130
131 fn margin(&self) -> Insets { self.base.margin }
132 fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
133 fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
134 fn min_size(&self) -> Size { self.base.min_size }
135 fn max_size(&self) -> Size { self.base.max_size }
136
137 fn layout(&mut self, available: Size) -> Size {
138 let h = BOX_SIZE.max(self.font_size * 1.5);
139 self.bounds = Rect::new(0.0, 0.0, available.width, h);
140 let label_avail_w = (available.width - BOX_SIZE - GAP).max(0.0);
142 let s = self.label_widget.layout(Size::new(label_avail_w, h));
143 self.label_widget.set_bounds(Rect::new(0.0, 0.0, s.width, s.height));
144 Size::new(available.width, h)
145 }
146
147 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
148 let v = ctx.visuals();
149 let h = self.bounds.height;
150 let box_y = (h - BOX_SIZE) * 0.5;
151
152 if self.focused {
154 ctx.set_stroke_color(v.accent_focus);
155 ctx.set_line_width(2.0);
156 ctx.begin_path();
157 ctx.rounded_rect(-1.5, box_y - 1.5, BOX_SIZE + 3.0, BOX_SIZE + 3.0, 4.0);
158 ctx.stroke();
159 }
160
161 let checked = self.effective_checked();
162
163 let bg = if checked {
165 v.accent
166 } else if self.hovered {
167 v.widget_bg_hovered
168 } else {
169 v.widget_bg
170 };
171 ctx.set_fill_color(bg);
172 ctx.begin_path();
173 ctx.rounded_rect(0.0, box_y, BOX_SIZE, BOX_SIZE, 3.0);
174 ctx.fill();
175
176 let border = if checked { v.widget_stroke_active } else { v.widget_stroke };
178 ctx.set_stroke_color(border);
179 ctx.set_line_width(1.5);
180 ctx.begin_path();
181 ctx.rounded_rect(0.0, box_y, BOX_SIZE, BOX_SIZE, 3.0);
182 ctx.stroke();
183
184 if checked {
186 ctx.set_stroke_color(Color::white());
187 ctx.set_line_width(2.0);
188 ctx.begin_path();
189 let bx = 0.0;
190 let by = box_y;
191 ctx.move_to(bx + 3.0, by + BOX_SIZE * 0.55);
192 ctx.line_to(bx + BOX_SIZE * 0.42, by + BOX_SIZE * 0.28);
193 ctx.line_to(bx + BOX_SIZE - 3.0, by + BOX_SIZE * 0.75);
194 ctx.stroke();
195 }
196
197 let label_color = self.label_color.unwrap_or(v.text_color);
199 self.label_widget.set_color(label_color);
200
201 let lw = self.label_widget.bounds().width;
202 let lh = self.label_widget.bounds().height;
203 let lx = BOX_SIZE + GAP;
204 let ly = (h - lh) * 0.5;
205 self.label_widget.set_bounds(Rect::new(lx, ly, lw, lh));
206
207 ctx.save();
208 ctx.translate(lx, ly);
209 paint_subtree(&mut self.label_widget, ctx);
210 ctx.restore();
211 }
212
213 fn on_event(&mut self, event: &Event) -> EventResult {
214 match event {
215 Event::MouseMove { pos } => {
216 let was = self.hovered;
217 self.hovered = self.hit_test(*pos);
218 if was != self.hovered { crate::animation::request_tick(); }
219 EventResult::Ignored
220 }
221 Event::MouseDown { button: MouseButton::Left, .. } => {
222 EventResult::Consumed
223 }
224 Event::MouseUp { button: MouseButton::Left, pos, .. } => {
225 if self.hit_test(*pos) {
226 self.toggle();
227 crate::animation::request_tick();
228 }
229 EventResult::Consumed
230 }
231 Event::KeyDown { key: Key::Char(' '), .. } => {
232 self.toggle();
233 crate::animation::request_tick();
234 EventResult::Consumed
235 }
236 Event::FocusGained => {
237 self.focused = true;
238 crate::animation::request_tick();
239 EventResult::Ignored
240 }
241 Event::FocusLost => {
242 self.focused = false;
243 crate::animation::request_tick();
244 EventResult::Ignored
245 }
246 _ => EventResult::Ignored,
247 }
248 }
249}