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