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 crate::core::alignment;
35use crate::core::keyboard;
36use crate::core::keyboard::key;
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::operation::accessible::{Accessible, Role};
45use crate::core::widget::operation::focusable::{self, Focusable};
46use crate::core::widget::tree::{self, Tree};
47use crate::core::window;
48use crate::core::{
49    Background, Border, Color, Element, Event, Layout, Length, Pixels, Rectangle, Shadow, Shell,
50    Size, Theme, Widget,
51};
52
53/// A box that can be checked.
54///
55/// # Example
56/// ```no_run
57/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
58/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
59/// #
60/// use iced::widget::checkbox;
61///
62/// struct State {
63///    is_checked: bool,
64/// }
65///
66/// enum Message {
67///     CheckboxToggled(bool),
68/// }
69///
70/// fn view(state: &State) -> Element<'_, Message> {
71///     checkbox(state.is_checked)
72///         .label("Toggle me!")
73///         .on_toggle(Message::CheckboxToggled)
74///         .into()
75/// }
76///
77/// fn update(state: &mut State, message: Message) {
78///     match message {
79///         Message::CheckboxToggled(is_checked) => {
80///             state.is_checked = is_checked;
81///         }
82///     }
83/// }
84/// ```
85/// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true)
86pub struct Checkbox<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
87where
88    Renderer: text::Renderer,
89    Theme: Catalog,
90{
91    is_checked: bool,
92    on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
93    label: Option<text::Fragment<'a>>,
94    width: Length,
95    size: f32,
96    spacing: f32,
97    text_size: Option<Pixels>,
98    line_height: text::LineHeight,
99    shaping: text::Shaping,
100    wrapping: text::Wrapping,
101    font: Option<Renderer::Font>,
102    icon: Icon<Renderer::Font>,
103    class: Theme::Class<'a>,
104    last_status: Option<Status>,
105}
106
107impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer>
108where
109    Renderer: text::Renderer,
110    Theme: Catalog,
111{
112    /// The default size of a [`Checkbox`].
113    const DEFAULT_SIZE: f32 = 16.0;
114
115    /// Creates a new [`Checkbox`].
116    ///
117    /// It expects:
118    ///   * a boolean describing whether the [`Checkbox`] is checked or not
119    pub fn new(is_checked: bool) -> Self {
120        Checkbox {
121            is_checked,
122            on_toggle: None,
123            label: None,
124            width: Length::Shrink,
125            size: Self::DEFAULT_SIZE,
126            spacing: Self::DEFAULT_SIZE / 2.0,
127            text_size: None,
128            line_height: text::LineHeight::default(),
129            shaping: text::Shaping::default(),
130            wrapping: text::Wrapping::default(),
131            font: None,
132            icon: Icon {
133                font: Renderer::ICON_FONT,
134                code_point: Renderer::CHECKMARK_ICON,
135                size: None,
136                line_height: text::LineHeight::default(),
137                shaping: text::Shaping::Basic,
138            },
139            class: Theme::default(),
140            last_status: None,
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 = 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 [`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
250#[derive(Debug, Clone, Default)]
251struct State<P: text::Paragraph> {
252    is_focused: bool,
253    focus_visible: bool,
254    label: widget::text::State<P>,
255}
256
257impl<P: text::Paragraph> focusable::Focusable for State<P> {
258    fn is_focused(&self) -> bool {
259        self.is_focused
260    }
261
262    fn focus(&mut self) {
263        self.is_focused = true;
264        self.focus_visible = true;
265    }
266
267    fn unfocus(&mut self) {
268        self.is_focused = false;
269        self.focus_visible = false;
270    }
271}
272
273impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
274    for Checkbox<'_, Message, Theme, Renderer>
275where
276    Renderer: text::Renderer,
277    Theme: Catalog,
278{
279    fn tag(&self) -> tree::Tag {
280        tree::Tag::of::<State<Renderer::Paragraph>>()
281    }
282
283    fn state(&self) -> tree::State {
284        tree::State::new(State::<Renderer::Paragraph>::default())
285    }
286
287    fn size(&self) -> Size<Length> {
288        Size {
289            width: self.width,
290            height: Length::Shrink,
291        }
292    }
293
294    fn layout(
295        &mut self,
296        tree: &mut Tree,
297        renderer: &Renderer,
298        limits: &layout::Limits,
299    ) -> layout::Node {
300        layout::next_to_each_other(
301            &limits.width(self.width),
302            if self.label.is_some() {
303                self.spacing
304            } else {
305                0.0
306            },
307            |_| layout::Node::new(Size::new(self.size, self.size)),
308            |limits| {
309                if let Some(label) = self.label.as_deref() {
310                    let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
311
312                    widget::text::layout(
313                        &mut state.label,
314                        renderer,
315                        limits,
316                        label,
317                        widget::text::Format {
318                            width: self.width,
319                            height: Length::Shrink,
320                            line_height: self.line_height,
321                            size: self.text_size,
322                            font: self.font,
323                            align_x: text::Alignment::Default,
324                            align_y: alignment::Vertical::Top,
325                            shaping: self.shaping,
326                            wrapping: self.wrapping,
327                            ellipsis: text::Ellipsis::None,
328                        },
329                    )
330                } else {
331                    layout::Node::new(Size::ZERO)
332                }
333            },
334        )
335    }
336
337    fn update(
338        &mut self,
339        tree: &mut Tree,
340        event: &Event,
341        layout: Layout<'_>,
342        cursor: mouse::Cursor,
343        _renderer: &Renderer,
344        shell: &mut Shell<'_, Message>,
345        _viewport: &Rectangle,
346    ) {
347        match event {
348            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
349            | Event::Touch(touch::Event::FingerPressed { .. }) => {
350                let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
351
352                if cursor.is_over(layout.bounds())
353                    && let Some(on_toggle) = &self.on_toggle
354                {
355                    state.is_focused = true;
356                    state.focus_visible = false;
357
358                    shell.publish((on_toggle)(!self.is_checked));
359                    shell.capture_event();
360                } else {
361                    state.is_focused = false;
362                    state.focus_visible = false;
363                }
364            }
365            Event::Keyboard(keyboard::Event::KeyPressed {
366                key: keyboard::Key::Named(key::Named::Space),
367                ..
368            }) => {
369                if let Some(on_toggle) = &self.on_toggle {
370                    let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
371
372                    if state.is_focused {
373                        shell.publish((on_toggle)(!self.is_checked));
374                        shell.capture_event();
375                    }
376                }
377            }
378            Event::Keyboard(keyboard::Event::KeyPressed {
379                key: keyboard::Key::Named(key::Named::Escape),
380                ..
381            }) => {
382                let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
383                if state.is_focused {
384                    state.is_focused = false;
385                    state.focus_visible = false;
386                    shell.capture_event();
387                }
388            }
389            _ => {}
390        }
391
392        let current_status = {
393            let is_mouse_over = cursor.is_over(layout.bounds());
394            let is_disabled = self.on_toggle.is_none();
395            let is_checked = self.is_checked;
396
397            if is_disabled {
398                Status::Disabled { is_checked }
399            } else {
400                let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
401
402                if state.focus_visible {
403                    Status::Focused { is_checked }
404                } else if is_mouse_over {
405                    Status::Hovered { is_checked }
406                } else {
407                    Status::Active { is_checked }
408                }
409            }
410        };
411
412        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
413            self.last_status = Some(current_status);
414        } else if self
415            .last_status
416            .is_some_and(|status| status != current_status)
417        {
418            shell.request_redraw();
419        }
420    }
421
422    fn mouse_interaction(
423        &self,
424        _tree: &Tree,
425        layout: Layout<'_>,
426        cursor: mouse::Cursor,
427        _viewport: &Rectangle,
428        _renderer: &Renderer,
429    ) -> mouse::Interaction {
430        if cursor.is_over(layout.bounds()) && self.on_toggle.is_some() {
431            mouse::Interaction::Pointer
432        } else {
433            mouse::Interaction::default()
434        }
435    }
436
437    fn draw(
438        &self,
439        tree: &Tree,
440        renderer: &mut Renderer,
441        theme: &Theme,
442        defaults: &renderer::Style,
443        layout: Layout<'_>,
444        _cursor: mouse::Cursor,
445        viewport: &Rectangle,
446    ) {
447        let mut children = layout.children();
448
449        let style = theme.style(
450            &self.class,
451            self.last_status.unwrap_or(Status::Disabled {
452                is_checked: self.is_checked,
453            }),
454        );
455
456        {
457            let layout = children.next().unwrap();
458            let bounds = layout.bounds();
459
460            renderer.fill_quad(
461                renderer::Quad {
462                    bounds,
463                    border: style.border,
464                    shadow: style.shadow,
465                    ..renderer::Quad::default()
466                },
467                style.background,
468            );
469
470            let Icon {
471                font,
472                code_point,
473                size,
474                line_height,
475                shaping,
476            } = &self.icon;
477            let size = size.unwrap_or(Pixels(bounds.height * 0.7));
478
479            if self.is_checked {
480                renderer.fill_text(
481                    text::Text {
482                        content: code_point.to_string(),
483                        font: *font,
484                        size,
485                        line_height: *line_height,
486                        bounds: bounds.size(),
487                        align_x: text::Alignment::Center,
488                        align_y: alignment::Vertical::Center,
489                        shaping: *shaping,
490                        wrapping: text::Wrapping::default(),
491                        ellipsis: text::Ellipsis::default(),
492                        hint_factor: None,
493                    },
494                    bounds.center(),
495                    style.icon_color,
496                    *viewport,
497                );
498            }
499        }
500
501        if self.label.is_none() {
502            return;
503        }
504
505        {
506            let label_layout = children.next().unwrap();
507            let state: &State<Renderer::Paragraph> = tree.state.downcast_ref();
508
509            crate::text::draw(
510                renderer,
511                defaults,
512                label_layout.bounds(),
513                state.label.raw(),
514                crate::text::Style {
515                    color: style.text_color,
516                },
517                viewport,
518            );
519        }
520    }
521
522    fn operate(
523        &mut self,
524        tree: &mut Tree,
525        layout: Layout<'_>,
526        _renderer: &Renderer,
527        operation: &mut dyn widget::Operation,
528    ) {
529        let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
530
531        operation.accessible(
532            None,
533            layout.bounds(),
534            &Accessible {
535                role: Role::CheckBox,
536                label: self.label.as_deref(),
537                toggled: Some(self.is_checked),
538                disabled: self.on_toggle.is_none(),
539                ..Accessible::default()
540            },
541        );
542
543        if self.on_toggle.is_some() {
544            operation.focusable(None, layout.bounds(), state);
545        } else {
546            state.unfocus();
547        }
548
549        if let Some(label) = self.label.as_deref() {
550            operation.text(None, layout.bounds(), label);
551        }
552    }
553}
554
555impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
556    for Element<'a, Message, Theme, Renderer>
557where
558    Message: 'a,
559    Theme: 'a + Catalog,
560    Renderer: 'a + text::Renderer,
561{
562    fn from(
563        checkbox: Checkbox<'a, Message, Theme, Renderer>,
564    ) -> Element<'a, Message, Theme, Renderer> {
565        Element::new(checkbox)
566    }
567}
568
569/// The icon in a [`Checkbox`].
570#[derive(Debug, Clone, PartialEq)]
571pub struct Icon<Font> {
572    /// Font that will be used to display the `code_point`,
573    pub font: Font,
574    /// The unicode code point that will be used as the icon.
575    pub code_point: char,
576    /// Font size of the content.
577    pub size: Option<Pixels>,
578    /// The line height of the icon.
579    pub line_height: text::LineHeight,
580    /// The shaping strategy of the icon.
581    pub shaping: text::Shaping,
582}
583
584/// The possible status of a [`Checkbox`].
585#[derive(Debug, Clone, Copy, PartialEq, Eq)]
586pub enum Status {
587    /// The [`Checkbox`] can be interacted with.
588    Active {
589        /// Indicates if the [`Checkbox`] is currently checked.
590        is_checked: bool,
591    },
592    /// The [`Checkbox`] can be interacted with and it is being hovered.
593    Hovered {
594        /// Indicates if the [`Checkbox`] is currently checked.
595        is_checked: bool,
596    },
597    /// The [`Checkbox`] has keyboard focus.
598    Focused {
599        /// Indicates if the [`Checkbox`] is currently checked.
600        is_checked: bool,
601    },
602    /// The [`Checkbox`] cannot be interacted with.
603    Disabled {
604        /// Indicates if the [`Checkbox`] is currently checked.
605        is_checked: bool,
606    },
607}
608
609/// The style of a checkbox.
610#[derive(Debug, Clone, Copy, PartialEq)]
611pub struct Style {
612    /// The [`Background`] of the checkbox.
613    pub background: Background,
614    /// The icon [`Color`] of the checkbox.
615    pub icon_color: Color,
616    /// The [`Border`] of the checkbox.
617    pub border: Border,
618    /// The [`Shadow`] of the checkbox.
619    pub shadow: Shadow,
620    /// The text [`Color`] of the checkbox.
621    pub text_color: Option<Color>,
622}
623
624/// The theme catalog of a [`Checkbox`].
625pub trait Catalog: Sized {
626    /// The item class of the [`Catalog`].
627    type Class<'a>;
628
629    /// The default class produced by the [`Catalog`].
630    fn default<'a>() -> Self::Class<'a>;
631
632    /// The [`Style`] of a class with the given status.
633    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
634}
635
636/// A styling function for a [`Checkbox`].
637///
638/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
639pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
640
641impl Catalog for Theme {
642    type Class<'a> = StyleFn<'a, Self>;
643
644    fn default<'a>() -> Self::Class<'a> {
645        Box::new(primary)
646    }
647
648    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
649        class(self, status)
650    }
651}
652
653/// A primary checkbox; denoting a main toggle.
654pub fn primary(theme: &Theme, status: Status) -> Style {
655    let palette = theme.palette();
656
657    match status {
658        Status::Active { is_checked } => styled(
659            palette.background.strong.color,
660            palette.background.base,
661            palette.primary.base.text,
662            palette.primary.base,
663            is_checked,
664        ),
665        Status::Hovered { is_checked } => styled(
666            palette.background.strong.color,
667            palette.background.weak,
668            palette.primary.base.text,
669            palette.primary.strong,
670            is_checked,
671        ),
672        Status::Focused { is_checked } => {
673            let base = styled(
674                palette.background.strong.color,
675                palette.background.base,
676                palette.primary.base.text,
677                palette.primary.base,
678                is_checked,
679            );
680            let accent = palette.primary.strong.color;
681            let page_bg = palette.background.base.color;
682            let widget_bg = if is_checked {
683                palette.primary.base.color
684            } else {
685                palette.background.base.color
686            };
687
688            Style {
689                border: Border {
690                    color: palette::focus_border_color(widget_bg, accent, page_bg),
691                    width: 2.0,
692                    ..base.border
693                },
694                shadow: palette::focus_shadow(accent, page_bg),
695                ..base
696            }
697        }
698        Status::Disabled { is_checked } => styled(
699            palette.background.weak.color,
700            palette.background.weaker,
701            palette.primary.base.text,
702            palette.background.strong,
703            is_checked,
704        ),
705    }
706}
707
708/// A secondary checkbox; denoting a complementary toggle.
709pub fn secondary(theme: &Theme, status: Status) -> Style {
710    let palette = theme.palette();
711
712    match status {
713        Status::Active { is_checked } => styled(
714            palette.background.strong.color,
715            palette.background.base,
716            palette.background.base.text,
717            palette.background.strong,
718            is_checked,
719        ),
720        Status::Hovered { is_checked } => styled(
721            palette.background.strong.color,
722            palette.background.weak,
723            palette.background.base.text,
724            palette.background.strong,
725            is_checked,
726        ),
727        Status::Focused { is_checked } => {
728            let base = styled(
729                palette.background.strong.color,
730                palette.background.base,
731                palette.background.base.text,
732                palette.background.strong,
733                is_checked,
734            );
735            let accent = palette.primary.strong.color;
736            let page_bg = palette.background.base.color;
737            let widget_bg = if is_checked {
738                palette.background.strong.color
739            } else {
740                palette.background.base.color
741            };
742
743            Style {
744                border: Border {
745                    color: palette::focus_border_color(widget_bg, accent, page_bg),
746                    width: 2.0,
747                    ..base.border
748                },
749                shadow: palette::focus_shadow(accent, page_bg),
750                ..base
751            }
752        }
753        Status::Disabled { is_checked } => styled(
754            palette.background.weak.color,
755            palette.background.weak,
756            palette.background.base.text,
757            palette.background.weak,
758            is_checked,
759        ),
760    }
761}
762
763/// A success checkbox; denoting a positive toggle.
764pub fn success(theme: &Theme, status: Status) -> Style {
765    let palette = theme.palette();
766
767    match status {
768        Status::Active { is_checked } => styled(
769            palette.background.weak.color,
770            palette.background.base,
771            palette.success.base.text,
772            palette.success.base,
773            is_checked,
774        ),
775        Status::Hovered { is_checked } => styled(
776            palette.background.strong.color,
777            palette.background.weak,
778            palette.success.base.text,
779            palette.success.strong,
780            is_checked,
781        ),
782        Status::Focused { is_checked } => {
783            let base = styled(
784                palette.background.weak.color,
785                palette.background.base,
786                palette.success.base.text,
787                palette.success.base,
788                is_checked,
789            );
790            let accent = palette.primary.strong.color;
791            let page_bg = palette.background.base.color;
792            let widget_bg = if is_checked {
793                palette.success.base.color
794            } else {
795                palette.background.base.color
796            };
797
798            Style {
799                border: Border {
800                    color: palette::focus_border_color(widget_bg, accent, page_bg),
801                    width: 2.0,
802                    ..base.border
803                },
804                shadow: palette::focus_shadow(accent, page_bg),
805                ..base
806            }
807        }
808        Status::Disabled { is_checked } => styled(
809            palette.background.weak.color,
810            palette.background.weak,
811            palette.success.base.text,
812            palette.success.weak,
813            is_checked,
814        ),
815    }
816}
817
818/// A danger checkbox; denoting a negative toggle.
819pub fn danger(theme: &Theme, status: Status) -> Style {
820    let palette = theme.palette();
821
822    match status {
823        Status::Active { is_checked } => styled(
824            palette.background.strong.color,
825            palette.background.base,
826            palette.danger.base.text,
827            palette.danger.base,
828            is_checked,
829        ),
830        Status::Hovered { is_checked } => styled(
831            palette.background.strong.color,
832            palette.background.weak,
833            palette.danger.base.text,
834            palette.danger.strong,
835            is_checked,
836        ),
837        Status::Focused { is_checked } => {
838            let base = styled(
839                palette.background.strong.color,
840                palette.background.base,
841                palette.danger.base.text,
842                palette.danger.base,
843                is_checked,
844            );
845            let accent = palette.primary.strong.color;
846            let page_bg = palette.background.base.color;
847            let widget_bg = if is_checked {
848                palette.danger.base.color
849            } else {
850                palette.background.base.color
851            };
852
853            Style {
854                border: Border {
855                    color: palette::focus_border_color(widget_bg, accent, page_bg),
856                    width: 2.0,
857                    ..base.border
858                },
859                shadow: palette::focus_shadow(accent, page_bg),
860                ..base
861            }
862        }
863        Status::Disabled { is_checked } => styled(
864            palette.background.weak.color,
865            palette.background.weak,
866            palette.danger.base.text,
867            palette.danger.weak,
868            is_checked,
869        ),
870    }
871}
872
873fn styled(
874    border_color: Color,
875    base: palette::Pair,
876    icon_color: Color,
877    accent: palette::Pair,
878    is_checked: bool,
879) -> Style {
880    let (background, border) = if is_checked {
881        (accent, accent.color)
882    } else {
883        (base, border_color)
884    };
885
886    Style {
887        background: Background::Color(background.color),
888        icon_color,
889        border: Border {
890            radius: 2.0.into(),
891            width: 1.0,
892            color: border,
893        },
894        shadow: Shadow::default(),
895        text_color: None,
896    }
897}
898
899#[cfg(test)]
900mod tests {
901    use super::*;
902    use crate::core::widget::operation::focusable::Focusable;
903
904    type TestState = State<()>;
905
906    #[test]
907    fn focusable_trait() {
908        let mut state = TestState::default();
909        assert!(!state.is_focused());
910        assert!(!state.focus_visible);
911        state.focus();
912        assert!(state.is_focused());
913        assert!(state.focus_visible);
914        state.unfocus();
915        assert!(!state.is_focused());
916        assert!(!state.focus_visible);
917    }
918}