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