Skip to main content

freya_components/
button.rs

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