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(state.is_checked)
20//!         .label("Toggle me!")
21//!         .on_toggle(Message::CheckboxToggled)
22//!         .into()
23//! }
24//!
25//! fn update(state: &mut State, message: Message) {
26//!     match message {
27//!         Message::CheckboxToggled(is_checked) => {
28//!             state.is_checked = is_checked;
29//!         }
30//!     }
31//! }
32//! ```
33//! ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true)
34use crate::core::alignment;
35use crate::core::layout;
36use crate::core::mouse;
37use crate::core::renderer;
38use crate::core::text;
39use crate::core::theme::palette;
40use crate::core::touch;
41use crate::core::widget;
42use crate::core::widget::tree::{self, Tree};
43use crate::core::window;
44use crate::core::{
45    Background, Border, Clipboard, Color, Element, Event, Layout, Length, Pixels, Rectangle, Shell,
46    Size, Theme, Widget,
47};
48
49/// A box that can be checked.
50///
51/// # Example
52/// ```no_run
53/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
54/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
55/// #
56/// use iced::widget::checkbox;
57///
58/// struct State {
59///    is_checked: bool,
60/// }
61///
62/// enum Message {
63///     CheckboxToggled(bool),
64/// }
65///
66/// fn view(state: &State) -> Element<'_, Message> {
67///     checkbox(state.is_checked)
68///         .label("Toggle me!")
69///         .on_toggle(Message::CheckboxToggled)
70///         .into()
71/// }
72///
73/// fn update(state: &mut State, message: Message) {
74///     match message {
75///         Message::CheckboxToggled(is_checked) => {
76///             state.is_checked = is_checked;
77///         }
78///     }
79/// }
80/// ```
81/// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true)
82pub struct Checkbox<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
83where
84    Renderer: text::Renderer,
85    Theme: Catalog,
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    text_line_height: text::LineHeight,
95    text_shaping: text::Shaping,
96    text_wrapping: text::Wrapping,
97    font: Option<Renderer::Font>,
98    icon: Icon<Renderer::Font>,
99    class: Theme::Class<'a>,
100    last_status: Option<Status>,
101}
102
103impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer>
104where
105    Renderer: text::Renderer,
106    Theme: Catalog,
107{
108    /// The default size of a [`Checkbox`].
109    const DEFAULT_SIZE: f32 = 16.0;
110
111    /// Creates a new [`Checkbox`].
112    ///
113    /// It expects:
114    ///   * a boolean describing whether the [`Checkbox`] is checked or not
115    pub fn new(is_checked: bool) -> Self {
116        Checkbox {
117            is_checked,
118            on_toggle: None,
119            label: None,
120            width: Length::Shrink,
121            size: Self::DEFAULT_SIZE,
122            spacing: Self::DEFAULT_SIZE / 2.0,
123            text_size: None,
124            text_line_height: text::LineHeight::default(),
125            text_shaping: text::Shaping::default(),
126            text_wrapping: text::Wrapping::default(),
127            font: None,
128            icon: Icon {
129                font: Renderer::ICON_FONT,
130                code_point: Renderer::CHECKMARK_ICON,
131                size: None,
132                line_height: text::LineHeight::default(),
133                shaping: text::Shaping::Basic,
134            },
135            class: Theme::default(),
136            last_status: None,
137        }
138    }
139
140    /// Sets the label of the [`Checkbox`].
141    pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
142        self.label = Some(label.into_fragment());
143        self
144    }
145
146    /// Sets the function that will be called when the [`Checkbox`] is toggled.
147    /// It will receive the new state of the [`Checkbox`] and must produce a
148    /// `Message`.
149    ///
150    /// Unless `on_toggle` is called, the [`Checkbox`] will be disabled.
151    pub fn on_toggle<F>(mut self, f: F) -> Self
152    where
153        F: 'a + Fn(bool) -> Message,
154    {
155        self.on_toggle = Some(Box::new(f));
156        self
157    }
158
159    /// Sets the function that will be called when the [`Checkbox`] is toggled,
160    /// if `Some`.
161    ///
162    /// If `None`, the checkbox will be disabled.
163    pub fn on_toggle_maybe<F>(mut self, f: Option<F>) -> Self
164    where
165        F: Fn(bool) -> Message + 'a,
166    {
167        self.on_toggle = f.map(|f| Box::new(f) as _);
168        self
169    }
170
171    /// Sets the size of the [`Checkbox`].
172    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
173        self.size = size.into().0;
174        self
175    }
176
177    /// Sets the width of the [`Checkbox`].
178    pub fn width(mut self, width: impl Into<Length>) -> Self {
179        self.width = width.into();
180        self
181    }
182
183    /// Sets the spacing between the [`Checkbox`] and the text.
184    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
185        self.spacing = spacing.into().0;
186        self
187    }
188
189    /// Sets the text size of the [`Checkbox`].
190    pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
191        self.text_size = Some(text_size.into());
192        self
193    }
194
195    /// Sets the text [`text::LineHeight`] of the [`Checkbox`].
196    pub fn text_line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
197        self.text_line_height = line_height.into();
198        self
199    }
200
201    /// Sets the [`text::Shaping`] strategy of the [`Checkbox`].
202    pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
203        self.text_shaping = shaping;
204        self
205    }
206
207    /// Sets the [`text::Wrapping`] strategy of the [`Checkbox`].
208    pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
209        self.text_wrapping = wrapping;
210        self
211    }
212
213    /// Sets the [`Renderer::Font`] of the text of the [`Checkbox`].
214    ///
215    /// [`Renderer::Font`]: crate::core::text::Renderer
216    pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
217        self.font = Some(font.into());
218        self
219    }
220
221    /// Sets the [`Icon`] of the [`Checkbox`].
222    pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self {
223        self.icon = icon;
224        self
225    }
226
227    /// Sets the style of the [`Checkbox`].
228    #[must_use]
229    pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
230    where
231        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
232    {
233        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
234        self
235    }
236
237    /// Sets the style class of the [`Checkbox`].
238    #[cfg(feature = "advanced")]
239    #[must_use]
240    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
241        self.class = class.into();
242        self
243    }
244}
245
246impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
247    for Checkbox<'_, Message, Theme, Renderer>
248where
249    Renderer: text::Renderer,
250    Theme: Catalog,
251{
252    fn tag(&self) -> tree::Tag {
253        tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
254    }
255
256    fn state(&self) -> tree::State {
257        tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
258    }
259
260    fn size(&self) -> Size<Length> {
261        Size {
262            width: self.width,
263            height: Length::Shrink,
264        }
265    }
266
267    fn layout(
268        &mut self,
269        tree: &mut Tree,
270        renderer: &Renderer,
271        limits: &layout::Limits,
272    ) -> layout::Node {
273        layout::next_to_each_other(
274            &limits.width(self.width),
275            if self.label.is_some() {
276                self.spacing
277            } else {
278                0.0
279            },
280            |_| layout::Node::new(Size::new(self.size, self.size)),
281            |limits| {
282                if let Some(label) = self.label.as_deref() {
283                    let state = tree
284                        .state
285                        .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
286
287                    widget::text::layout(
288                        state,
289                        renderer,
290                        limits,
291                        label,
292                        widget::text::Format {
293                            width: self.width,
294                            height: Length::Shrink,
295                            line_height: self.text_line_height,
296                            size: self.text_size,
297                            font: self.font,
298                            align_x: text::Alignment::Default,
299                            align_y: alignment::Vertical::Top,
300                            shaping: self.text_shaping,
301                            wrapping: self.text_wrapping,
302                        },
303                    )
304                } else {
305                    layout::Node::new(Size::ZERO)
306                }
307            },
308        )
309    }
310
311    fn update(
312        &mut self,
313        _tree: &mut Tree,
314        event: &Event,
315        layout: Layout<'_>,
316        cursor: mouse::Cursor,
317        _renderer: &Renderer,
318        _clipboard: &mut dyn Clipboard,
319        shell: &mut Shell<'_, Message>,
320        _viewport: &Rectangle,
321    ) {
322        match event {
323            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
324            | Event::Touch(touch::Event::FingerPressed { .. }) => {
325                let mouse_over = cursor.is_over(layout.bounds());
326
327                if mouse_over && let Some(on_toggle) = &self.on_toggle {
328                    shell.publish((on_toggle)(!self.is_checked));
329                    shell.capture_event();
330                }
331            }
332            _ => {}
333        }
334
335        let current_status = {
336            let is_mouse_over = cursor.is_over(layout.bounds());
337            let is_disabled = self.on_toggle.is_none();
338            let is_checked = self.is_checked;
339
340            if is_disabled {
341                Status::Disabled { is_checked }
342            } else if is_mouse_over {
343                Status::Hovered { is_checked }
344            } else {
345                Status::Active { is_checked }
346            }
347        };
348
349        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
350            self.last_status = Some(current_status);
351        } else if self
352            .last_status
353            .is_some_and(|status| status != current_status)
354        {
355            shell.request_redraw();
356        }
357    }
358
359    fn mouse_interaction(
360        &self,
361        _tree: &Tree,
362        layout: Layout<'_>,
363        cursor: mouse::Cursor,
364        _viewport: &Rectangle,
365        _renderer: &Renderer,
366    ) -> mouse::Interaction {
367        if cursor.is_over(layout.bounds()) && self.on_toggle.is_some() {
368            mouse::Interaction::Pointer
369        } else {
370            mouse::Interaction::default()
371        }
372    }
373
374    fn draw(
375        &self,
376        tree: &Tree,
377        renderer: &mut Renderer,
378        theme: &Theme,
379        defaults: &renderer::Style,
380        layout: Layout<'_>,
381        _cursor: mouse::Cursor,
382        viewport: &Rectangle,
383    ) {
384        let mut children = layout.children();
385
386        let style = theme.style(
387            &self.class,
388            self.last_status.unwrap_or(Status::Disabled {
389                is_checked: self.is_checked,
390            }),
391        );
392
393        {
394            let layout = children.next().unwrap();
395            let bounds = layout.bounds();
396
397            renderer.fill_quad(
398                renderer::Quad {
399                    bounds,
400                    border: style.border,
401                    ..renderer::Quad::default()
402                },
403                style.background,
404            );
405
406            let Icon {
407                font,
408                code_point,
409                size,
410                line_height,
411                shaping,
412            } = &self.icon;
413            let size = size.unwrap_or(Pixels(bounds.height * 0.7));
414
415            if self.is_checked {
416                renderer.fill_text(
417                    text::Text {
418                        content: code_point.to_string(),
419                        font: *font,
420                        size,
421                        line_height: *line_height,
422                        bounds: bounds.size(),
423                        align_x: text::Alignment::Center,
424                        align_y: alignment::Vertical::Center,
425                        shaping: *shaping,
426                        wrapping: text::Wrapping::default(),
427                    },
428                    bounds.center(),
429                    style.icon_color,
430                    *viewport,
431                );
432            }
433        }
434
435        if self.label.is_none() {
436            return;
437        }
438
439        {
440            let label_layout = children.next().unwrap();
441            let state: &widget::text::State<Renderer::Paragraph> = tree.state.downcast_ref();
442
443            crate::text::draw(
444                renderer,
445                defaults,
446                label_layout.bounds(),
447                state.raw(),
448                crate::text::Style {
449                    color: style.text_color,
450                },
451                viewport,
452            );
453        }
454    }
455
456    fn operate(
457        &mut self,
458        _tree: &mut Tree,
459        layout: Layout<'_>,
460        _renderer: &Renderer,
461        operation: &mut dyn widget::Operation,
462    ) {
463        if let Some(label) = self.label.as_deref() {
464            operation.text(None, layout.bounds(), label);
465        }
466    }
467}
468
469impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
470    for Element<'a, Message, Theme, Renderer>
471where
472    Message: 'a,
473    Theme: 'a + Catalog,
474    Renderer: 'a + text::Renderer,
475{
476    fn from(
477        checkbox: Checkbox<'a, Message, Theme, Renderer>,
478    ) -> Element<'a, Message, Theme, Renderer> {
479        Element::new(checkbox)
480    }
481}
482
483/// The icon in a [`Checkbox`].
484#[derive(Debug, Clone, PartialEq)]
485pub struct Icon<Font> {
486    /// Font that will be used to display the `code_point`,
487    pub font: Font,
488    /// The unicode code point that will be used as the icon.
489    pub code_point: char,
490    /// Font size of the content.
491    pub size: Option<Pixels>,
492    /// The line height of the icon.
493    pub line_height: text::LineHeight,
494    /// The shaping strategy of the icon.
495    pub shaping: text::Shaping,
496}
497
498/// The possible status of a [`Checkbox`].
499#[derive(Debug, Clone, Copy, PartialEq, Eq)]
500pub enum Status {
501    /// The [`Checkbox`] can be interacted with.
502    Active {
503        /// Indicates if the [`Checkbox`] is currently checked.
504        is_checked: bool,
505    },
506    /// The [`Checkbox`] can be interacted with and it is being hovered.
507    Hovered {
508        /// Indicates if the [`Checkbox`] is currently checked.
509        is_checked: bool,
510    },
511    /// The [`Checkbox`] cannot be interacted with.
512    Disabled {
513        /// Indicates if the [`Checkbox`] is currently checked.
514        is_checked: bool,
515    },
516}
517
518/// The style of a checkbox.
519#[derive(Debug, Clone, Copy, PartialEq)]
520pub struct Style {
521    /// The [`Background`] of the checkbox.
522    pub background: Background,
523    /// The icon [`Color`] of the checkbox.
524    pub icon_color: Color,
525    /// The [`Border`] of the checkbox.
526    pub border: Border,
527    /// The text [`Color`] of the checkbox.
528    pub text_color: Option<Color>,
529}
530
531/// The theme catalog of a [`Checkbox`].
532pub trait Catalog: Sized {
533    /// The item class of the [`Catalog`].
534    type Class<'a>;
535
536    /// The default class produced by the [`Catalog`].
537    fn default<'a>() -> Self::Class<'a>;
538
539    /// The [`Style`] of a class with the given status.
540    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
541}
542
543/// A styling function for a [`Checkbox`].
544///
545/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
546pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
547
548impl Catalog for Theme {
549    type Class<'a> = StyleFn<'a, Self>;
550
551    fn default<'a>() -> Self::Class<'a> {
552        Box::new(primary)
553    }
554
555    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
556        class(self, status)
557    }
558}
559
560/// A primary checkbox; denoting a main toggle.
561pub fn primary(theme: &Theme, status: Status) -> Style {
562    let palette = theme.extended_palette();
563
564    match status {
565        Status::Active { is_checked } => styled(
566            palette.background.strong.color,
567            palette.background.base,
568            palette.primary.base.text,
569            palette.primary.base,
570            is_checked,
571        ),
572        Status::Hovered { is_checked } => styled(
573            palette.background.strong.color,
574            palette.background.weak,
575            palette.primary.base.text,
576            palette.primary.strong,
577            is_checked,
578        ),
579        Status::Disabled { is_checked } => styled(
580            palette.background.weak.color,
581            palette.background.weaker,
582            palette.primary.base.text,
583            palette.background.strong,
584            is_checked,
585        ),
586    }
587}
588
589/// A secondary checkbox; denoting a complementary toggle.
590pub fn secondary(theme: &Theme, status: Status) -> Style {
591    let palette = theme.extended_palette();
592
593    match status {
594        Status::Active { is_checked } => styled(
595            palette.background.strong.color,
596            palette.background.base,
597            palette.background.base.text,
598            palette.background.strong,
599            is_checked,
600        ),
601        Status::Hovered { is_checked } => styled(
602            palette.background.strong.color,
603            palette.background.weak,
604            palette.background.base.text,
605            palette.background.strong,
606            is_checked,
607        ),
608        Status::Disabled { is_checked } => styled(
609            palette.background.weak.color,
610            palette.background.weak,
611            palette.background.base.text,
612            palette.background.weak,
613            is_checked,
614        ),
615    }
616}
617
618/// A success checkbox; denoting a positive toggle.
619pub fn success(theme: &Theme, status: Status) -> Style {
620    let palette = theme.extended_palette();
621
622    match status {
623        Status::Active { is_checked } => styled(
624            palette.background.weak.color,
625            palette.background.base,
626            palette.success.base.text,
627            palette.success.base,
628            is_checked,
629        ),
630        Status::Hovered { is_checked } => styled(
631            palette.background.strong.color,
632            palette.background.weak,
633            palette.success.base.text,
634            palette.success.strong,
635            is_checked,
636        ),
637        Status::Disabled { is_checked } => styled(
638            palette.background.weak.color,
639            palette.background.weak,
640            palette.success.base.text,
641            palette.success.weak,
642            is_checked,
643        ),
644    }
645}
646
647/// A danger checkbox; denoting a negative toggle.
648pub fn danger(theme: &Theme, status: Status) -> Style {
649    let palette = theme.extended_palette();
650
651    match status {
652        Status::Active { is_checked } => styled(
653            palette.background.strong.color,
654            palette.background.base,
655            palette.danger.base.text,
656            palette.danger.base,
657            is_checked,
658        ),
659        Status::Hovered { is_checked } => styled(
660            palette.background.strong.color,
661            palette.background.weak,
662            palette.danger.base.text,
663            palette.danger.strong,
664            is_checked,
665        ),
666        Status::Disabled { is_checked } => styled(
667            palette.background.weak.color,
668            palette.background.weak,
669            palette.danger.base.text,
670            palette.danger.weak,
671            is_checked,
672        ),
673    }
674}
675
676fn styled(
677    border_color: Color,
678    base: palette::Pair,
679    icon_color: Color,
680    accent: palette::Pair,
681    is_checked: bool,
682) -> Style {
683    let (background, border) = if is_checked {
684        (accent, accent.color)
685    } else {
686        (base, border_color)
687    };
688
689    Style {
690        background: Background::Color(background.color),
691        icon_color,
692        border: Border {
693            radius: 2.0.into(),
694            width: 1.0,
695            color: border,
696        },
697        text_color: None,
698    }
699}