Skip to main content

freya_components/
button.rs

1use freya_core::prelude::*;
2
3use crate::{
4    get_theme,
5    theming::component_themes::{
6        ButtonColorsThemePartial,
7        ButtonLayoutThemePartial,
8        ButtonLayoutThemePartialExt,
9    },
10};
11
12#[derive(Clone, PartialEq)]
13pub enum ButtonStyleVariant {
14    Normal,
15    Filled,
16    Outline,
17    Flat,
18}
19
20#[derive(Clone, PartialEq)]
21pub enum ButtonLayoutVariant {
22    Normal,
23    Compact,
24    Expanded,
25}
26
27/// Simply a button.
28///
29/// ## **Normal**
30///
31/// ```rust
32/// # use freya::prelude::*;
33/// fn app() -> impl IntoElement {
34///     Button::new()
35///         .on_press(|_| println!("Pressed!"))
36///         .child("Press me")
37/// }
38/// # use freya_testing::prelude::*;
39/// # launch_doc(|| {
40/// #   rect().center().expanded().child(app())
41/// # }, "./images/gallery_button.png").render();
42/// ```
43/// ## **Filled**
44///
45/// ```rust
46/// # use freya::prelude::*;
47/// fn app() -> impl IntoElement {
48///     Button::new()
49///         .on_press(|_| println!("Pressed!"))
50///         .filled()
51///         .child("Press me")
52/// }
53/// # use freya_testing::prelude::*;
54/// # launch_doc(|| {
55/// #   rect().center().expanded().child(app())
56/// # }, "./images/gallery_filled_button.png").render();
57/// ```
58/// ## **Outline**
59///
60/// ```rust
61/// # use freya::prelude::*;
62/// fn app() -> impl IntoElement {
63///     Button::new()
64///         .on_press(|_| println!("Pressed!"))
65///         .outline()
66///         .child("Press me")
67/// }
68/// # use freya_testing::prelude::*;
69/// # launch_doc(|| {
70/// #   rect().center().expanded().child(app())
71/// # }, "./images/gallery_outline_button.png").render();
72/// ```
73/// ## **Flat**
74///
75/// ```rust
76/// # use freya::prelude::*;
77/// fn app() -> impl IntoElement {
78///     Button::new()
79///         .on_press(|_| println!("Pressed!"))
80///         .flat()
81///         .child("Press me")
82/// }
83/// # use freya_testing::prelude::*;
84/// # launch_doc(|| {
85/// #   rect().center().expanded().child(app())
86/// # }, "./images/gallery_flat_button.png").render();
87/// ```
88///
89/// # Preview
90/// ![Button Preview][button]
91/// ![Outline Button Preview][outline_button]
92/// ![Filled Button Preview][filled_button]
93/// ![Flat Button Preview][flat_button]
94#[cfg_attr(feature = "docs",
95    doc = embed_doc_image::embed_image!("button", "images/gallery_button.png"),
96    doc = embed_doc_image::embed_image!("filled_button", "images/gallery_filled_button.png"),
97    doc = embed_doc_image::embed_image!("outline_button", "images/gallery_outline_button.png"),
98    doc = embed_doc_image::embed_image!("flat_button", "images/gallery_flat_button.png"),
99)]
100#[derive(Clone, PartialEq)]
101pub struct Button {
102    pub(crate) theme_colors: Option<ButtonColorsThemePartial>,
103    pub(crate) theme_layout: Option<ButtonLayoutThemePartial>,
104    elements: Vec<Element>,
105    on_press: Option<EventHandler<Event<PressEventData>>>,
106    on_secondary_down: Option<EventHandler<Event<PressEventData>>>,
107    on_pointer_down: Option<EventHandler<Event<PointerEventData>>>,
108    key: DiffKey,
109    style_variant: ButtonStyleVariant,
110    layout_variant: ButtonLayoutVariant,
111    enabled: bool,
112    focusable: bool,
113}
114
115impl Default for Button {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121impl ChildrenExt for Button {
122    fn get_children(&mut self) -> &mut Vec<Element> {
123        &mut self.elements
124    }
125}
126
127impl KeyExt for Button {
128    fn write_key(&mut self) -> &mut DiffKey {
129        &mut self.key
130    }
131}
132
133impl Button {
134    pub fn new() -> Self {
135        Self {
136            theme_colors: None,
137            theme_layout: None,
138            style_variant: ButtonStyleVariant::Normal,
139            layout_variant: ButtonLayoutVariant::Normal,
140            on_press: None,
141            on_secondary_down: None,
142            on_pointer_down: None,
143            elements: Vec::default(),
144            enabled: true,
145            focusable: true,
146            key: DiffKey::None,
147        }
148    }
149
150    pub fn get_layout_variant(&self) -> &ButtonLayoutVariant {
151        &self.layout_variant
152    }
153
154    pub fn get_theme_layout(&self) -> Option<&ButtonLayoutThemePartial> {
155        self.theme_layout.as_ref()
156    }
157
158    pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
159        self.enabled = enabled.into();
160        self
161    }
162
163    pub fn focusable(mut self, focusable: impl Into<bool>) -> Self {
164        self.focusable = focusable.into();
165        self
166    }
167
168    pub fn style_variant(mut self, style_variant: impl Into<ButtonStyleVariant>) -> Self {
169        self.style_variant = style_variant.into();
170        self
171    }
172
173    pub fn layout_variant(mut self, layout_variant: impl Into<ButtonLayoutVariant>) -> Self {
174        self.layout_variant = layout_variant.into();
175        self
176    }
177
178    pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
179        self.on_press = Some(on_press.into());
180        self
181    }
182
183    pub fn on_secondary_down(
184        mut self,
185        on_secondary_down: impl Into<EventHandler<Event<PressEventData>>>,
186    ) -> Self {
187        self.on_secondary_down = Some(on_secondary_down.into());
188        self
189    }
190
191    pub fn on_pointer_down(
192        mut self,
193        on_pointer_down: impl Into<EventHandler<Event<PointerEventData>>>,
194    ) -> Self {
195        self.on_pointer_down = Some(on_pointer_down.into());
196        self
197    }
198
199    pub fn theme_colors(mut self, theme: ButtonColorsThemePartial) -> Self {
200        self.theme_colors = Some(theme);
201        self
202    }
203
204    pub fn theme_layout(mut self, theme: ButtonLayoutThemePartial) -> Self {
205        self.theme_layout = Some(theme);
206        self
207    }
208
209    /// Shortcut for [Self::theme_layout] and [ButtonLayoutVariant::Compact].
210    pub fn compact(self) -> Self {
211        self.layout_variant(ButtonLayoutVariant::Compact)
212    }
213
214    /// Shortcut for [Self::theme_layout] and [ButtonLayoutVariant::Expanded].
215    pub fn expanded(self) -> Self {
216        self.layout_variant(ButtonLayoutVariant::Expanded)
217    }
218
219    /// Shortcut for [Self::style_variant] and [ButtonStyleVariant::Filled].
220    pub fn filled(self) -> Self {
221        self.style_variant(ButtonStyleVariant::Filled)
222    }
223
224    /// Shortcut for [Self::style_variant] and [ButtonStyleVariant::Outline].
225    pub fn outline(self) -> Self {
226        self.style_variant(ButtonStyleVariant::Outline)
227    }
228
229    /// Shortcut for [Self::style_variant] and [ButtonStyleVariant::Flat].
230    pub fn flat(self) -> Self {
231        self.style_variant(ButtonStyleVariant::Flat)
232    }
233}
234
235impl CornerRadiusExt for Button {
236    fn with_corner_radius(self, corner_radius: f32) -> Self {
237        self.corner_radius(corner_radius)
238    }
239}
240
241impl Component for Button {
242    fn render(&self) -> impl IntoElement {
243        let mut hovering = use_state(|| false);
244        let focus = use_focus();
245        let focus_status = use_focus_status(focus);
246
247        let enabled = use_reactive(&self.enabled);
248        use_drop(move || {
249            if hovering() {
250                Cursor::set(CursorIcon::default());
251            }
252        });
253
254        let theme_colors = match self.style_variant {
255            ButtonStyleVariant::Normal => get_theme!(&self.theme_colors, button),
256            ButtonStyleVariant::Outline => get_theme!(&self.theme_colors, outline_button),
257            ButtonStyleVariant::Filled => get_theme!(&self.theme_colors, filled_button),
258            ButtonStyleVariant::Flat => get_theme!(&self.theme_colors, flat_button),
259        };
260        let theme_layout = match self.layout_variant {
261            ButtonLayoutVariant::Normal => get_theme!(&self.theme_layout, button_layout),
262            ButtonLayoutVariant::Compact => get_theme!(&self.theme_layout, compact_button_layout),
263            ButtonLayoutVariant::Expanded => get_theme!(&self.theme_layout, expanded_button_layout),
264        };
265
266        let border = if focus_status() == FocusStatus::Keyboard {
267            Border::new()
268                .fill(theme_colors.focus_border_fill)
269                .width(2.)
270                .alignment(BorderAlignment::Inner)
271        } else {
272            Border::new()
273                .fill(theme_colors.border_fill.mul_if(!self.enabled, 0.9))
274                .width(1.)
275                .alignment(BorderAlignment::Inner)
276        };
277        let background = if enabled() && hovering() {
278            theme_colors.hover_background
279        } else {
280            theme_colors.background
281        };
282
283        rect()
284            .overflow(Overflow::Clip)
285            .a11y_id(focus.a11y_id())
286            .a11y_focusable(self.enabled && self.focusable)
287            .a11y_role(AccessibilityRole::Button)
288            .background(background.mul_if(!self.enabled, 0.9))
289            .border(border)
290            .padding(theme_layout.padding)
291            .corner_radius(theme_layout.corner_radius)
292            .width(theme_layout.width)
293            .height(theme_layout.height)
294            .color(theme_colors.color.mul_if(!self.enabled, 0.9))
295            .center()
296            .maybe(self.enabled, |rect| {
297                rect.map(self.on_pointer_down.clone(), |rect, on_pointer_down| {
298                    rect.on_pointer_down(move |e: Event<PointerEventData>| {
299                        on_pointer_down.call(e);
300                    })
301                })
302                .on_all_press({
303                    let on_press = self.on_press.clone();
304                    let on_secondary_down = self.on_secondary_down.clone();
305                    move |e: Event<PressEventData>| {
306                        focus.request_focus();
307                        match e.data() {
308                            PressEventData::Mouse(data) => match data.button {
309                                Some(MouseButton::Left) => {
310                                    if let Some(handler) = &on_press {
311                                        handler.call(e);
312                                    }
313                                }
314                                Some(MouseButton::Right) => {
315                                    if let Some(handler) = &on_secondary_down {
316                                        handler.call(e);
317                                    }
318                                }
319                                _ => {}
320                            },
321                            PressEventData::Touch(_) | PressEventData::Keyboard(_) => {
322                                if let Some(handler) = &on_press {
323                                    handler.call(e);
324                                }
325                            }
326                        }
327                    }
328                })
329                .on_pointer_over(move |_| {
330                    hovering.set(true);
331                })
332                .on_pointer_out(move |_| hovering.set_if_modified(false))
333            })
334            .on_pointer_enter(move |_| {
335                if enabled() {
336                    Cursor::set(CursorIcon::Pointer);
337                } else {
338                    Cursor::set(CursorIcon::NotAllowed);
339                }
340            })
341            .on_pointer_leave(move |_| {
342                Cursor::set(CursorIcon::default());
343            })
344            .children(self.elements.clone())
345    }
346
347    fn render_key(&self) -> DiffKey {
348        self.key.clone().or(self.default_key())
349    }
350}