1use std::cell::Cell;
15use std::rc::Rc;
16use std::sync::Arc;
17
18use crate::color::Color;
19use crate::draw_ctx::DrawCtx;
20use crate::event::{Event, EventResult, Key, MouseButton};
21use crate::geometry::{Rect, Size};
22use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
23use crate::text::Font;
24use crate::widget::{paint_subtree, Widget};
25use crate::widgets::label::Label;
26
27const BOX_SIZE: f64 = 16.0;
28const FOCUS_PAD: f64 = 2.0;
29const GAP: f64 = 8.0;
30const BOX_STROKE_WIDTH: f64 = 1.5;
31
32pub struct Checkbox {
34 bounds: Rect,
35 children: Vec<Box<dyn Widget>>, base: WidgetBase,
37 font: Arc<Font>,
38 font_size: f64,
39 label_color: Option<Color>,
41 checked: bool,
42 state_cell: Option<Rc<Cell<bool>>>,
46 hovered: bool,
47 focused: bool,
48 on_change: Option<Box<dyn FnMut(bool)>>,
49 label_widget: Label,
51}
52
53impl Checkbox {
54 pub fn new(label: impl Into<String>, font: Arc<Font>, checked: bool) -> Self {
55 let label_str: String = label.into();
56 let font_size = 14.0;
57 let label_widget = Label::new(&label_str, Arc::clone(&font)).with_font_size(font_size);
58 Self {
59 bounds: Rect::default(),
60 children: Vec::new(),
61 base: WidgetBase::new(),
62 font,
63 font_size,
64 label_color: None,
65 checked,
66 state_cell: None,
67 hovered: false,
68 focused: false,
69 on_change: None,
70 label_widget,
71 }
72 }
73
74 pub fn with_font_size(mut self, size: f64) -> Self {
75 self.font_size = size;
76 self.label_widget =
77 Label::new(self.label_widget.text_str(), Arc::clone(&self.font)).with_font_size(size);
78 self
79 }
80 pub fn with_label_color(mut self, c: Color) -> Self {
81 self.label_color = Some(c);
82 self
83 }
84
85 pub fn with_state_cell(mut self, cell: Rc<Cell<bool>>) -> Self {
91 self.state_cell = Some(cell);
92 self
93 }
94
95 pub fn with_margin(mut self, m: Insets) -> Self {
96 self.base.margin = m;
97 self
98 }
99 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
100 self.base.h_anchor = h;
101 self
102 }
103 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
104 self.base.v_anchor = v;
105 self
106 }
107 pub fn with_min_size(mut self, s: Size) -> Self {
108 self.base.min_size = s;
109 self
110 }
111 pub fn with_max_size(mut self, s: Size) -> Self {
112 self.base.max_size = s;
113 self
114 }
115
116 pub fn on_change(mut self, cb: impl FnMut(bool) + 'static) -> Self {
117 self.on_change = Some(Box::new(cb));
118 self
119 }
120
121 pub fn checked(&self) -> bool {
122 self.checked
123 }
124 pub fn set_checked(&mut self, v: bool) {
125 self.checked = v;
126 }
127
128 fn toggle(&mut self) {
129 let new_val = !self.effective_checked();
130 self.checked = new_val;
131 if let Some(ref cell) = self.state_cell {
132 cell.set(new_val);
133 }
134 if let Some(cb) = self.on_change.as_mut() {
135 cb(new_val);
136 }
137 }
138
139 #[inline]
142 fn effective_checked(&self) -> bool {
143 if let Some(ref cell) = self.state_cell {
144 cell.get()
145 } else {
146 self.checked
147 }
148 }
149
150 fn unchecked_colors(v: &crate::theme::Visuals, hovered: bool) -> (Color, Color) {
151 let luma = v.bg_color.r * 0.299 + v.bg_color.g * 0.587 + v.bg_color.b * 0.114;
152 if luma < 0.5 {
153 let fill = if hovered { v.widget_bg } else { v.window_fill };
154 (fill, Color::rgba(1.0, 1.0, 1.0, 0.34))
155 } else {
156 let fill = if hovered {
157 v.widget_bg_hovered
158 } else {
159 v.widget_bg
160 };
161 (fill, v.widget_stroke)
162 }
163 }
164}
165
166impl Widget for Checkbox {
167 fn type_name(&self) -> &'static str {
168 "Checkbox"
169 }
170 fn bounds(&self) -> Rect {
171 self.bounds
172 }
173 fn set_bounds(&mut self, b: Rect) {
174 self.bounds = b;
175 }
176 fn children(&self) -> &[Box<dyn Widget>] {
177 &self.children
178 }
179 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
180 &mut self.children
181 }
182
183 fn is_focusable(&self) -> bool {
184 true
185 }
186
187 fn margin(&self) -> Insets {
188 self.base.margin
189 }
190 fn h_anchor(&self) -> HAnchor {
191 self.base.h_anchor
192 }
193 fn v_anchor(&self) -> VAnchor {
194 self.base.v_anchor
195 }
196 fn min_size(&self) -> Size {
197 self.base.min_size
198 }
199 fn max_size(&self) -> Size {
200 self.base.max_size
201 }
202
203 fn layout(&mut self, available: Size) -> Size {
204 let box_slot_w = BOX_SIZE + FOCUS_PAD * 2.0;
205 let h = (BOX_SIZE + FOCUS_PAD * 2.0).max(self.font_size * 1.25);
206 let label_avail_w = (available.width - box_slot_w - GAP).max(0.0);
208 let s = self.label_widget.layout(Size::new(label_avail_w, h));
209 self.label_widget
210 .set_bounds(Rect::new(0.0, 0.0, s.width, s.height));
211 let natural_w = if self.label_widget.text_str().is_empty() {
212 box_slot_w
213 } else {
214 box_slot_w + GAP + s.width
215 };
216 let w = natural_w.min(available.width);
217 self.bounds = Rect::new(0.0, 0.0, w, h);
218 Size::new(w, h)
219 }
220
221 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
222 let v = ctx.visuals();
223 let h = self.bounds.height;
224 let box_x = FOCUS_PAD;
225 let box_y = (h - BOX_SIZE) * 0.5;
226
227 if self.focused {
229 ctx.set_stroke_color(v.accent_focus);
230 ctx.set_line_width(2.0);
231 ctx.begin_path();
232 ctx.rounded_rect(
233 box_x - 1.5,
234 box_y - 1.5,
235 BOX_SIZE + 3.0,
236 BOX_SIZE + 3.0,
237 4.0,
238 );
239 ctx.stroke();
240 }
241
242 let checked = self.effective_checked();
243
244 let (unchecked_bg, unchecked_border) = Self::unchecked_colors(&v, self.hovered);
246 let bg = if checked { v.accent } else { unchecked_bg };
247 ctx.set_fill_color(bg);
248 ctx.begin_path();
249 ctx.rounded_rect(box_x, box_y, BOX_SIZE, BOX_SIZE, 3.0);
250 ctx.fill();
251
252 let border = if checked {
254 v.widget_stroke_active
255 } else {
256 unchecked_border
257 };
258 ctx.set_stroke_color(border);
259 ctx.set_line_width(BOX_STROKE_WIDTH);
260 ctx.begin_path();
261 let stroke_inset = BOX_STROKE_WIDTH * 0.5;
262 ctx.rounded_rect(
263 box_x + stroke_inset,
264 box_y + stroke_inset,
265 BOX_SIZE - BOX_STROKE_WIDTH,
266 BOX_SIZE - BOX_STROKE_WIDTH,
267 3.0,
268 );
269 ctx.stroke();
270
271 if checked {
273 ctx.set_stroke_color(Color::white());
274 ctx.set_line_width(2.0);
275 ctx.begin_path();
276 let bx = box_x;
277 let by = box_y;
278 ctx.move_to(bx + 3.0, by + BOX_SIZE * 0.55);
279 ctx.line_to(bx + BOX_SIZE * 0.42, by + BOX_SIZE * 0.28);
280 ctx.line_to(bx + BOX_SIZE - 3.0, by + BOX_SIZE * 0.75);
281 ctx.stroke();
282 }
283
284 let label_color = self.label_color.unwrap_or(v.text_color);
286 self.label_widget.set_color(label_color);
287
288 let lw = self.label_widget.bounds().width;
289 let lh = self.label_widget.bounds().height;
290 let lx = if self.label_widget.text_str().is_empty() {
291 BOX_SIZE + FOCUS_PAD * 2.0
292 } else {
293 BOX_SIZE + FOCUS_PAD * 2.0 + GAP
294 };
295 let ly = (h - lh) * 0.5;
296 self.label_widget.set_bounds(Rect::new(lx, ly, lw, lh));
297
298 ctx.save();
299 ctx.translate(lx, ly);
300 paint_subtree(&mut self.label_widget, ctx);
301 ctx.restore();
302 }
303
304 fn on_event(&mut self, event: &Event) -> EventResult {
305 match event {
306 Event::MouseMove { pos } => {
307 let was = self.hovered;
308 self.hovered = self.hit_test(*pos);
309 if was != self.hovered {
310 crate::animation::request_draw();
311 return EventResult::Consumed;
312 }
313 EventResult::Ignored
314 }
315 Event::MouseDown {
316 button: MouseButton::Left,
317 ..
318 } => EventResult::Consumed,
319 Event::MouseUp {
320 button: MouseButton::Left,
321 pos,
322 ..
323 } => {
324 if self.hit_test(*pos) {
325 self.toggle();
326 crate::animation::request_draw();
327 }
328 EventResult::Consumed
329 }
330 Event::KeyDown {
331 key: Key::Char(' '),
332 ..
333 } => {
334 self.toggle();
335 crate::animation::request_draw();
336 EventResult::Consumed
337 }
338 Event::FocusGained => {
339 let was = self.focused;
340 self.focused = true;
341 if !was {
342 crate::animation::request_draw();
343 EventResult::Consumed
344 } else {
345 EventResult::Ignored
346 }
347 }
348 Event::FocusLost => {
349 let was = self.focused;
350 self.focused = false;
351 if was {
352 crate::animation::request_draw();
353 EventResult::Consumed
354 } else {
355 EventResult::Ignored
356 }
357 }
358 _ => EventResult::Ignored,
359 }
360 }
361}