Skip to main content

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