1use iced_runtime::core::widget::Id;
35#[cfg(feature = "a11y")]
36use std::borrow::Cow;
37
38use crate::core::alignment;
39use crate::core::event::{self, Event};
40use crate::core::layout;
41use crate::core::mouse;
42use crate::core::renderer;
43use crate::core::text;
44use crate::core::theme::palette;
45use crate::core::touch;
46use crate::core::widget;
47use crate::core::widget::tree::{self, Tree};
48use crate::core::{
49 id::Internal, Background, Border, Clipboard, Color, Element, Layout,
50 Length, Pixels, Rectangle, Shell, Size, Theme, Widget,
51};
52
53#[allow(missing_debug_implementations)]
86pub struct Checkbox<
87 'a,
88 Message,
89 Theme = crate::Theme,
90 Renderer = crate::Renderer,
91> where
92 Renderer: text::Renderer,
93 Theme: Catalog,
94{
95 id: Id,
96 label_id: Id,
97 #[cfg(feature = "a11y")]
98 name: Option<Cow<'a, str>>,
99 #[cfg(feature = "a11y")]
100 description: Option<iced_accessibility::Description<'a>>,
101 is_checked: bool,
102 on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
103 label: String,
104 width: Length,
105 size: f32,
106 spacing: f32,
107 text_size: Option<Pixels>,
108 text_line_height: text::LineHeight,
109 text_shaping: text::Shaping,
110 text_wrapping: text::Wrapping,
111 font: Option<Renderer::Font>,
112 icon: Icon<Renderer::Font>,
113 class: Theme::Class<'a>,
114}
115
116impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer>
117where
118 Renderer: text::Renderer,
119 Theme: Catalog,
120{
121 const DEFAULT_SIZE: f32 = 16.0;
123
124 const DEFAULT_SPACING: f32 = 8.0;
126
127 pub fn new(label: impl Into<String>, is_checked: bool) -> Self {
133 Checkbox {
134 id: Id::unique(),
135 label_id: Id::unique(),
136 #[cfg(feature = "a11y")]
137 name: None,
138 #[cfg(feature = "a11y")]
139 description: None,
140 is_checked,
141 on_toggle: None,
142 label: label.into(),
143 width: Length::Shrink,
144 size: Self::DEFAULT_SIZE,
145 spacing: Self::DEFAULT_SPACING,
146 text_size: None,
147 text_line_height: text::LineHeight::default(),
148 text_shaping: text::Shaping::default(),
149 text_wrapping: text::Wrapping::default(),
150 font: None,
151 icon: Icon {
152 font: Renderer::ICON_FONT,
153 code_point: Renderer::CHECKMARK_ICON,
154 size: None,
155 line_height: text::LineHeight::default(),
156 shaping: text::Shaping::Advanced,
157 wrap: text::Wrapping::default(),
158 },
159 class: Theme::default(),
160 }
161 }
162
163 pub fn on_toggle<F>(mut self, f: F) -> Self
169 where
170 F: 'a + Fn(bool) -> Message,
171 {
172 self.on_toggle = Some(Box::new(f));
173 self
174 }
175
176 pub fn on_toggle_maybe<F>(mut self, f: Option<F>) -> Self
181 where
182 F: Fn(bool) -> Message + 'a,
183 {
184 self.on_toggle = f.map(|f| Box::new(f) as _);
185 self
186 }
187
188 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
190 self.size = size.into().0;
191 self
192 }
193
194 pub fn width(mut self, width: impl Into<Length>) -> Self {
196 self.width = width.into();
197 self
198 }
199
200 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
202 self.spacing = spacing.into().0;
203 self
204 }
205
206 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
208 self.text_size = Some(text_size.into());
209 self
210 }
211
212 pub fn text_line_height(
214 mut self,
215 line_height: impl Into<text::LineHeight>,
216 ) -> Self {
217 self.text_line_height = line_height.into();
218 self
219 }
220
221 pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
223 self.text_shaping = shaping;
224 self
225 }
226
227 pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
229 self.text_wrapping = wrapping;
230 self
231 }
232
233 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
237 self.font = Some(font.into());
238 self
239 }
240
241 pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self {
243 self.icon = icon;
244 self
245 }
246
247 #[must_use]
249 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
250 where
251 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
252 {
253 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
254 self
255 }
256
257 #[cfg(feature = "advanced")]
259 #[must_use]
260 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
261 self.class = class.into();
262 self
263 }
264
265 #[cfg(feature = "a11y")]
266 pub fn name(mut self, name: impl Into<Cow<'a, str>>) -> Self {
268 self.name = Some(name.into());
269 self
270 }
271
272 #[cfg(feature = "a11y")]
273 pub fn description_widget<T: iced_accessibility::Describes>(
275 mut self,
276 description: &T,
277 ) -> Self {
278 self.description = Some(iced_accessibility::Description::Id(
279 description.description(),
280 ));
281 self
282 }
283
284 #[cfg(feature = "a11y")]
285 pub fn description(mut self, description: impl Into<Cow<'a, str>>) -> Self {
287 self.description =
288 Some(iced_accessibility::Description::Text(description.into()));
289 self
290 }
291}
292
293impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
294 for Checkbox<'a, Message, Theme, Renderer>
295where
296 Renderer: text::Renderer,
297 Theme: Catalog,
298{
299 fn tag(&self) -> tree::Tag {
300 tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
301 }
302
303 fn state(&self) -> tree::State {
304 tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
305 }
306
307 fn size(&self) -> Size<Length> {
308 Size {
309 width: self.width,
310 height: Length::Shrink,
311 }
312 }
313
314 fn layout(
315 &self,
316 tree: &mut Tree,
317 renderer: &Renderer,
318 limits: &layout::Limits,
319 ) -> layout::Node {
320 layout::next_to_each_other(
321 &limits.width(self.width),
322 self.spacing,
323 |_| layout::Node::new(crate::core::Size::new(self.size, self.size)),
324 |limits| {
325 let state = tree
326 .state
327 .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
328
329 widget::text::layout(
330 state,
331 renderer,
332 limits,
333 self.width,
334 Length::Shrink,
335 &self.label,
336 self.text_line_height,
337 self.text_size,
338 self.font,
339 alignment::Horizontal::Left,
340 alignment::Vertical::Top,
341 self.text_shaping,
342 self.text_wrapping,
343 )
344 },
345 )
346 }
347
348 fn on_event(
349 &mut self,
350 _tree: &mut Tree,
351 event: Event,
352 layout: Layout<'_>,
353 cursor: mouse::Cursor,
354 _renderer: &Renderer,
355 _clipboard: &mut dyn Clipboard,
356 shell: &mut Shell<'_, Message>,
357 _viewport: &Rectangle,
358 ) -> event::Status {
359 match event {
360 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
361 | Event::Touch(touch::Event::FingerPressed { .. }) => {
362 let mouse_over = cursor.is_over(layout.bounds());
363
364 if mouse_over {
365 if let Some(on_toggle) = &self.on_toggle {
366 shell.publish((on_toggle)(!self.is_checked));
367 return event::Status::Captured;
368 }
369 }
370 }
371 _ => {}
372 }
373
374 event::Status::Ignored
375 }
376
377 fn mouse_interaction(
378 &self,
379 _tree: &Tree,
380 layout: Layout<'_>,
381 cursor: mouse::Cursor,
382 _viewport: &Rectangle,
383 _renderer: &Renderer,
384 ) -> mouse::Interaction {
385 if cursor.is_over(layout.bounds()) && self.on_toggle.is_some() {
386 mouse::Interaction::Pointer
387 } else {
388 mouse::Interaction::default()
389 }
390 }
391
392 fn draw(
393 &self,
394 tree: &Tree,
395 renderer: &mut Renderer,
396 theme: &Theme,
397 defaults: &renderer::Style,
398 layout: Layout<'_>,
399 cursor: mouse::Cursor,
400 viewport: &Rectangle,
401 ) {
402 let is_mouse_over = cursor.is_over(layout.bounds());
403 let is_disabled = self.on_toggle.is_none();
404 let is_checked = self.is_checked;
405
406 let mut children = layout.children();
407
408 let status = if is_disabled {
409 Status::Disabled { is_checked }
410 } else if is_mouse_over {
411 Status::Hovered { is_checked }
412 } else {
413 Status::Active { is_checked }
414 };
415
416 let style = theme.style(&self.class, status);
417
418 {
419 let layout = children.next().unwrap();
420 let bounds = layout.bounds();
421
422 renderer.fill_quad(
423 renderer::Quad {
424 bounds,
425 border: style.border,
426 ..renderer::Quad::default()
427 },
428 style.background,
429 );
430
431 let Icon {
432 font,
433 code_point,
434 size,
435 line_height,
436 shaping,
437 wrap: _,
438 } = &self.icon;
439 let size = size.unwrap_or(Pixels(bounds.height * 0.7));
440
441 if self.is_checked {
442 renderer.fill_text(
443 text::Text {
444 content: code_point.to_string(),
445 font: *font,
446 size,
447 line_height: *line_height,
448 bounds: bounds.size(),
449 horizontal_alignment: alignment::Horizontal::Center,
450 vertical_alignment: alignment::Vertical::Center,
451 shaping: *shaping,
452 wrapping: text::Wrapping::default(),
453 },
454 bounds.center(),
455 style.icon_color,
456 *viewport,
457 );
458 }
459 }
460
461 {
462 let label_layout = children.next().unwrap();
463 let state: &widget::text::State<Renderer::Paragraph> =
464 tree.state.downcast_ref();
465
466 crate::text::draw(
467 renderer,
468 defaults,
469 label_layout,
470 state.0.raw(),
471 crate::text::Style {
472 color: style.text_color,
473 },
474 viewport,
475 );
476 }
477 }
478
479 #[cfg(feature = "a11y")]
480 fn a11y_nodes(
482 &self,
483 layout: Layout<'_>,
484 _state: &Tree,
485 cursor: mouse::Cursor,
486 ) -> iced_accessibility::A11yTree {
487 use iced_accessibility::{
488 accesskit::{Action, NodeBuilder, NodeId, Rect, Role},
489 A11yNode, A11yTree,
490 };
491
492 let bounds = layout.bounds();
493 let is_hovered = cursor.is_over(bounds);
494 let Rectangle {
495 x,
496 y,
497 width,
498 height,
499 } = bounds;
500
501 let bounds = Rect::new(
502 x as f64,
503 y as f64,
504 (x + width) as f64,
505 (y + height) as f64,
506 );
507
508 let mut node = NodeBuilder::new(Role::CheckBox);
509 node.add_action(Action::Focus);
510 node.add_action(Action::Default);
511 node.set_bounds(bounds);
512 if let Some(name) = self.name.as_ref() {
513 node.set_name(name.clone());
514 }
515 match self.description.as_ref() {
516 Some(iced_accessibility::Description::Id(id)) => {
517 node.set_described_by(
518 id.iter()
519 .cloned()
520 .map(|id| NodeId::from(id))
521 .collect::<Vec<_>>(),
522 );
523 }
524 Some(iced_accessibility::Description::Text(text)) => {
525 node.set_description(text.clone());
526 }
527 None => {}
528 }
529 node.set_selected(self.is_checked);
530 if is_hovered {
531 node.set_hovered();
532 }
533 node.add_action(Action::Default);
534 let mut label_node = NodeBuilder::new(Role::Label);
535 label_node.set_name(self.label.clone());
536 label_node.set_bounds(bounds);
538
539 A11yTree::node_with_child_tree(
540 A11yNode::new(node, self.id.clone()),
541 A11yTree::leaf(label_node, self.label_id.clone()),
542 )
543 }
544 fn id(&self) -> Option<Id> {
545 Some(Id(Internal::Set(vec![
546 self.id.0.clone(),
547 self.label_id.0.clone(),
548 ])))
549 }
550
551 fn set_id(&mut self, id: Id) {
552 if let Id(Internal::Set(list)) = id {
553 if list.len() == 2 {
554 self.id.0 = list[0].clone();
555 self.label_id.0 = list[1].clone();
556 }
557 }
558 }
559}
560
561impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
562 for Element<'a, Message, Theme, Renderer>
563where
564 Message: 'a,
565 Theme: 'a + Catalog,
566 Renderer: 'a + text::Renderer,
567{
568 fn from(
569 checkbox: Checkbox<'a, Message, Theme, Renderer>,
570 ) -> Element<'a, Message, Theme, Renderer> {
571 Element::new(checkbox)
572 }
573}
574
575#[derive(Debug, Clone, PartialEq)]
577pub struct Icon<Font> {
578 pub font: Font,
580 pub code_point: char,
582 pub size: Option<Pixels>,
584 pub line_height: text::LineHeight,
586 pub shaping: text::Shaping,
588 pub wrap: text::Wrapping,
590}
591
592#[derive(Debug, Clone, Copy, PartialEq, Eq)]
594pub enum Status {
595 Active {
597 is_checked: bool,
599 },
600 Hovered {
602 is_checked: bool,
604 },
605 Disabled {
607 is_checked: bool,
609 },
610}
611
612#[derive(Debug, Clone, Copy, PartialEq)]
614pub struct Style {
615 pub background: Background,
617 pub icon_color: Color,
619 pub border: Border,
621 pub text_color: Option<Color>,
623}
624
625pub trait Catalog: Sized {
627 type Class<'a>;
629
630 fn default<'a>() -> Self::Class<'a>;
632
633 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
635}
636
637pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
641
642impl Catalog for Theme {
643 type Class<'a> = StyleFn<'a, Self>;
644
645 fn default<'a>() -> Self::Class<'a> {
646 Box::new(primary)
647 }
648
649 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
650 class(self, status)
651 }
652}
653
654pub fn primary(theme: &Theme, status: Status) -> Style {
656 let palette = theme.extended_palette();
657
658 match status {
659 Status::Active { is_checked } => styled(
660 palette.primary.strong.text,
661 palette.background.base,
662 palette.primary.strong,
663 is_checked,
664 ),
665 Status::Hovered { is_checked } => styled(
666 palette.primary.strong.text,
667 palette.background.weak,
668 palette.primary.base,
669 is_checked,
670 ),
671 Status::Disabled { is_checked } => styled(
672 palette.primary.strong.text,
673 palette.background.weak,
674 palette.background.strong,
675 is_checked,
676 ),
677 }
678}
679
680pub fn secondary(theme: &Theme, status: Status) -> Style {
682 let palette = theme.extended_palette();
683
684 match status {
685 Status::Active { is_checked } => styled(
686 palette.background.base.text,
687 palette.background.base,
688 palette.background.strong,
689 is_checked,
690 ),
691 Status::Hovered { is_checked } => styled(
692 palette.background.base.text,
693 palette.background.weak,
694 palette.background.strong,
695 is_checked,
696 ),
697 Status::Disabled { is_checked } => styled(
698 palette.background.strong.color,
699 palette.background.weak,
700 palette.background.weak,
701 is_checked,
702 ),
703 }
704}
705
706pub fn success(theme: &Theme, status: Status) -> Style {
708 let palette = theme.extended_palette();
709
710 match status {
711 Status::Active { is_checked } => styled(
712 palette.success.base.text,
713 palette.background.base,
714 palette.success.base,
715 is_checked,
716 ),
717 Status::Hovered { is_checked } => styled(
718 palette.success.base.text,
719 palette.background.weak,
720 palette.success.base,
721 is_checked,
722 ),
723 Status::Disabled { is_checked } => styled(
724 palette.success.base.text,
725 palette.background.weak,
726 palette.success.weak,
727 is_checked,
728 ),
729 }
730}
731
732pub fn danger(theme: &Theme, status: Status) -> Style {
734 let palette = theme.extended_palette();
735
736 match status {
737 Status::Active { is_checked } => styled(
738 palette.danger.base.text,
739 palette.background.base,
740 palette.danger.base,
741 is_checked,
742 ),
743 Status::Hovered { is_checked } => styled(
744 palette.danger.base.text,
745 palette.background.weak,
746 palette.danger.base,
747 is_checked,
748 ),
749 Status::Disabled { is_checked } => styled(
750 palette.danger.base.text,
751 palette.background.weak,
752 palette.danger.weak,
753 is_checked,
754 ),
755 }
756}
757
758fn styled(
759 icon_color: Color,
760 base: palette::Pair,
761 accent: palette::Pair,
762 is_checked: bool,
763) -> Style {
764 Style {
765 background: Background::Color(if is_checked {
766 accent.color
767 } else {
768 base.color
769 }),
770 icon_color,
771 border: Border {
772 radius: 2.0.into(),
773 width: 1.0,
774 color: accent.color,
775 },
776 text_color: None,
777 }
778}