freya_components/
checkbox.rs

1use dioxus::prelude::*;
2use freya_elements::{
3    self as dioxus_elements,
4    events::KeyboardEvent,
5};
6use freya_hooks::{
7    use_applied_theme,
8    use_focus,
9    CheckboxTheme,
10    CheckboxThemeWith,
11};
12
13use crate::TickIcon;
14
15/// Controlled `Checkbox` component.
16///
17/// # Styling
18/// Inherits the [`CheckboxTheme`](freya_hooks::CheckboxTheme) theme.
19///
20/// # Example
21///
22/// ```rust
23/// # use std::collections::HashSet;
24/// # use freya::prelude::*;
25/// #[derive(PartialEq, Eq, Hash)]
26/// enum Choice {
27///     First,
28///     Second,
29/// }
30///
31/// fn app() -> Element {
32///     let mut selected = use_signal::<HashSet<Choice>>(HashSet::default);
33///     rsx!(
34///         Tile {
35///             onselect: move |_| {
36///                 if selected.read().contains(&Choice::First) {
37///                     selected.write().remove(&Choice::First);
38///                 } else {
39///                     selected.write().insert(Choice::First);
40///                 }
41///             },
42///             leading: rsx!(
43///                 Checkbox {
44///                     selected: selected.read().contains(&Choice::First),
45///                 }
46///             ),
47///             label { "First choice" }
48///         }
49///         Tile {
50///             onselect: move |_| {
51///                 if selected.read().contains(&Choice::Second) {
52///                     selected.write().remove(&Choice::Second);
53///                 } else {
54///                     selected.write().insert(Choice::Second);
55///                 }
56///             },
57///             leading: rsx!(
58///                 Checkbox {
59///                     selected: selected.read().contains(&Choice::Second),
60///                 }
61///             ),
62///             label { "Second choice" }
63///         }
64///     )
65/// }
66///
67/// # // DISABLED
68/// # use freya_testing::prelude::*;
69/// # launch_doc(|| {
70/// #   rsx!(
71/// #       Preview {
72/// #           Checkbox {
73/// #               selected: true,
74/// #           },
75/// #       }
76/// #   )
77/// # }, (250., 250.).into(), "./images/gallery_checkbox.png");
78/// ```
79///
80/// # Preview
81/// ![Checkbox Preview][checkbox]
82#[cfg_attr(feature = "docs",
83    doc = embed_doc_image::embed_image!("checkbox", "images/gallery_checkbox.png")
84)]
85#[allow(non_snake_case)]
86#[component]
87pub fn Checkbox(
88    /// Indicate whether this checkbox is selected or not.
89    selected: bool,
90    /// Theme override.
91    theme: Option<CheckboxThemeWith>,
92) -> Element {
93    let focus = use_focus();
94    let CheckboxTheme {
95        border_fill,
96        unselected_fill,
97        selected_fill,
98        selected_icon_fill,
99    } = use_applied_theme!(&theme, checkbox);
100    let (inner_fill, outer_fill) = if selected {
101        (selected_fill.as_ref(), selected_fill.as_ref())
102    } else {
103        ("transparent", unselected_fill.as_ref())
104    };
105    let border = if focus.is_focused_with_keyboard() {
106        format!("2 inner {outer_fill}, 4 outer {border_fill}")
107    } else {
108        format!("2 inner {outer_fill}")
109    };
110
111    let onkeydown = move |e: KeyboardEvent| {
112        if !focus.validate_keydown(&e) {
113            e.stop_propagation();
114        }
115    };
116
117    rsx!(
118        rect {
119            a11y_id: focus.attribute(),
120            width: "18",
121            height: "18",
122            padding: "4",
123            main_align: "center",
124            cross_align: "center",
125            corner_radius: "4",
126            border,
127            background: "{inner_fill}",
128            onkeydown,
129            if selected {
130                TickIcon {
131                    fill: selected_icon_fill
132                }
133            }
134        }
135    )
136}
137
138#[cfg(test)]
139mod test {
140    use std::collections::HashSet;
141
142    use dioxus::prelude::use_signal;
143    use freya::prelude::*;
144    use freya_testing::prelude::*;
145
146    #[tokio::test]
147    pub async fn checkbox() {
148        #[derive(PartialEq, Eq, Hash)]
149        enum Choice {
150            First,
151            Second,
152            Third,
153        }
154
155        fn checkbox_app() -> Element {
156            let mut selected = use_signal::<HashSet<Choice>>(HashSet::default);
157
158            rsx!(
159                Tile {
160                    onselect: move |_| {
161                        if selected.read().contains(&Choice::First) {
162                            selected.write().remove(&Choice::First);
163                        } else {
164                            selected.write().insert(Choice::First);
165                        }
166                    },
167                    leading: rsx!(
168                        Checkbox {
169                            selected: selected.read().contains(&Choice::First),
170                        }
171                    ),
172                    label { "First choice" }
173                }
174                Tile {
175                    onselect: move |_| {
176                        if selected.read().contains(&Choice::Second) {
177                            selected.write().remove(&Choice::Second);
178                        } else {
179                            selected.write().insert(Choice::Second);
180                        }
181                    },
182                    leading: rsx!(
183                        Checkbox {
184                            selected: selected.read().contains(&Choice::Second),
185                        }
186                    ),
187                    label { "Second choice" }
188                }
189                Tile {
190                    onselect: move |_| {
191                        if selected.read().contains(&Choice::Third) {
192                            selected.write().remove(&Choice::Third);
193                        } else {
194                            selected.write().insert(Choice::Third);
195                        }
196                    },
197                    leading: rsx!(
198                        Checkbox {
199                            selected: selected.read().contains(&Choice::Third),
200                        }
201                    ),
202                    label { "Third choice" }
203                }
204            )
205        }
206
207        let mut utils = launch_test(checkbox_app);
208        let root = utils.root();
209        utils.wait_for_update().await;
210
211        // If the inner square exists it means that the Checkbox is selected, otherwise it isn't
212        assert!(root.get(0).get(0).get(0).get(0).is_placeholder());
213        assert!(root.get(1).get(0).get(0).get(0).is_placeholder());
214        assert!(root.get(2).get(0).get(0).get(0).is_placeholder());
215
216        utils.click_cursor((20., 50.)).await;
217
218        assert!(root.get(0).get(0).get(0).get(0).is_placeholder());
219        assert!(root.get(1).get(0).get(0).get(0).is_element());
220        assert!(root.get(2).get(0).get(0).get(0).is_placeholder());
221
222        utils.click_cursor((10., 90.)).await;
223        utils.wait_for_update().await;
224
225        assert!(root.get(0).get(0).get(0).get(0).is_placeholder());
226        assert!(root.get(1).get(0).get(0).get(0).is_element());
227        assert!(root.get(2).get(0).get(0).get(0).is_element());
228
229        utils.click_cursor((10., 10.)).await;
230        utils.click_cursor((10., 50.)).await;
231        utils.wait_for_update().await;
232
233        assert!(root.get(0).get(0).get(0).get(0).is_element());
234        assert!(root.get(1).get(0).get(0).get(0).is_placeholder());
235        assert!(root.get(2).get(0).get(0).get(0).is_element());
236    }
237}