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