1use crate::core::alignment;
35use crate::core::keyboard;
36use crate::core::keyboard::key;
37use crate::core::layout;
38use crate::core::mouse;
39use crate::core::renderer;
40use crate::core::text;
41use crate::core::theme::palette;
42use crate::core::touch;
43use crate::core::widget;
44use crate::core::widget::operation::accessible::{Accessible, Role};
45use crate::core::widget::operation::focusable::{self, Focusable};
46use crate::core::widget::tree::{self, Tree};
47use crate::core::window;
48use crate::core::{
49 Background, Border, Color, Element, Event, Layout, Length, Pixels, Rectangle, Shadow, Shell,
50 Size, Theme, Widget,
51};
52
53pub struct Checkbox<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
87where
88 Renderer: text::Renderer,
89 Theme: Catalog,
90{
91 is_checked: bool,
92 on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
93 label: Option<text::Fragment<'a>>,
94 width: Length,
95 size: f32,
96 spacing: f32,
97 text_size: Option<Pixels>,
98 line_height: text::LineHeight,
99 shaping: text::Shaping,
100 wrapping: text::Wrapping,
101 font: Option<Renderer::Font>,
102 icon: Icon<Renderer::Font>,
103 class: Theme::Class<'a>,
104 last_status: Option<Status>,
105}
106
107impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer>
108where
109 Renderer: text::Renderer,
110 Theme: Catalog,
111{
112 const DEFAULT_SIZE: f32 = 16.0;
114
115 pub fn new(is_checked: bool) -> Self {
120 Checkbox {
121 is_checked,
122 on_toggle: None,
123 label: None,
124 width: Length::Shrink,
125 size: Self::DEFAULT_SIZE,
126 spacing: Self::DEFAULT_SIZE / 2.0,
127 text_size: None,
128 line_height: text::LineHeight::default(),
129 shaping: text::Shaping::default(),
130 wrapping: text::Wrapping::default(),
131 font: None,
132 icon: Icon {
133 font: Renderer::ICON_FONT,
134 code_point: Renderer::CHECKMARK_ICON,
135 size: None,
136 line_height: text::LineHeight::default(),
137 shaping: text::Shaping::Basic,
138 },
139 class: Theme::default(),
140 last_status: None,
141 }
142 }
143
144 pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
146 self.label = Some(label.into_fragment());
147 self
148 }
149
150 pub fn on_toggle<F>(mut self, f: F) -> Self
156 where
157 F: 'a + Fn(bool) -> Message,
158 {
159 self.on_toggle = Some(Box::new(f));
160 self
161 }
162
163 pub fn on_toggle_maybe<F>(mut self, f: Option<F>) -> Self
168 where
169 F: Fn(bool) -> Message + 'a,
170 {
171 self.on_toggle = f.map(|f| Box::new(f) as _);
172 self
173 }
174
175 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
177 self.size = size.into().0;
178 self
179 }
180
181 pub fn width(mut self, width: impl Into<Length>) -> Self {
183 self.width = width.into();
184 self
185 }
186
187 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
189 self.spacing = spacing.into().0;
190 self
191 }
192
193 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
195 self.text_size = Some(text_size.into());
196 self
197 }
198
199 pub fn line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
201 self.line_height = line_height.into();
202 self
203 }
204
205 pub fn shaping(mut self, shaping: text::Shaping) -> Self {
207 self.shaping = shaping;
208 self
209 }
210
211 pub fn wrapping(mut self, wrapping: text::Wrapping) -> Self {
213 self.wrapping = wrapping;
214 self
215 }
216
217 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
221 self.font = Some(font.into());
222 self
223 }
224
225 pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self {
227 self.icon = icon;
228 self
229 }
230
231 #[must_use]
233 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
234 where
235 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
236 {
237 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
238 self
239 }
240
241 #[cfg(feature = "advanced")]
243 #[must_use]
244 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
245 self.class = class.into();
246 self
247 }
248}
249
250#[derive(Debug, Clone, Default)]
251struct State<P: text::Paragraph> {
252 is_focused: bool,
253 focus_visible: bool,
254 label: widget::text::State<P>,
255}
256
257impl<P: text::Paragraph> focusable::Focusable for State<P> {
258 fn is_focused(&self) -> bool {
259 self.is_focused
260 }
261
262 fn focus(&mut self) {
263 self.is_focused = true;
264 self.focus_visible = true;
265 }
266
267 fn unfocus(&mut self) {
268 self.is_focused = false;
269 self.focus_visible = false;
270 }
271}
272
273impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
274 for Checkbox<'_, Message, Theme, Renderer>
275where
276 Renderer: text::Renderer,
277 Theme: Catalog,
278{
279 fn tag(&self) -> tree::Tag {
280 tree::Tag::of::<State<Renderer::Paragraph>>()
281 }
282
283 fn state(&self) -> tree::State {
284 tree::State::new(State::<Renderer::Paragraph>::default())
285 }
286
287 fn size(&self) -> Size<Length> {
288 Size {
289 width: self.width,
290 height: Length::Shrink,
291 }
292 }
293
294 fn layout(
295 &mut self,
296 tree: &mut Tree,
297 renderer: &Renderer,
298 limits: &layout::Limits,
299 ) -> layout::Node {
300 layout::next_to_each_other(
301 &limits.width(self.width),
302 if self.label.is_some() {
303 self.spacing
304 } else {
305 0.0
306 },
307 |_| layout::Node::new(Size::new(self.size, self.size)),
308 |limits| {
309 if let Some(label) = self.label.as_deref() {
310 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
311
312 widget::text::layout(
313 &mut state.label,
314 renderer,
315 limits,
316 label,
317 widget::text::Format {
318 width: self.width,
319 height: Length::Shrink,
320 line_height: self.line_height,
321 size: self.text_size,
322 font: self.font,
323 align_x: text::Alignment::Default,
324 align_y: alignment::Vertical::Top,
325 shaping: self.shaping,
326 wrapping: self.wrapping,
327 ellipsis: text::Ellipsis::None,
328 },
329 )
330 } else {
331 layout::Node::new(Size::ZERO)
332 }
333 },
334 )
335 }
336
337 fn update(
338 &mut self,
339 tree: &mut Tree,
340 event: &Event,
341 layout: Layout<'_>,
342 cursor: mouse::Cursor,
343 _renderer: &Renderer,
344 shell: &mut Shell<'_, Message>,
345 _viewport: &Rectangle,
346 ) {
347 match event {
348 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
349 | Event::Touch(touch::Event::FingerPressed { .. }) => {
350 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
351
352 if cursor.is_over(layout.bounds())
353 && let Some(on_toggle) = &self.on_toggle
354 {
355 state.is_focused = true;
356 state.focus_visible = false;
357
358 shell.publish((on_toggle)(!self.is_checked));
359 shell.capture_event();
360 } else {
361 state.is_focused = false;
362 state.focus_visible = false;
363 }
364 }
365 Event::Keyboard(keyboard::Event::KeyPressed {
366 key: keyboard::Key::Named(key::Named::Space),
367 ..
368 }) => {
369 if let Some(on_toggle) = &self.on_toggle {
370 let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
371
372 if state.is_focused {
373 shell.publish((on_toggle)(!self.is_checked));
374 shell.capture_event();
375 }
376 }
377 }
378 Event::Keyboard(keyboard::Event::KeyPressed {
379 key: keyboard::Key::Named(key::Named::Escape),
380 ..
381 }) => {
382 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
383 if state.is_focused {
384 state.is_focused = false;
385 state.focus_visible = false;
386 shell.capture_event();
387 }
388 }
389 _ => {}
390 }
391
392 let current_status = {
393 let is_mouse_over = cursor.is_over(layout.bounds());
394 let is_disabled = self.on_toggle.is_none();
395 let is_checked = self.is_checked;
396
397 if is_disabled {
398 Status::Disabled { is_checked }
399 } else {
400 let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
401
402 if state.focus_visible {
403 Status::Focused { is_checked }
404 } else if is_mouse_over {
405 Status::Hovered { is_checked }
406 } else {
407 Status::Active { is_checked }
408 }
409 }
410 };
411
412 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
413 self.last_status = Some(current_status);
414 } else if self
415 .last_status
416 .is_some_and(|status| status != current_status)
417 {
418 shell.request_redraw();
419 }
420 }
421
422 fn mouse_interaction(
423 &self,
424 _tree: &Tree,
425 layout: Layout<'_>,
426 cursor: mouse::Cursor,
427 _viewport: &Rectangle,
428 _renderer: &Renderer,
429 ) -> mouse::Interaction {
430 if cursor.is_over(layout.bounds()) && self.on_toggle.is_some() {
431 mouse::Interaction::Pointer
432 } else {
433 mouse::Interaction::default()
434 }
435 }
436
437 fn draw(
438 &self,
439 tree: &Tree,
440 renderer: &mut Renderer,
441 theme: &Theme,
442 defaults: &renderer::Style,
443 layout: Layout<'_>,
444 _cursor: mouse::Cursor,
445 viewport: &Rectangle,
446 ) {
447 let mut children = layout.children();
448
449 let style = theme.style(
450 &self.class,
451 self.last_status.unwrap_or(Status::Disabled {
452 is_checked: self.is_checked,
453 }),
454 );
455
456 {
457 let layout = children.next().unwrap();
458 let bounds = layout.bounds();
459
460 renderer.fill_quad(
461 renderer::Quad {
462 bounds,
463 border: style.border,
464 shadow: style.shadow,
465 ..renderer::Quad::default()
466 },
467 style.background,
468 );
469
470 let Icon {
471 font,
472 code_point,
473 size,
474 line_height,
475 shaping,
476 } = &self.icon;
477 let size = size.unwrap_or(Pixels(bounds.height * 0.7));
478
479 if self.is_checked {
480 renderer.fill_text(
481 text::Text {
482 content: code_point.to_string(),
483 font: *font,
484 size,
485 line_height: *line_height,
486 bounds: bounds.size(),
487 align_x: text::Alignment::Center,
488 align_y: alignment::Vertical::Center,
489 shaping: *shaping,
490 wrapping: text::Wrapping::default(),
491 ellipsis: text::Ellipsis::default(),
492 hint_factor: None,
493 },
494 bounds.center(),
495 style.icon_color,
496 *viewport,
497 );
498 }
499 }
500
501 if self.label.is_none() {
502 return;
503 }
504
505 {
506 let label_layout = children.next().unwrap();
507 let state: &State<Renderer::Paragraph> = tree.state.downcast_ref();
508
509 crate::text::draw(
510 renderer,
511 defaults,
512 label_layout.bounds(),
513 state.label.raw(),
514 crate::text::Style {
515 color: style.text_color,
516 },
517 viewport,
518 );
519 }
520 }
521
522 fn operate(
523 &mut self,
524 tree: &mut Tree,
525 layout: Layout<'_>,
526 _renderer: &Renderer,
527 operation: &mut dyn widget::Operation,
528 ) {
529 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
530
531 operation.accessible(
532 None,
533 layout.bounds(),
534 &Accessible {
535 role: Role::CheckBox,
536 label: self.label.as_deref(),
537 toggled: Some(self.is_checked),
538 disabled: self.on_toggle.is_none(),
539 ..Accessible::default()
540 },
541 );
542
543 if self.on_toggle.is_some() {
544 operation.focusable(None, layout.bounds(), state);
545 } else {
546 state.unfocus();
547 }
548
549 if let Some(label) = self.label.as_deref() {
550 operation.text(None, layout.bounds(), label);
551 }
552 }
553}
554
555impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
556 for Element<'a, Message, Theme, Renderer>
557where
558 Message: 'a,
559 Theme: 'a + Catalog,
560 Renderer: 'a + text::Renderer,
561{
562 fn from(
563 checkbox: Checkbox<'a, Message, Theme, Renderer>,
564 ) -> Element<'a, Message, Theme, Renderer> {
565 Element::new(checkbox)
566 }
567}
568
569#[derive(Debug, Clone, PartialEq)]
571pub struct Icon<Font> {
572 pub font: Font,
574 pub code_point: char,
576 pub size: Option<Pixels>,
578 pub line_height: text::LineHeight,
580 pub shaping: text::Shaping,
582}
583
584#[derive(Debug, Clone, Copy, PartialEq, Eq)]
586pub enum Status {
587 Active {
589 is_checked: bool,
591 },
592 Hovered {
594 is_checked: bool,
596 },
597 Focused {
599 is_checked: bool,
601 },
602 Disabled {
604 is_checked: bool,
606 },
607}
608
609#[derive(Debug, Clone, Copy, PartialEq)]
611pub struct Style {
612 pub background: Background,
614 pub icon_color: Color,
616 pub border: Border,
618 pub shadow: Shadow,
620 pub text_color: Option<Color>,
622}
623
624pub trait Catalog: Sized {
626 type Class<'a>;
628
629 fn default<'a>() -> Self::Class<'a>;
631
632 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
634}
635
636pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
640
641impl Catalog for Theme {
642 type Class<'a> = StyleFn<'a, Self>;
643
644 fn default<'a>() -> Self::Class<'a> {
645 Box::new(primary)
646 }
647
648 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
649 class(self, status)
650 }
651}
652
653pub fn primary(theme: &Theme, status: Status) -> Style {
655 let palette = theme.palette();
656
657 match status {
658 Status::Active { is_checked } => styled(
659 palette.background.strong.color,
660 palette.background.base,
661 palette.primary.base.text,
662 palette.primary.base,
663 is_checked,
664 ),
665 Status::Hovered { is_checked } => styled(
666 palette.background.strong.color,
667 palette.background.weak,
668 palette.primary.base.text,
669 palette.primary.strong,
670 is_checked,
671 ),
672 Status::Focused { is_checked } => {
673 let base = styled(
674 palette.background.strong.color,
675 palette.background.base,
676 palette.primary.base.text,
677 palette.primary.base,
678 is_checked,
679 );
680 let accent = palette.primary.strong.color;
681 let page_bg = palette.background.base.color;
682 let widget_bg = if is_checked {
683 palette.primary.base.color
684 } else {
685 palette.background.base.color
686 };
687
688 Style {
689 border: Border {
690 color: palette::focus_border_color(widget_bg, accent, page_bg),
691 width: 2.0,
692 ..base.border
693 },
694 shadow: palette::focus_shadow(accent, page_bg),
695 ..base
696 }
697 }
698 Status::Disabled { is_checked } => styled(
699 palette.background.weak.color,
700 palette.background.weaker,
701 palette.primary.base.text,
702 palette.background.strong,
703 is_checked,
704 ),
705 }
706}
707
708pub fn secondary(theme: &Theme, status: Status) -> Style {
710 let palette = theme.palette();
711
712 match status {
713 Status::Active { is_checked } => styled(
714 palette.background.strong.color,
715 palette.background.base,
716 palette.background.base.text,
717 palette.background.strong,
718 is_checked,
719 ),
720 Status::Hovered { is_checked } => styled(
721 palette.background.strong.color,
722 palette.background.weak,
723 palette.background.base.text,
724 palette.background.strong,
725 is_checked,
726 ),
727 Status::Focused { is_checked } => {
728 let base = styled(
729 palette.background.strong.color,
730 palette.background.base,
731 palette.background.base.text,
732 palette.background.strong,
733 is_checked,
734 );
735 let accent = palette.primary.strong.color;
736 let page_bg = palette.background.base.color;
737 let widget_bg = if is_checked {
738 palette.background.strong.color
739 } else {
740 palette.background.base.color
741 };
742
743 Style {
744 border: Border {
745 color: palette::focus_border_color(widget_bg, accent, page_bg),
746 width: 2.0,
747 ..base.border
748 },
749 shadow: palette::focus_shadow(accent, page_bg),
750 ..base
751 }
752 }
753 Status::Disabled { is_checked } => styled(
754 palette.background.weak.color,
755 palette.background.weak,
756 palette.background.base.text,
757 palette.background.weak,
758 is_checked,
759 ),
760 }
761}
762
763pub fn success(theme: &Theme, status: Status) -> Style {
765 let palette = theme.palette();
766
767 match status {
768 Status::Active { is_checked } => styled(
769 palette.background.weak.color,
770 palette.background.base,
771 palette.success.base.text,
772 palette.success.base,
773 is_checked,
774 ),
775 Status::Hovered { is_checked } => styled(
776 palette.background.strong.color,
777 palette.background.weak,
778 palette.success.base.text,
779 palette.success.strong,
780 is_checked,
781 ),
782 Status::Focused { is_checked } => {
783 let base = styled(
784 palette.background.weak.color,
785 palette.background.base,
786 palette.success.base.text,
787 palette.success.base,
788 is_checked,
789 );
790 let accent = palette.primary.strong.color;
791 let page_bg = palette.background.base.color;
792 let widget_bg = if is_checked {
793 palette.success.base.color
794 } else {
795 palette.background.base.color
796 };
797
798 Style {
799 border: Border {
800 color: palette::focus_border_color(widget_bg, accent, page_bg),
801 width: 2.0,
802 ..base.border
803 },
804 shadow: palette::focus_shadow(accent, page_bg),
805 ..base
806 }
807 }
808 Status::Disabled { is_checked } => styled(
809 palette.background.weak.color,
810 palette.background.weak,
811 palette.success.base.text,
812 palette.success.weak,
813 is_checked,
814 ),
815 }
816}
817
818pub fn danger(theme: &Theme, status: Status) -> Style {
820 let palette = theme.palette();
821
822 match status {
823 Status::Active { is_checked } => styled(
824 palette.background.strong.color,
825 palette.background.base,
826 palette.danger.base.text,
827 palette.danger.base,
828 is_checked,
829 ),
830 Status::Hovered { is_checked } => styled(
831 palette.background.strong.color,
832 palette.background.weak,
833 palette.danger.base.text,
834 palette.danger.strong,
835 is_checked,
836 ),
837 Status::Focused { is_checked } => {
838 let base = styled(
839 palette.background.strong.color,
840 palette.background.base,
841 palette.danger.base.text,
842 palette.danger.base,
843 is_checked,
844 );
845 let accent = palette.primary.strong.color;
846 let page_bg = palette.background.base.color;
847 let widget_bg = if is_checked {
848 palette.danger.base.color
849 } else {
850 palette.background.base.color
851 };
852
853 Style {
854 border: Border {
855 color: palette::focus_border_color(widget_bg, accent, page_bg),
856 width: 2.0,
857 ..base.border
858 },
859 shadow: palette::focus_shadow(accent, page_bg),
860 ..base
861 }
862 }
863 Status::Disabled { is_checked } => styled(
864 palette.background.weak.color,
865 palette.background.weak,
866 palette.danger.base.text,
867 palette.danger.weak,
868 is_checked,
869 ),
870 }
871}
872
873fn styled(
874 border_color: Color,
875 base: palette::Pair,
876 icon_color: Color,
877 accent: palette::Pair,
878 is_checked: bool,
879) -> Style {
880 let (background, border) = if is_checked {
881 (accent, accent.color)
882 } else {
883 (base, border_color)
884 };
885
886 Style {
887 background: Background::Color(background.color),
888 icon_color,
889 border: Border {
890 radius: 2.0.into(),
891 width: 1.0,
892 color: border,
893 },
894 shadow: Shadow::default(),
895 text_color: None,
896 }
897}
898
899#[cfg(test)]
900mod tests {
901 use super::*;
902 use crate::core::widget::operation::focusable::Focusable;
903
904 type TestState = State<()>;
905
906 #[test]
907 fn focusable_trait() {
908 let mut state = TestState::default();
909 assert!(!state.is_focused());
910 assert!(!state.focus_visible);
911 state.focus();
912 assert!(state.is_focused());
913 assert!(state.focus_visible);
914 state.unfocus();
915 assert!(!state.is_focused());
916 assert!(!state.focus_visible);
917 }
918}