freya_components/
checkbox.rs

1use freya_animation::prelude::*;
2use freya_core::prelude::*;
3use torin::prelude::*;
4
5use crate::{
6    get_theme,
7    icons::tick::TickIcon,
8    theming::component_themes::{
9        CheckboxTheme,
10        CheckboxThemePartial,
11    },
12};
13
14/// Checkbox component.
15///
16/// # Example
17///
18/// ```rust
19/// # use std::collections::HashSet;
20/// # use freya::prelude::*;
21/// fn app() -> impl IntoElement {
22///     let mut checked = use_state(|| false);
23///
24///     rect()
25///         .spacing(8.)
26///         .child(
27///             Tile::new()
28///                 .on_select(move |_| checked.toggle())
29///                 .child(Checkbox::new().selected(checked()))
30///                 .leading("Click to check"),
31///         )
32///         .child(
33///             Tile::new()
34///                 .on_select(move |_| checked.toggle())
35///                 .child(Checkbox::new().selected(!checked()))
36///                 .child("Click to check"),
37///         )
38/// }
39///
40/// # use freya_testing::prelude::*;
41/// # launch_doc(|| {
42/// #   rect().center().expanded().child(app())
43/// # }, "./images/gallery_checkbox.png").render();
44/// ```
45///
46/// # Preview
47/// ![Checkbox Preview][checkbox]
48#[cfg_attr(feature = "docs",
49    doc = embed_doc_image::embed_image!("checkbox", "images/gallery_checkbox.png")
50)]
51#[derive(Clone, PartialEq)]
52pub struct Checkbox {
53    pub(crate) theme: Option<CheckboxThemePartial>,
54    selected: bool,
55    key: DiffKey,
56    size: f32,
57}
58
59impl Default for Checkbox {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl Checkbox {
66    pub fn new() -> Self {
67        Self {
68            selected: false,
69            theme: None,
70            key: DiffKey::None,
71            size: 20.,
72        }
73    }
74
75    pub fn selected(mut self, selected: bool) -> Self {
76        self.selected = selected;
77        self
78    }
79
80    pub fn theme(mut self, theme: CheckboxThemePartial) -> Self {
81        self.theme = Some(theme);
82        self
83    }
84
85    pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
86        self.key = key.into();
87        self
88    }
89
90    pub fn size(mut self, size: impl Into<f32>) -> Self {
91        self.size = size.into();
92        self
93    }
94}
95
96impl Component for Checkbox {
97    fn render(&self) -> impl IntoElement {
98        let focus = use_focus();
99        let focus_status = use_focus_status(focus);
100        let CheckboxTheme {
101            border_fill,
102            unselected_fill,
103            selected_fill,
104            selected_icon_fill,
105        } = get_theme!(&self.theme, checkbox);
106
107        let animation = use_animation_with_dependencies(&self.selected, move |conf, selected| {
108            conf.on_change(OnChange::Rerun);
109            conf.on_creation(OnCreation::Finish);
110
111            let scale = AnimNum::new(0.6, 1.)
112                .time(350)
113                .ease(Ease::Out)
114                .function(Function::Expo);
115            let opacity = AnimNum::new(0., 1.)
116                .time(350)
117                .ease(Ease::Out)
118                .function(Function::Expo);
119
120            if *selected {
121                (scale, opacity)
122            } else {
123                (scale.into_reversed(), opacity.into_reversed())
124            }
125        });
126
127        let (scale, opacity) = animation.read().value();
128
129        let (background, fill) = if self.selected {
130            (selected_fill, selected_fill)
131        } else {
132            (Color::TRANSPARENT, unselected_fill)
133        };
134
135        let border = Border::new()
136            .fill(fill)
137            .width(2.)
138            .alignment(BorderAlignment::Inner);
139
140        let focused_border = (focus_status() == FocusStatus::Keyboard).then(|| {
141            Border::new()
142                .fill(border_fill)
143                .width((self.size * 0.15).ceil())
144                .alignment(BorderAlignment::Outer)
145        });
146
147        rect()
148            .a11y_id(focus.a11y_id())
149            .a11y_focusable(Focusable::Enabled)
150            .a11y_role(AccessibilityRole::CheckBox)
151            .width(Size::px(self.size))
152            .height(Size::px(self.size))
153            .padding(Gaps::new_all(4.0))
154            .main_align(Alignment::center())
155            .cross_align(Alignment::center())
156            .corner_radius(CornerRadius::new_all(self.size * 0.24))
157            .border(border)
158            .border(focused_border)
159            .background(background)
160            .on_key_down({
161                move |e: Event<KeyboardEventData>| {
162                    if !Focus::is_pressed(&e) {
163                        e.stop_propagation();
164                    }
165                }
166            })
167            .maybe_child((self.selected || opacity > 0.).then(|| {
168                rect().opacity(opacity).scale(scale).child(
169                    TickIcon::new()
170                        .width(Size::px(self.size * 0.7))
171                        .height(Size::px(self.size * 0.7))
172                        .fill(selected_icon_fill),
173                )
174            }))
175    }
176
177    fn render_key(&self) -> DiffKey {
178        self.key.clone().or(self.default_key())
179    }
180}