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                        hint_factor: None,
428                    },
429                    bounds.center(),
430                    style.icon_color,
431                    *viewport,
432                );
433            }
434        }
435
436        if self.label.is_none() {
437            return;
438        }
439
440        {
441            let label_layout = children.next().unwrap();
442            let state: &widget::text::State<Renderer::Paragraph> = tree.state.downcast_ref();
443
444            crate::text::draw(
445                renderer,
446                defaults,
447                label_layout.bounds(),
448                state.raw(),
449                crate::text::Style {
450                    color: style.text_color,
451                },
452                viewport,
453            );
454        }
455    }
456
457    fn operate(
458        &mut self,
459        _tree: &mut Tree,
460        layout: Layout<'_>,
461        _renderer: &Renderer,
462        operation: &mut dyn widget::Operation,
463    ) {
464        if let Some(label) = self.label.as_deref() {
465            operation.text(None, layout.bounds(), label);
466        }
467    }
468}
469
470impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
471    for Element<'a, Message, Theme, Renderer>
472where
473    Message: 'a,
474    Theme: 'a + Catalog,
475    Renderer: 'a + text::Renderer,
476{
477    fn from(
478        checkbox: Checkbox<'a, Message, Theme, Renderer>,
479    ) -> Element<'a, Message, Theme, Renderer> {
480        Element::new(checkbox)
481    }
482}
483
484/// The icon in a [`Checkbox`].
485#[derive(Debug, Clone, PartialEq)]
486pub struct Icon<Font> {
487    /// Font that will be used to display the `code_point`,
488    pub font: Font,
489    /// The unicode code point that will be used as the icon.
490    pub code_point: char,
491    /// Font size of the content.
492    pub size: Option<Pixels>,
493    /// The line height of the icon.
494    pub line_height: text::LineHeight,
495    /// The shaping strategy of the icon.
496    pub shaping: text::Shaping,
497}
498
499/// The possible status of a [`Checkbox`].
500#[derive(Debug, Clone, Copy, PartialEq, Eq)]
501pub enum Status {
502    /// The [`Checkbox`] can be interacted with.
503    Active {
504        /// Indicates if the [`Checkbox`] is currently checked.
505        is_checked: bool,
506    },
507    /// The [`Checkbox`] can be interacted with and it is being hovered.
508    Hovered {
509        /// Indicates if the [`Checkbox`] is currently checked.
510        is_checked: bool,
511    },
512    /// The [`Checkbox`] cannot be interacted with.
513    Disabled {
514        /// Indicates if the [`Checkbox`] is currently checked.
515        is_checked: bool,
516    },
517}
518
519/// The style of a checkbox.
520#[derive(Debug, Clone, Copy, PartialEq)]
521pub struct Style {
522    /// The [`Background`] of the checkbox.
523    pub background: Background,
524    /// The icon [`Color`] of the checkbox.
525    pub icon_color: Color,
526    /// The [`Border`] of the checkbox.
527    pub border: Border,
528    /// The text [`Color`] of the checkbox.
529    pub text_color: Option<Color>,
530}
531
532/// The theme catalog of a [`Checkbox`].
533pub trait Catalog: Sized {
534    /// The item class of the [`Catalog`].
535    type Class<'a>;
536
537    /// The default class produced by the [`Catalog`].
538    fn default<'a>() -> Self::Class<'a>;
539
540    /// The [`Style`] of a class with the given status.
541    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
542}
543
544/// A styling function for a [`Checkbox`].
545///
546/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
547pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
548
549impl Catalog for Theme {
550    type Class<'a> = StyleFn<'a, Self>;
551
552    fn default<'a>() -> Self::Class<'a> {
553        Box::new(primary)
554    }
555
556    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
557        class(self, status)
558    }
559}
560
561/// A primary checkbox; denoting a main toggle.
562pub fn primary(theme: &Theme, status: Status) -> Style {
563    let palette = theme.extended_palette();
564
565    match status {
566        Status::Active { is_checked } => styled(
567            palette.background.strong.color,
568            palette.background.base,
569            palette.primary.base.text,
570            palette.primary.base,
571            is_checked,
572        ),
573        Status::Hovered { is_checked } => styled(
574            palette.background.strong.color,
575            palette.background.weak,
576            palette.primary.base.text,
577            palette.primary.strong,
578            is_checked,
579        ),
580        Status::Disabled { is_checked } => styled(
581            palette.background.weak.color,
582            palette.background.weaker,
583            palette.primary.base.text,
584            palette.background.strong,
585            is_checked,
586        ),
587    }
588}
589
590/// A secondary checkbox; denoting a complementary toggle.
591pub fn secondary(theme: &Theme, status: Status) -> Style {
592    let palette = theme.extended_palette();
593
594    match status {
595        Status::Active { is_checked } => styled(
596            palette.background.strong.color,
597            palette.background.base,
598            palette.background.base.text,
599            palette.background.strong,
600            is_checked,
601        ),
602        Status::Hovered { is_checked } => styled(
603            palette.background.strong.color,
604            palette.background.weak,
605            palette.background.base.text,
606            palette.background.strong,
607            is_checked,
608        ),
609        Status::Disabled { is_checked } => styled(
610            palette.background.weak.color,
611            palette.background.weak,
612            palette.background.base.text,
613            palette.background.weak,
614            is_checked,
615        ),
616    }
617}
618
619/// A success checkbox; denoting a positive toggle.
620pub fn success(theme: &Theme, status: Status) -> Style {
621    let palette = theme.extended_palette();
622
623    match status {
624        Status::Active { is_checked } => styled(
625            palette.background.weak.color,
626            palette.background.base,
627            palette.success.base.text,
628            palette.success.base,
629            is_checked,
630        ),
631        Status::Hovered { is_checked } => styled(
632            palette.background.strong.color,
633            palette.background.weak,
634            palette.success.base.text,
635            palette.success.strong,
636            is_checked,
637        ),
638        Status::Disabled { is_checked } => styled(
639            palette.background.weak.color,
640            palette.background.weak,
641            palette.success.base.text,
642            palette.success.weak,
643            is_checked,
644        ),
645    }
646}
647
648/// A danger checkbox; denoting a negative toggle.
649pub fn danger(theme: &Theme, status: Status) -> Style {
650    let palette = theme.extended_palette();
651
652    match status {
653        Status::Active { is_checked } => styled(
654            palette.background.strong.color,
655            palette.background.base,
656            palette.danger.base.text,
657            palette.danger.base,
658            is_checked,
659        ),
660        Status::Hovered { is_checked } => styled(
661            palette.background.strong.color,
662            palette.background.weak,
663            palette.danger.base.text,
664            palette.danger.strong,
665            is_checked,
666        ),
667        Status::Disabled { is_checked } => styled(
668            palette.background.weak.color,
669            palette.background.weak,
670            palette.danger.base.text,
671            palette.danger.weak,
672            is_checked,
673        ),
674    }
675}
676
677fn styled(
678    border_color: Color,
679    base: palette::Pair,
680    icon_color: Color,
681    accent: palette::Pair,
682    is_checked: bool,
683) -> Style {
684    let (background, border) = if is_checked {
685        (accent, accent.color)
686    } else {
687        (base, border_color)
688    };
689
690    Style {
691        background: Background::Color(background.color),
692        icon_color,
693        border: Border {
694            radius: 2.0.into(),
695            width: 1.0,
696            color: border,
697        },
698        text_color: None,
699    }
700}