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.label,
291                    widget::text::Format {
292                        width: self.width,
293                        height: Length::Shrink,
294                        line_height: self.text_line_height,
295                        size: self.text_size,
296                        font: self.font,
297                        align_x: text::Alignment::Default,
298                        align_y: alignment::Vertical::Top,
299                        shaping: self.text_shaping,
300                        wrapping: self.text_wrapping,
301                    },
302                )
303            },
304        )
305    }
306
307    fn update(
308        &mut self,
309        _tree: &mut Tree,
310        event: &Event,
311        layout: Layout<'_>,
312        cursor: mouse::Cursor,
313        _renderer: &Renderer,
314        _clipboard: &mut dyn Clipboard,
315        shell: &mut Shell<'_, Message>,
316        _viewport: &Rectangle,
317    ) {
318        match event {
319            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
320            | Event::Touch(touch::Event::FingerPressed { .. }) => {
321                let mouse_over = cursor.is_over(layout.bounds());
322
323                if mouse_over {
324                    if let Some(on_toggle) = &self.on_toggle {
325                        shell.publish((on_toggle)(!self.is_checked));
326                        shell.capture_event();
327                    }
328                }
329            }
330            _ => {}
331        }
332
333        let current_status = {
334            let is_mouse_over = cursor.is_over(layout.bounds());
335            let is_disabled = self.on_toggle.is_none();
336            let is_checked = self.is_checked;
337
338            if is_disabled {
339                Status::Disabled { is_checked }
340            } else if is_mouse_over {
341                Status::Hovered { is_checked }
342            } else {
343                Status::Active { is_checked }
344            }
345        };
346
347        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
348            self.last_status = Some(current_status);
349        } else if self
350            .last_status
351            .is_some_and(|status| status != current_status)
352        {
353            shell.request_redraw();
354        }
355    }
356
357    fn mouse_interaction(
358        &self,
359        _tree: &Tree,
360        layout: Layout<'_>,
361        cursor: mouse::Cursor,
362        _viewport: &Rectangle,
363        _renderer: &Renderer,
364    ) -> mouse::Interaction {
365        if cursor.is_over(layout.bounds()) && self.on_toggle.is_some() {
366            mouse::Interaction::Pointer
367        } else {
368            mouse::Interaction::default()
369        }
370    }
371
372    fn draw(
373        &self,
374        tree: &Tree,
375        renderer: &mut Renderer,
376        theme: &Theme,
377        defaults: &renderer::Style,
378        layout: Layout<'_>,
379        _cursor: mouse::Cursor,
380        viewport: &Rectangle,
381    ) {
382        let mut children = layout.children();
383
384        let style = theme.style(
385            &self.class,
386            self.last_status.unwrap_or(Status::Disabled {
387                is_checked: self.is_checked,
388            }),
389        );
390
391        {
392            let layout = children.next().unwrap();
393            let bounds = layout.bounds();
394
395            renderer.fill_quad(
396                renderer::Quad {
397                    bounds,
398                    border: style.border,
399                    ..renderer::Quad::default()
400                },
401                style.background,
402            );
403
404            let Icon {
405                font,
406                code_point,
407                size,
408                line_height,
409                shaping,
410            } = &self.icon;
411            let size = size.unwrap_or(Pixels(bounds.height * 0.7));
412
413            if self.is_checked {
414                renderer.fill_text(
415                    text::Text {
416                        content: code_point.to_string(),
417                        font: *font,
418                        size,
419                        line_height: *line_height,
420                        bounds: bounds.size(),
421                        align_x: text::Alignment::Center,
422                        align_y: alignment::Vertical::Center,
423                        shaping: *shaping,
424                        wrapping: text::Wrapping::default(),
425                    },
426                    bounds.center(),
427                    style.icon_color,
428                    *viewport,
429                );
430            }
431        }
432
433        {
434            let label_layout = children.next().unwrap();
435            let state: &widget::text::State<Renderer::Paragraph> =
436                tree.state.downcast_ref();
437
438            crate::text::draw(
439                renderer,
440                defaults,
441                label_layout.bounds(),
442                state.raw(),
443                crate::text::Style {
444                    color: style.text_color,
445                },
446                viewport,
447            );
448        }
449    }
450
451    fn operate(
452        &self,
453        _state: &mut Tree,
454        layout: Layout<'_>,
455        _renderer: &Renderer,
456        operation: &mut dyn widget::Operation,
457    ) {
458        operation.text(None, layout.bounds(), &self.label);
459    }
460}
461
462impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
463    for Element<'a, Message, Theme, Renderer>
464where
465    Message: 'a,
466    Theme: 'a + Catalog,
467    Renderer: 'a + text::Renderer,
468{
469    fn from(
470        checkbox: Checkbox<'a, Message, Theme, Renderer>,
471    ) -> Element<'a, Message, Theme, Renderer> {
472        Element::new(checkbox)
473    }
474}
475
476/// The icon in a [`Checkbox`].
477#[derive(Debug, Clone, PartialEq)]
478pub struct Icon<Font> {
479    /// Font that will be used to display the `code_point`,
480    pub font: Font,
481    /// The unicode code point that will be used as the icon.
482    pub code_point: char,
483    /// Font size of the content.
484    pub size: Option<Pixels>,
485    /// The line height of the icon.
486    pub line_height: text::LineHeight,
487    /// The shaping strategy of the icon.
488    pub shaping: text::Shaping,
489}
490
491/// The possible status of a [`Checkbox`].
492#[derive(Debug, Clone, Copy, PartialEq, Eq)]
493pub enum Status {
494    /// The [`Checkbox`] can be interacted with.
495    Active {
496        /// Indicates if the [`Checkbox`] is currently checked.
497        is_checked: bool,
498    },
499    /// The [`Checkbox`] can be interacted with and it is being hovered.
500    Hovered {
501        /// Indicates if the [`Checkbox`] is currently checked.
502        is_checked: bool,
503    },
504    /// The [`Checkbox`] cannot be interacted with.
505    Disabled {
506        /// Indicates if the [`Checkbox`] is currently checked.
507        is_checked: bool,
508    },
509}
510
511/// The style of a checkbox.
512#[derive(Debug, Clone, Copy, PartialEq)]
513pub struct Style {
514    /// The [`Background`] of the checkbox.
515    pub background: Background,
516    /// The icon [`Color`] of the checkbox.
517    pub icon_color: Color,
518    /// The [`Border`] of the checkbox.
519    pub border: Border,
520    /// The text [`Color`] of the checkbox.
521    pub text_color: Option<Color>,
522}
523
524/// The theme catalog of a [`Checkbox`].
525pub trait Catalog: Sized {
526    /// The item class of the [`Catalog`].
527    type Class<'a>;
528
529    /// The default class produced by the [`Catalog`].
530    fn default<'a>() -> Self::Class<'a>;
531
532    /// The [`Style`] of a class with the given status.
533    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
534}
535
536/// A styling function for a [`Checkbox`].
537///
538/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
539pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
540
541impl Catalog for Theme {
542    type Class<'a> = StyleFn<'a, Self>;
543
544    fn default<'a>() -> Self::Class<'a> {
545        Box::new(primary)
546    }
547
548    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
549        class(self, status)
550    }
551}
552
553/// A primary checkbox; denoting a main toggle.
554pub fn primary(theme: &Theme, status: Status) -> Style {
555    let palette = theme.extended_palette();
556
557    match status {
558        Status::Active { is_checked } => styled(
559            palette.primary.strong.text,
560            palette.background.strongest.color,
561            palette.background.base,
562            palette.primary.base,
563            is_checked,
564        ),
565        Status::Hovered { is_checked } => styled(
566            palette.primary.strong.text,
567            palette.background.strongest.color,
568            palette.background.weak,
569            palette.primary.strong,
570            is_checked,
571        ),
572        Status::Disabled { is_checked } => styled(
573            palette.primary.strong.text,
574            palette.background.weak.color,
575            palette.background.weak,
576            palette.background.strong,
577            is_checked,
578        ),
579    }
580}
581
582/// A secondary checkbox; denoting a complementary toggle.
583pub fn secondary(theme: &Theme, status: Status) -> Style {
584    let palette = theme.extended_palette();
585
586    match status {
587        Status::Active { is_checked } => styled(
588            palette.background.base.text,
589            palette.background.strongest.color,
590            palette.background.base,
591            palette.background.strong,
592            is_checked,
593        ),
594        Status::Hovered { is_checked } => styled(
595            palette.background.base.text,
596            palette.background.strongest.color,
597            palette.background.weak,
598            palette.background.strong,
599            is_checked,
600        ),
601        Status::Disabled { is_checked } => styled(
602            palette.background.strong.color,
603            palette.background.weak.color,
604            palette.background.weak,
605            palette.background.weak,
606            is_checked,
607        ),
608    }
609}
610
611/// A success checkbox; denoting a positive toggle.
612pub fn success(theme: &Theme, status: Status) -> Style {
613    let palette = theme.extended_palette();
614
615    match status {
616        Status::Active { is_checked } => styled(
617            palette.success.base.text,
618            palette.background.weak.color,
619            palette.background.base,
620            palette.success.base,
621            is_checked,
622        ),
623        Status::Hovered { is_checked } => styled(
624            palette.success.base.text,
625            palette.background.strongest.color,
626            palette.background.weak,
627            palette.success.strong,
628            is_checked,
629        ),
630        Status::Disabled { is_checked } => styled(
631            palette.success.base.text,
632            palette.background.weak.color,
633            palette.background.weak,
634            palette.success.weak,
635            is_checked,
636        ),
637    }
638}
639
640/// A danger checkbox; denoting a negative toggle.
641pub fn danger(theme: &Theme, status: Status) -> Style {
642    let palette = theme.extended_palette();
643
644    match status {
645        Status::Active { is_checked } => styled(
646            palette.danger.base.text,
647            palette.background.strongest.color,
648            palette.background.base,
649            palette.danger.base,
650            is_checked,
651        ),
652        Status::Hovered { is_checked } => styled(
653            palette.danger.base.text,
654            palette.background.strongest.color,
655            palette.background.weak,
656            palette.danger.strong,
657            is_checked,
658        ),
659        Status::Disabled { is_checked } => styled(
660            palette.danger.base.text,
661            palette.background.weak.color,
662            palette.background.weak,
663            palette.danger.weak,
664            is_checked,
665        ),
666    }
667}
668
669fn styled(
670    icon_color: Color,
671    border_color: Color,
672    base: palette::Pair,
673    accent: palette::Pair,
674    is_checked: bool,
675) -> Style {
676    Style {
677        background: Background::Color(if is_checked {
678            accent.color
679        } else {
680            base.color
681        }),
682        icon_color,
683        border: Border {
684            radius: 2.0.into(),
685            width: 1.0,
686            color: if is_checked {
687                accent.color
688            } else {
689                border_color
690            },
691        },
692        text_color: None,
693    }
694}