Skip to main content

freya_components/
checkbox.rs

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