iced_widget/
checkbox.rs

1//! Checkboxes can be used to let users make binary choices.
2//!
3//! # Example
4//! ```no_run
5//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
6//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
7//! #
8//! use iced::widget::checkbox;
9//!
10//! struct State {
11//!    is_checked: bool,
12//! }
13//!
14//! enum Message {
15//!     CheckboxToggled(bool),
16//! }
17//!
18//! fn view(state: &State) -> Element<'_, Message> {
19//!     checkbox("Toggle me!", state.is_checked)
20//!         .on_toggle(Message::CheckboxToggled)
21//!         .into()
22//! }
23//!
24//! fn update(state: &mut State, message: Message) {
25//!     match message {
26//!         Message::CheckboxToggled(is_checked) => {
27//!             state.is_checked = is_checked;
28//!         }
29//!     }
30//! }
31//! ```
32//! ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true)
33//! Show toggle controls using checkboxes.
34use 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/// A box that can be checked.
54///
55/// # Example
56/// ```no_run
57/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
58/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
59/// #
60/// use iced::widget::checkbox;
61///
62/// struct State {
63///    is_checked: bool,
64/// }
65///
66/// enum Message {
67///     CheckboxToggled(bool),
68/// }
69///
70/// fn view(state: &State) -> Element<'_, Message> {
71///     checkbox("Toggle me!", state.is_checked)
72///         .on_toggle(Message::CheckboxToggled)
73///         .into()
74/// }
75///
76/// fn update(state: &mut State, message: Message) {
77///     match message {
78///         Message::CheckboxToggled(is_checked) => {
79///             state.is_checked = is_checked;
80///         }
81///     }
82/// }
83/// ```
84/// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true)
85#[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    /// The default size of a [`Checkbox`].
122    const DEFAULT_SIZE: f32 = 16.0;
123
124    /// The default spacing of a [`Checkbox`].
125    const DEFAULT_SPACING: f32 = 8.0;
126
127    /// Creates a new [`Checkbox`].
128    ///
129    /// It expects:
130    ///   * the label of the [`Checkbox`]
131    ///   * a boolean describing whether the [`Checkbox`] is checked or not
132    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    /// Sets the function that will be called when the [`Checkbox`] is toggled.
164    /// It will receive the new state of the [`Checkbox`] and must produce a
165    /// `Message`.
166    ///
167    /// Unless `on_toggle` is called, the [`Checkbox`] will be disabled.
168    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    /// Sets the function that will be called when the [`Checkbox`] is toggled,
177    /// if `Some`.
178    ///
179    /// If `None`, the checkbox will be disabled.
180    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    /// Sets the size of the [`Checkbox`].
189    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
190        self.size = size.into().0;
191        self
192    }
193
194    /// Sets the width of the [`Checkbox`].
195    pub fn width(mut self, width: impl Into<Length>) -> Self {
196        self.width = width.into();
197        self
198    }
199
200    /// Sets the spacing between the [`Checkbox`] and the text.
201    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
202        self.spacing = spacing.into().0;
203        self
204    }
205
206    /// Sets the text size of the [`Checkbox`].
207    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    /// Sets the text [`text::LineHeight`] of the [`Checkbox`].
213    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    /// Sets the [`text::Shaping`] strategy of the [`Checkbox`].
222    pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
223        self.text_shaping = shaping;
224        self
225    }
226
227    /// Sets the [`text::Wrapping`] strategy of the [`Checkbox`].
228    pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
229        self.text_wrapping = wrapping;
230        self
231    }
232
233    /// Sets the [`Renderer::Font`] of the text of the [`Checkbox`].
234    ///
235    /// [`Renderer::Font`]: crate::core::text::Renderer
236    pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
237        self.font = Some(font.into());
238        self
239    }
240
241    /// Sets the [`Icon`] of the [`Checkbox`].
242    pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self {
243        self.icon = icon;
244        self
245    }
246
247    /// Sets the style of the [`Checkbox`].
248    #[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    /// Sets the style class of the [`Checkbox`].
258    #[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    /// Sets the name of the [`Checkbox`].
267    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    /// Sets the description of the [`Checkbox`].
274    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    /// Sets the description of the [`Checkbox`].
286    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    /// get the a11y nodes for the widget
481    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        // TODO proper label bounds
537        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/// The icon in a [`Checkbox`].
576#[derive(Debug, Clone, PartialEq)]
577pub struct Icon<Font> {
578    /// Font that will be used to display the `code_point`,
579    pub font: Font,
580    /// The unicode code point that will be used as the icon.
581    pub code_point: char,
582    /// Font size of the content.
583    pub size: Option<Pixels>,
584    /// The line height of the icon.
585    pub line_height: text::LineHeight,
586    /// The shaping strategy of the icon.
587    pub shaping: text::Shaping,
588    /// The wrap mode of the icon.
589    pub wrap: text::Wrapping,
590}
591
592/// The possible status of a [`Checkbox`].
593#[derive(Debug, Clone, Copy, PartialEq, Eq)]
594pub enum Status {
595    /// The [`Checkbox`] can be interacted with.
596    Active {
597        /// Indicates if the [`Checkbox`] is currently checked.
598        is_checked: bool,
599    },
600    /// The [`Checkbox`] can be interacted with and it is being hovered.
601    Hovered {
602        /// Indicates if the [`Checkbox`] is currently checked.
603        is_checked: bool,
604    },
605    /// The [`Checkbox`] cannot be interacted with.
606    Disabled {
607        /// Indicates if the [`Checkbox`] is currently checked.
608        is_checked: bool,
609    },
610}
611
612/// The style of a checkbox.
613#[derive(Debug, Clone, Copy, PartialEq)]
614pub struct Style {
615    /// The [`Background`] of the checkbox.
616    pub background: Background,
617    /// The icon [`Color`] of the checkbox.
618    pub icon_color: Color,
619    /// The [`Border`] of the checkbox.
620    pub border: Border,
621    /// The text [`Color`] of the checkbox.
622    pub text_color: Option<Color>,
623}
624
625/// The theme catalog of a [`Checkbox`].
626pub trait Catalog: Sized {
627    /// The item class of the [`Catalog`].
628    type Class<'a>;
629
630    /// The default class produced by the [`Catalog`].
631    fn default<'a>() -> Self::Class<'a>;
632
633    /// The [`Style`] of a class with the given status.
634    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
635}
636
637/// A styling function for a [`Checkbox`].
638///
639/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
640pub 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
654/// A primary checkbox; denoting a main toggle.
655pub 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
680/// A secondary checkbox; denoting a complementary toggle.
681pub 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
706/// A success checkbox; denoting a positive toggle.
707pub 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
732/// A danger checkbox; denoting a negative toggle.
733pub 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}