1use std::marker::PhantomData;
34
35use crate::core::alignment;
36use crate::core::layout;
37use crate::core::mouse;
38use crate::core::renderer;
39use crate::core::text;
40use crate::core::theme::palette;
41use crate::core::touch;
42use crate::core::widget;
43use crate::core::widget::tree::{self, Tree};
44use crate::core::window;
45use crate::core::{
46 Background, Border, Color, Event, Font, Layout, Length, Pixels, Rectangle, Shell, Size, Theme,
47 Widget,
48};
49
50pub struct Checkbox<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
83where
84 Theme: Catalog,
85 Renderer: text::Renderer,
86{
87 is_checked: bool,
88 on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
89 label: Option<text::Fragment<'a>>,
90 width: Length,
91 size: f32,
92 spacing: f32,
93 text_size: Option<Pixels>,
94 line_height: Option<text::LineHeight>,
95 shaping: text::Shaping,
96 wrapping: text::Wrapping,
97 font: Option<Font>,
98 icon: Icon,
99 class: Theme::Class<'a>,
100 last_status: Option<Status>,
101 renderer_: PhantomData<Renderer>,
102}
103
104impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer>
105where
106 Renderer: text::Renderer,
107 Theme: Catalog,
108{
109 const DEFAULT_SIZE: f32 = 16.0;
111
112 pub fn new(is_checked: bool) -> Self {
117 Checkbox {
118 is_checked,
119 on_toggle: None,
120 label: None,
121 width: Length::Fit,
122 size: Self::DEFAULT_SIZE,
123 spacing: Self::DEFAULT_SIZE / 2.0,
124 text_size: None,
125 line_height: None,
126 shaping: text::Shaping::default(),
127 wrapping: text::Wrapping::default(),
128 font: None,
129 icon: Icon {
130 font: Renderer::ICON_FONT,
131 code_point: Renderer::CHECKMARK_ICON,
132 size: None,
133 line_height: None,
134 shaping: text::Shaping::Basic,
135 },
136 class: Theme::default(),
137 last_status: None,
138 renderer_: PhantomData,
139 }
140 }
141
142 pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
144 self.label = Some(label.into_fragment());
145 self
146 }
147
148 pub fn on_toggle<F>(mut self, f: F) -> Self
154 where
155 F: 'a + Fn(bool) -> Message,
156 {
157 self.on_toggle = Some(Box::new(f));
158 self
159 }
160
161 pub fn on_toggle_maybe<F>(mut self, f: Option<F>) -> Self
166 where
167 F: Fn(bool) -> Message + 'a,
168 {
169 self.on_toggle = f.map(|f| Box::new(f) as _);
170 self
171 }
172
173 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
175 self.size = size.into().0;
176 self
177 }
178
179 pub fn width(mut self, width: impl Into<Length>) -> Self {
181 self.width = width.into();
182 self
183 }
184
185 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
187 self.spacing = spacing.into().0;
188 self
189 }
190
191 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
193 self.text_size = Some(text_size.into());
194 self
195 }
196
197 pub fn line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
199 self.line_height = Some(line_height.into());
200 self
201 }
202
203 pub fn shaping(mut self, shaping: text::Shaping) -> Self {
205 self.shaping = shaping;
206 self
207 }
208
209 pub fn wrapping(mut self, wrapping: text::Wrapping) -> Self {
211 self.wrapping = wrapping;
212 self
213 }
214
215 pub fn font(mut self, font: impl Into<Font>) -> Self {
219 self.font = Some(font.into());
220 self
221 }
222
223 pub fn icon(mut self, icon: Icon) -> Self {
225 self.icon = icon;
226 self
227 }
228
229 #[must_use]
231 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
232 where
233 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
234 {
235 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
236 self
237 }
238
239 #[cfg(feature = "advanced")]
241 #[must_use]
242 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
243 self.class = class.into();
244 self
245 }
246}
247
248impl<Message, Theme, Renderer> widget::Meta for Checkbox<'_, Message, Theme, Renderer>
249where
250 Theme: Catalog,
251 Renderer: text::Renderer,
252{
253}
254
255impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
256 for Checkbox<'_, Message, Theme, Renderer>
257where
258 Renderer: text::Renderer,
259 Theme: Catalog,
260{
261 fn tag(&self) -> tree::Tag {
262 tree::Tag::of::<text::paragraph::Plain<Renderer::Paragraph>>()
263 }
264
265 fn state(&self) -> tree::State {
266 tree::State::new(text::paragraph::Plain::<Renderer::Paragraph>::default())
267 }
268
269 fn size(&self) -> Size<Length> {
270 Size {
271 width: self.width,
272 height: Length::Fit,
273 }
274 }
275
276 fn diff(&mut self, tree: &mut Tree) {
277 tree.children.resize_with(2, Tree::empty);
280 }
281
282 fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits) {
283 let limits = limits.width(self.width);
284
285 let checkbox = Size::new(self.size, self.size);
286 let spacing = if self.label.is_some() {
287 self.spacing
288 } else {
289 0.0
290 };
291
292 let label = if let Some(label) = self.label.as_deref() {
293 let state = tree
294 .state
295 .downcast_mut::<text::paragraph::Plain<Renderer::Paragraph>>();
296
297 widget::text::layout(
298 state,
299 renderer,
300 &limits.shrink(Size::new(checkbox.width + spacing, 0.0)),
301 label,
302 widget::text::Format {
303 width: self.width,
304 height: Length::Fit,
305 line_height: self.line_height,
306 size: self.text_size,
307 font: self.font,
308 align_x: text::Alignment::Default,
309 align_y: alignment::Vertical::Top,
310 shaping: self.shaping,
311 wrapping: self.wrapping,
312 ellipsis: text::Ellipsis::None,
313 },
314 )
315 } else {
316 Size::ZERO
317 };
318
319 layout::next_to_each_other(tree, checkbox, label, spacing);
320 }
321
322 fn update(
323 &mut self,
324 _tree: &mut Tree,
325 event: &Event,
326 layout: Layout,
327 cursor: mouse::Cursor,
328 _renderer: &Renderer,
329 shell: &mut Shell<'_, Message>,
330 _viewport: &Rectangle,
331 ) {
332 match event {
333 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
334 | Event::Touch(touch::Event::FingerPressed { .. }) => {
335 let mouse_over = cursor.is_over(layout.bounds());
336
337 if mouse_over && let Some(on_toggle) = &self.on_toggle {
338 shell.publish((on_toggle)(!self.is_checked));
339 shell.capture_event();
340 }
341 }
342 _ => {}
343 }
344
345 let current_status = {
346 let is_mouse_over = cursor.is_over(layout.bounds());
347 let is_disabled = self.on_toggle.is_none();
348 let is_checked = self.is_checked;
349
350 if is_disabled {
351 Status::Disabled { is_checked }
352 } else if is_mouse_over {
353 Status::Hovered { is_checked }
354 } else {
355 Status::Active { is_checked }
356 }
357 };
358
359 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
360 self.last_status = Some(current_status);
361 } else if self
362 .last_status
363 .is_some_and(|status| status != current_status)
364 {
365 shell.request_redraw();
366 }
367 }
368
369 fn mouse_interaction(
370 &self,
371 _tree: &Tree,
372 layout: Layout,
373 cursor: mouse::Cursor,
374 _viewport: &Rectangle,
375 _renderer: &Renderer,
376 ) -> mouse::Interaction {
377 if cursor.is_over(layout.bounds()) && self.on_toggle.is_some() {
378 mouse::Interaction::Pointer
379 } else {
380 mouse::Interaction::default()
381 }
382 }
383
384 fn draw(
385 &self,
386 tree: &Tree,
387 renderer: &mut Renderer,
388 theme: &Theme,
389 defaults: &renderer::Style,
390 layout: Layout,
391 _cursor: mouse::Cursor,
392 viewport: &Rectangle,
393 ) {
394 let mut children = layout.iter(&tree.children);
395
396 let style = theme.style(
397 &self.class,
398 self.last_status.unwrap_or(Status::Disabled {
399 is_checked: self.is_checked,
400 }),
401 );
402
403 {
404 let (checkbox_layout, _) = children.next().unwrap();
405 let bounds = checkbox_layout.bounds();
406
407 renderer.fill_quad(
408 renderer::Quad {
409 bounds,
410 border: style.border,
411 ..renderer::Quad::default()
412 },
413 style.background,
414 );
415
416 let Icon {
417 font,
418 code_point,
419 size,
420 line_height,
421 shaping,
422 } = &self.icon;
423 let size = size.unwrap_or(Pixels(bounds.height * 0.7));
424 let line_height = line_height.unwrap_or_else(|| renderer.line_height());
425
426 if self.is_checked {
427 renderer.fill_text(
428 text::Text {
429 content: code_point.to_string(),
430 font: *font,
431 size,
432 line_height,
433 bounds: bounds.size(),
434 align_x: text::Alignment::Center,
435 align_y: alignment::Vertical::Center,
436 shaping: *shaping,
437 wrapping: text::Wrapping::default(),
438 ellipsis: text::Ellipsis::default(),
439 hint_factor: None,
440 },
441 bounds.center(),
442 style.icon_color,
443 *viewport,
444 );
445 }
446 }
447
448 if self.label.is_none() {
449 return;
450 }
451
452 let (label_layout, _) = children.next().unwrap();
453 let state = tree
454 .state
455 .downcast_ref::<text::paragraph::Plain<Renderer::Paragraph>>();
456
457 crate::text::draw(
458 renderer,
459 defaults,
460 label_layout.bounds(),
461 state.raw(),
462 crate::text::Style {
463 color: style.text_color,
464 selection: None,
465 },
466 theme.selection(),
467 viewport,
468 );
469 }
470
471 fn operate(
472 &mut self,
473 tree: &mut Tree,
474 layout: Layout,
475 _viewport: &Rectangle,
476 _renderer: &Renderer,
477 operation: &mut dyn widget::Operation,
478 ) {
479 if self.label.is_none() {
480 return;
481 }
482
483 let paragraph = tree
484 .state
485 .downcast_mut::<text::paragraph::Plain<Renderer::Paragraph>>();
486
487 let mut children = layout.iter(&tree.children);
488 let (layout, _) = children.next().unwrap();
489
490 operation.text(
491 None,
492 layout.bounds(),
493 &mut widget::text::Operand {
494 paragraph,
495 layout,
496 selectable: true,
497 },
498 );
499 }
500}
501
502#[derive(Debug, Clone, PartialEq)]
504pub struct Icon {
505 pub font: Font,
507 pub code_point: char,
509 pub size: Option<Pixels>,
511 pub line_height: Option<text::LineHeight>,
513 pub shaping: text::Shaping,
515}
516
517#[derive(Debug, Clone, Copy, PartialEq, Eq)]
519pub enum Status {
520 Active {
522 is_checked: bool,
524 },
525 Hovered {
527 is_checked: bool,
529 },
530 Disabled {
532 is_checked: bool,
534 },
535}
536
537#[derive(Debug, Clone, Copy, PartialEq)]
539pub struct Style {
540 pub background: Background,
542 pub icon_color: Color,
544 pub border: Border,
546 pub text_color: Option<Color>,
548}
549
550pub trait Catalog: Sized {
552 type Class<'a>;
554
555 fn default<'a>() -> Self::Class<'a>;
557
558 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
560
561 fn selection(&self) -> Color;
563}
564
565pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
569
570impl Catalog for Theme {
571 type Class<'a> = StyleFn<'a, Self>;
572
573 fn default<'a>() -> Self::Class<'a> {
574 Box::new(primary)
575 }
576
577 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
578 class(self, status)
579 }
580
581 fn selection(&self) -> Color {
582 self.palette().background.strongest.color
583 }
584}
585
586pub fn primary(theme: &Theme, status: Status) -> Style {
588 let palette = theme.palette();
589
590 match status {
591 Status::Active { is_checked } => styled(
592 palette.background.strong.color,
593 palette.background.base,
594 palette.primary.base.text,
595 palette.primary.base,
596 is_checked,
597 ),
598 Status::Hovered { is_checked } => styled(
599 palette.background.strong.color,
600 palette.background.weak,
601 palette.primary.base.text,
602 palette.primary.strong,
603 is_checked,
604 ),
605 Status::Disabled { is_checked } => styled(
606 palette.background.weak.color,
607 palette.background.weaker,
608 palette.primary.base.text,
609 palette.background.strong,
610 is_checked,
611 ),
612 }
613}
614
615pub fn secondary(theme: &Theme, status: Status) -> Style {
617 let palette = theme.palette();
618
619 match status {
620 Status::Active { is_checked } => styled(
621 palette.background.strong.color,
622 palette.background.base,
623 palette.background.base.text,
624 palette.background.strong,
625 is_checked,
626 ),
627 Status::Hovered { is_checked } => styled(
628 palette.background.strong.color,
629 palette.background.weak,
630 palette.background.base.text,
631 palette.background.strong,
632 is_checked,
633 ),
634 Status::Disabled { is_checked } => styled(
635 palette.background.weak.color,
636 palette.background.weak,
637 palette.background.base.text,
638 palette.background.weak,
639 is_checked,
640 ),
641 }
642}
643
644pub fn success(theme: &Theme, status: Status) -> Style {
646 let palette = theme.palette();
647
648 match status {
649 Status::Active { is_checked } => styled(
650 palette.background.weak.color,
651 palette.background.base,
652 palette.success.base.text,
653 palette.success.base,
654 is_checked,
655 ),
656 Status::Hovered { is_checked } => styled(
657 palette.background.strong.color,
658 palette.background.weak,
659 palette.success.base.text,
660 palette.success.strong,
661 is_checked,
662 ),
663 Status::Disabled { is_checked } => styled(
664 palette.background.weak.color,
665 palette.background.weak,
666 palette.success.base.text,
667 palette.success.weak,
668 is_checked,
669 ),
670 }
671}
672
673pub fn danger(theme: &Theme, status: Status) -> Style {
675 let palette = theme.palette();
676
677 match status {
678 Status::Active { is_checked } => styled(
679 palette.background.strong.color,
680 palette.background.base,
681 palette.danger.base.text,
682 palette.danger.base,
683 is_checked,
684 ),
685 Status::Hovered { is_checked } => styled(
686 palette.background.strong.color,
687 palette.background.weak,
688 palette.danger.base.text,
689 palette.danger.strong,
690 is_checked,
691 ),
692 Status::Disabled { is_checked } => styled(
693 palette.background.weak.color,
694 palette.background.weak,
695 palette.danger.base.text,
696 palette.danger.weak,
697 is_checked,
698 ),
699 }
700}
701
702fn styled(
703 border_color: Color,
704 base: palette::Pair,
705 icon_color: Color,
706 accent: palette::Pair,
707 is_checked: bool,
708) -> Style {
709 let (background, border) = if is_checked {
710 (accent, accent.color)
711 } else {
712 (base, border_color)
713 };
714
715 Style {
716 background: Background::Color(background.color),
717 icon_color,
718 border: Border {
719 radius: 2.0.into(),
720 width: 1.0,
721 color: border,
722 },
723 text_color: None,
724 }
725}