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