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/// ```
69/// ## **Filled**
70///
71/// ```rust
72/// # use freya::prelude::*;
73/// fn app() -> impl IntoElement {
74///     Button::new()
75///         .on_press(|_| println!("Pressed!"))
76///         .filled()
77///         .child("Press me")
78/// }
79/// ```
80/// ## **Outline**
81///
82/// ```rust
83/// # use freya::prelude::*;
84/// fn app() -> impl IntoElement {
85///     Button::new()
86///         .on_press(|_| println!("Pressed!"))
87///         .outline()
88///         .child("Press me")
89/// }
90/// ```
91/// ## **Flat**
92///
93/// ```rust
94/// # use freya::prelude::*;
95/// fn app() -> impl IntoElement {
96///     Button::new()
97///         .on_press(|_| println!("Pressed!"))
98///         .flat()
99///         .child("Press me")
100/// }
101/// ```
102///
103/// See the [interactive components demo](https://freyaui.dev/demo).
104#[derive(Clone, PartialEq)]
105pub struct Button {
106    pub(crate) theme_colors: Option<ButtonColorsThemePartial>,
107    pub(crate) theme_layout: Option<ButtonLayoutThemePartial>,
108    elements: Vec<Element>,
109    on_press: Option<EventHandler<Event<PressEventData>>>,
110    on_secondary_down: Option<EventHandler<Event<PressEventData>>>,
111    on_pointer_down: Option<EventHandler<Event<PointerEventData>>>,
112    key: DiffKey,
113    style_variant: ButtonStyleVariant,
114    layout_variant: ButtonLayoutVariant,
115    enabled: bool,
116    focusable: bool,
117    cursor_icon: Option<CursorIcon>,
118}
119
120impl Default for Button {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126impl ChildrenExt for Button {
127    fn children_mut(&mut self) -> &mut Vec<Element> {
128        &mut self.elements
129    }
130}
131
132impl KeyExt for Button {
133    fn key_mut(&mut self) -> &mut DiffKey {
134        &mut self.key
135    }
136}
137
138impl Button {
139    pub fn new() -> Self {
140        Self {
141            theme_colors: None,
142            theme_layout: None,
143            style_variant: ButtonStyleVariant::Normal,
144            layout_variant: ButtonLayoutVariant::Normal,
145            on_press: None,
146            on_secondary_down: None,
147            on_pointer_down: None,
148            elements: Vec::default(),
149            enabled: true,
150            focusable: true,
151            cursor_icon: None,
152            key: DiffKey::None,
153        }
154    }
155
156    pub fn get_layout_variant(&self) -> &ButtonLayoutVariant {
157        &self.layout_variant
158    }
159
160    pub fn get_theme_layout(&self) -> Option<&ButtonLayoutThemePartial> {
161        self.theme_layout.as_ref()
162    }
163
164    pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
165        self.enabled = enabled.into();
166        self
167    }
168
169    pub fn focusable(mut self, focusable: impl Into<bool>) -> Self {
170        self.focusable = focusable.into();
171        self
172    }
173
174    pub fn style_variant(mut self, style_variant: impl Into<ButtonStyleVariant>) -> Self {
175        self.style_variant = style_variant.into();
176        self
177    }
178
179    pub fn layout_variant(mut self, layout_variant: impl Into<ButtonLayoutVariant>) -> Self {
180        self.layout_variant = layout_variant.into();
181        self
182    }
183
184    pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
185        self.on_press = Some(on_press.into());
186        self
187    }
188
189    pub fn on_secondary_down(
190        mut self,
191        on_secondary_down: impl Into<EventHandler<Event<PressEventData>>>,
192    ) -> Self {
193        self.on_secondary_down = Some(on_secondary_down.into());
194        self
195    }
196
197    pub fn on_pointer_down(
198        mut self,
199        on_pointer_down: impl Into<EventHandler<Event<PointerEventData>>>,
200    ) -> Self {
201        self.on_pointer_down = Some(on_pointer_down.into());
202        self
203    }
204
205    pub fn theme_colors(mut self, theme: ButtonColorsThemePartial) -> Self {
206        self.theme_colors = Some(theme);
207        self
208    }
209
210    pub fn theme_layout(mut self, theme: ButtonLayoutThemePartial) -> Self {
211        self.theme_layout = Some(theme);
212        self
213    }
214
215    /// Shortcut for [Self::theme_layout] and [ButtonLayoutVariant::Compact].
216    pub fn compact(self) -> Self {
217        self.layout_variant(ButtonLayoutVariant::Compact)
218    }
219
220    /// Shortcut for [Self::theme_layout] and [ButtonLayoutVariant::Expanded].
221    pub fn expanded(self) -> Self {
222        self.layout_variant(ButtonLayoutVariant::Expanded)
223    }
224
225    /// Shortcut for [Self::style_variant] and [ButtonStyleVariant::Filled].
226    pub fn filled(self) -> Self {
227        self.style_variant(ButtonStyleVariant::Filled)
228    }
229
230    /// Shortcut for [Self::style_variant] and [ButtonStyleVariant::Outline].
231    pub fn outline(self) -> Self {
232        self.style_variant(ButtonStyleVariant::Outline)
233    }
234
235    /// Shortcut for [Self::style_variant] and [ButtonStyleVariant::Flat].
236    pub fn flat(self) -> Self {
237        self.style_variant(ButtonStyleVariant::Flat)
238    }
239
240    /// Override the cursor icon shown when hovering over the button while enabled.
241    pub fn cursor(mut self, cursor_icon: impl Into<Option<CursorIcon>>) -> Self {
242        self.cursor_icon = cursor_icon.into();
243        self
244    }
245}
246
247impl CornerRadiusExt for Button {
248    fn with_corner_radius(self, corner_radius: f32) -> Self {
249        self.corner_radius(corner_radius)
250    }
251}
252
253impl Component for Button {
254    fn render(&self) -> impl IntoElement {
255        let mut hovering = use_state(|| false);
256        let a11y_id = use_a11y();
257        let focus = use_focus(a11y_id);
258
259        let enabled = use_reactive(&self.enabled);
260
261        let theme_colors = match self.style_variant {
262            ButtonStyleVariant::Normal => {
263                get_theme!(&self.theme_colors, ButtonColorsThemePreference, "button")
264            }
265            ButtonStyleVariant::Outline => get_theme!(
266                &self.theme_colors,
267                ButtonColorsThemePreference,
268                "outline_button"
269            ),
270            ButtonStyleVariant::Filled => get_theme!(
271                &self.theme_colors,
272                ButtonColorsThemePreference,
273                "filled_button"
274            ),
275            ButtonStyleVariant::Flat => get_theme!(
276                &self.theme_colors,
277                ButtonColorsThemePreference,
278                "flat_button"
279            ),
280        };
281        let theme_layout = match self.layout_variant {
282            ButtonLayoutVariant::Normal => get_theme!(
283                &self.theme_layout,
284                ButtonLayoutThemePreference,
285                "button_layout"
286            ),
287            ButtonLayoutVariant::Compact => get_theme!(
288                &self.theme_layout,
289                ButtonLayoutThemePreference,
290                "compact_button_layout"
291            ),
292            ButtonLayoutVariant::Expanded => get_theme!(
293                &self.theme_layout,
294                ButtonLayoutThemePreference,
295                "expanded_button_layout"
296            ),
297        };
298
299        let border = if focus() == Focus::Keyboard {
300            Border::new()
301                .fill(theme_colors.focus_border_fill)
302                .width(2.)
303                .alignment(BorderAlignment::Inner)
304        } else {
305            Border::new()
306                .fill(theme_colors.border_fill.mul_if(!self.enabled, 0.9))
307                .width(1.)
308                .alignment(BorderAlignment::Inner)
309        };
310        let background = if enabled() && hovering() {
311            theme_colors.hover_background
312        } else {
313            theme_colors.background
314        };
315
316        rect()
317            .overflow(Overflow::Clip)
318            .a11y_id(a11y_id)
319            .a11y_enabled(self.enabled)
320            .a11y_focusable(self.enabled && self.focusable)
321            .a11y_role(AccessibilityRole::Button)
322            .background(background.mul_if(!self.enabled, 0.9))
323            .border(border)
324            .padding(theme_layout.padding)
325            .corner_radius(theme_layout.corner_radius)
326            .width(theme_layout.width)
327            .height(theme_layout.height)
328            .color(theme_colors.color.mul_if(!self.enabled, 0.9))
329            .center()
330            .maybe(self.enabled, |rect| {
331                rect.map(self.on_pointer_down.clone(), |rect, on_pointer_down| {
332                    rect.on_pointer_down(move |e: Event<PointerEventData>| {
333                        on_pointer_down.call(e);
334                    })
335                })
336                .map(self.on_secondary_down.clone(), |el, on_secondary_down| {
337                    el.on_secondary_down(move |e: Event<PressEventData>| {
338                        on_secondary_down.call(e);
339                    })
340                })
341                .on_all_press({
342                    let on_press = self.on_press.clone();
343                    move |e: Event<PressEventData>| {
344                        a11y_id.request_focus();
345                        match e.data() {
346                            PressEventData::Mouse(data) => {
347                                if data.button == Some(MouseButton::Left)
348                                    && let Some(handler) = &on_press
349                                {
350                                    handler.call(e);
351                                }
352                            }
353                            PressEventData::Touch(_)
354                            | PressEventData::Keyboard(_)
355                            | PressEventData::Accessibility(_) => {
356                                if let Some(handler) = &on_press {
357                                    handler.call(e);
358                                }
359                            }
360                        }
361                    }
362                })
363                .on_pointer_over(move |_| {
364                    hovering.set(true);
365                })
366            })
367            .on_pointer_out(move |_| hovering.set_if_modified(false))
368            .cursor(if self.enabled {
369                self.cursor_icon
370            } else {
371                CursorIcon::NotAllowed.into()
372            })
373            .children(self.elements.clone())
374    }
375
376    fn render_key(&self) -> DiffKey {
377        self.key.clone().or(self.default_key())
378    }
379}