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