freya_components/
button.rs

1use dioxus::prelude::*;
2use freya_core::platform::CursorIcon;
3use freya_elements::{
4    self as dioxus_elements,
5    events::{
6        KeyboardEvent,
7        PointerEvent,
8        PointerType,
9    },
10    MouseButton,
11    TouchPhase,
12};
13use freya_hooks::{
14    use_applied_theme,
15    use_focus,
16    use_platform,
17    ButtonTheme,
18    ButtonThemeWith,
19};
20
21/// Properties for the [`Button`], [`FilledButton`] and [`OutlineButton`] components.
22#[derive(Props, Clone, PartialEq)]
23pub struct ButtonProps {
24    /// Theme override.
25    pub theme: Option<ButtonThemeWith>,
26    /// Inner children for the button.
27    pub children: Element,
28    /// Event handler for when the button is pressed.
29    pub onpress: Option<EventHandler<PressEvent>>,
30    /// Event handler for when the button is clicked. Not recommended, use `onpress` instead.
31    pub onclick: Option<EventHandler<()>>,
32}
33
34/// Clickable button.
35///
36/// # Styling
37/// Inherits the [`ButtonTheme`](freya_hooks::ButtonTheme) theme.
38///
39/// # Example
40///
41/// ```rust
42/// # use freya::prelude::*;
43/// fn app() -> Element {
44///     rsx!(
45///         Button {
46///             onpress: |_| println!("clicked"),
47///             label {
48///                 "Click this"
49///             }
50///         }
51///     )
52/// }
53/// # use freya_testing::prelude::*;
54/// # launch_doc(|| {
55/// #   rsx!(
56/// #       Preview {
57/// #           {app()}
58/// #       }
59/// #   )
60/// # }, (250., 250.).into(), "./images/gallery_button.png");
61/// ```
62///
63/// # Preview
64/// ![Button Preview][button]
65#[cfg_attr(feature = "docs",
66    doc = embed_doc_image::embed_image!("button", "images/gallery_button.png")
67)]
68#[allow(non_snake_case)]
69pub fn Button(props: ButtonProps) -> Element {
70    let theme = use_applied_theme!(&props.theme, button);
71    ButtonBase(BaseButtonProps {
72        theme,
73        children: props.children,
74        onpress: props.onpress,
75        onclick: props.onclick,
76    })
77}
78
79/// Clickable button with a solid fill color.
80///
81/// # Styling
82/// Inherits the filled [`ButtonTheme`](freya_hooks::ButtonTheme) theme.
83///
84/// # Example
85///
86/// ```rust
87/// # use freya::prelude::*;
88/// fn app() -> Element {
89///     rsx!(
90///         FilledButton {
91///             onpress: |_| println!("clicked"),
92///             label {
93///                 "Click this"
94///             }
95///         }
96///     )
97/// }
98/// # use freya_testing::prelude::*;
99/// # launch_doc(|| {
100/// #   rsx!(
101/// #       Preview {
102/// #           {app()}
103/// #       }
104/// #   )
105/// # }, (250., 250.).into(), "./images/gallery_filled_button.png");
106/// ```
107///
108/// # Preview
109/// ![FilledButton Preview][filled_button]
110#[cfg_attr(feature = "docs",
111    doc = embed_doc_image::embed_image!("filled_button", "images/gallery_filled_button.png")
112)]
113#[allow(non_snake_case)]
114pub fn FilledButton(props: ButtonProps) -> Element {
115    let theme = use_applied_theme!(&props.theme, filled_button);
116    ButtonBase(BaseButtonProps {
117        theme,
118        children: props.children,
119        onpress: props.onpress,
120        onclick: props.onclick,
121    })
122}
123
124/// Clickable button with an outline style.
125///
126/// # Styling
127/// Inherits the outline [`ButtonTheme`](freya_hooks::ButtonTheme) theme.
128///
129/// # Example
130///
131/// ```rust
132/// # use freya::prelude::*;
133/// fn app() -> Element {
134///     rsx!(
135///         OutlineButton {
136///             onpress: |_| println!("clicked"),
137///             label {
138///                 "Click this"
139///             }
140///         }
141///     )
142/// }
143/// # use freya_testing::prelude::*;
144/// # launch_doc(|| {
145/// #   rsx!(
146/// #       Preview {
147/// #           {app()}
148/// #       }
149/// #   )
150/// # }, (250., 250.).into(), "./images/gallery_outline_button.png");
151/// ```
152///
153/// # Preview
154/// ![OutlineButton Preview][outline_button]
155#[cfg_attr(feature = "docs",
156    doc = embed_doc_image::embed_image!("outline_button", "images/gallery_outline_button.png")
157)]
158#[allow(non_snake_case)]
159pub fn OutlineButton(props: ButtonProps) -> Element {
160    let theme = use_applied_theme!(&props.theme, outline_button);
161    ButtonBase(BaseButtonProps {
162        theme,
163        children: props.children,
164        onpress: props.onpress,
165        onclick: props.onclick,
166    })
167}
168
169pub enum PressEvent {
170    Pointer(PointerEvent),
171    Key(KeyboardEvent),
172}
173
174impl PressEvent {
175    pub fn stop_propagation(&self) {
176        match &self {
177            Self::Pointer(ev) => ev.stop_propagation(),
178            Self::Key(ev) => ev.stop_propagation(),
179        }
180    }
181}
182
183/// Properties for the [`Button`] component.
184#[derive(Props, Clone, PartialEq)]
185pub struct BaseButtonProps {
186    /// Theme.
187    pub theme: ButtonTheme,
188    /// Inner children for the button.
189    pub children: Element,
190    /// Event handler for when the button is pressed.
191    ///
192    /// This will fire upon **mouse click** or pressing the **enter key**.
193    pub onpress: Option<EventHandler<PressEvent>>,
194    /// Event handler for when the button is clicked. Not recommended, use `onpress` instead.
195    pub onclick: Option<EventHandler<()>>,
196}
197
198/// Identifies the current status of the Button.
199#[derive(Debug, Default, PartialEq, Clone, Copy)]
200pub enum ButtonStatus {
201    /// Default state.
202    #[default]
203    Idle,
204    /// Mouse is hovering the button.
205    Hovering,
206}
207
208#[allow(non_snake_case)]
209pub fn ButtonBase(
210    BaseButtonProps {
211        onpress,
212        children,
213        theme,
214        onclick,
215    }: BaseButtonProps,
216) -> Element {
217    let mut focus = use_focus();
218    let mut status = use_signal(ButtonStatus::default);
219    let platform = use_platform();
220
221    let a11y_id = focus.attribute();
222
223    let ButtonTheme {
224        background,
225        hover_background,
226        border_fill,
227        focus_border_fill,
228        padding,
229        margin,
230        corner_radius,
231        width,
232        height,
233        font_theme,
234        shadow,
235    } = theme;
236
237    let onpointerup = {
238        to_owned![onpress, onclick];
239        move |ev: PointerEvent| {
240            focus.request_focus();
241            if let Some(onpress) = &onpress {
242                let is_valid = match ev.data.pointer_type {
243                    PointerType::Mouse {
244                        trigger_button: Some(MouseButton::Left),
245                    } => true,
246                    PointerType::Touch { phase, .. } => phase == TouchPhase::Ended,
247                    _ => false,
248                };
249                if is_valid {
250                    onpress.call(PressEvent::Pointer(ev))
251                }
252            } else if let Some(onclick) = onclick {
253                if let PointerType::Mouse {
254                    trigger_button: Some(MouseButton::Left),
255                    ..
256                } = ev.data.pointer_type
257                {
258                    onclick.call(())
259                }
260            }
261        }
262    };
263
264    use_drop(move || {
265        if *status.read() == ButtonStatus::Hovering {
266            platform.set_cursor(CursorIcon::default());
267        }
268    });
269
270    let onmouseenter = move |_| {
271        platform.set_cursor(CursorIcon::Pointer);
272        status.set(ButtonStatus::Hovering);
273    };
274
275    let onmouseleave = move |_| {
276        platform.set_cursor(CursorIcon::default());
277        status.set(ButtonStatus::default());
278    };
279
280    let onkeydown = move |ev: KeyboardEvent| {
281        if focus.validate_keydown(&ev) {
282            if let Some(onpress) = &onpress {
283                onpress.call(PressEvent::Key(ev))
284            }
285        }
286    };
287
288    let background = match *status.read() {
289        ButtonStatus::Hovering => hover_background,
290        ButtonStatus::Idle => background,
291    };
292    let border = if focus.is_focused_with_keyboard() {
293        format!("2 inner {focus_border_fill}")
294    } else {
295        format!("1 inner {border_fill}")
296    };
297
298    rsx!(
299        rect {
300            onpointerup,
301            onmouseenter,
302            onmouseleave,
303            onkeydown,
304            a11y_id,
305            width: "{width}",
306            height: "{height}",
307            padding: "{padding}",
308            margin: "{margin}",
309            overflow: "clip",
310            a11y_role:"button",
311            color: "{font_theme.color}",
312            shadow: "{shadow}",
313            border,
314            corner_radius: "{corner_radius}",
315            background: "{background}",
316            text_height: "disable-least-ascent",
317            main_align: "center",
318            cross_align: "center",
319            {&children}
320        }
321    )
322}
323
324#[cfg(test)]
325mod test {
326    use freya::prelude::*;
327    use freya_testing::prelude::*;
328
329    #[tokio::test]
330    pub async fn button() {
331        fn button_app() -> Element {
332            let mut state = use_signal(|| false);
333
334            rsx!(
335                Button {
336                    onpress: move |_| state.toggle(),
337                    label {
338                        "{state}"
339                    }
340                }
341            )
342        }
343
344        let mut utils = launch_test(button_app);
345        let root = utils.root();
346        let label = root.get(0).get(0);
347        utils.wait_for_update().await;
348
349        assert_eq!(label.get(0).text(), Some("false"));
350
351        utils.click_cursor((15.0, 15.0)).await;
352
353        assert_eq!(label.get(0).text(), Some("true"));
354
355        utils.push_event(TestEvent::Touch {
356            name: EventName::TouchStart,
357            location: (15.0, 15.0).into(),
358            finger_id: 1,
359            phase: TouchPhase::Started,
360            force: None,
361        });
362        utils.wait_for_update().await;
363
364        utils.push_event(TestEvent::Touch {
365            name: EventName::TouchEnd,
366            location: (15.0, 15.0).into(),
367            finger_id: 1,
368            phase: TouchPhase::Ended,
369            force: None,
370        });
371        utils.wait_for_update().await;
372
373        assert_eq!(label.get(0).text(), Some("false"));
374    }
375}