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