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}
144
145impl Default for Button {
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151impl ChildrenExt for Button {
152    fn get_children(&mut self) -> &mut Vec<Element> {
153        &mut self.elements
154    }
155}
156
157impl KeyExt for Button {
158    fn write_key(&mut self) -> &mut DiffKey {
159        &mut self.key
160    }
161}
162
163impl Button {
164    pub fn new() -> Self {
165        Self {
166            theme_colors: None,
167            theme_layout: None,
168            style_variant: ButtonStyleVariant::Normal,
169            layout_variant: ButtonLayoutVariant::Normal,
170            on_press: None,
171            on_secondary_down: None,
172            on_pointer_down: None,
173            elements: Vec::default(),
174            enabled: true,
175            focusable: true,
176            key: DiffKey::None,
177        }
178    }
179
180    pub fn get_layout_variant(&self) -> &ButtonLayoutVariant {
181        &self.layout_variant
182    }
183
184    pub fn get_theme_layout(&self) -> Option<&ButtonLayoutThemePartial> {
185        self.theme_layout.as_ref()
186    }
187
188    pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
189        self.enabled = enabled.into();
190        self
191    }
192
193    pub fn focusable(mut self, focusable: impl Into<bool>) -> Self {
194        self.focusable = focusable.into();
195        self
196    }
197
198    pub fn style_variant(mut self, style_variant: impl Into<ButtonStyleVariant>) -> Self {
199        self.style_variant = style_variant.into();
200        self
201    }
202
203    pub fn layout_variant(mut self, layout_variant: impl Into<ButtonLayoutVariant>) -> Self {
204        self.layout_variant = layout_variant.into();
205        self
206    }
207
208    pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
209        self.on_press = Some(on_press.into());
210        self
211    }
212
213    pub fn on_secondary_down(
214        mut self,
215        on_secondary_down: impl Into<EventHandler<Event<PressEventData>>>,
216    ) -> Self {
217        self.on_secondary_down = Some(on_secondary_down.into());
218        self
219    }
220
221    pub fn on_pointer_down(
222        mut self,
223        on_pointer_down: impl Into<EventHandler<Event<PointerEventData>>>,
224    ) -> Self {
225        self.on_pointer_down = Some(on_pointer_down.into());
226        self
227    }
228
229    pub fn theme_colors(mut self, theme: ButtonColorsThemePartial) -> Self {
230        self.theme_colors = Some(theme);
231        self
232    }
233
234    pub fn theme_layout(mut self, theme: ButtonLayoutThemePartial) -> Self {
235        self.theme_layout = Some(theme);
236        self
237    }
238
239    /// Shortcut for [Self::theme_layout] and [ButtonLayoutVariant::Compact].
240    pub fn compact(self) -> Self {
241        self.layout_variant(ButtonLayoutVariant::Compact)
242    }
243
244    /// Shortcut for [Self::theme_layout] and [ButtonLayoutVariant::Expanded].
245    pub fn expanded(self) -> Self {
246        self.layout_variant(ButtonLayoutVariant::Expanded)
247    }
248
249    /// Shortcut for [Self::style_variant] and [ButtonStyleVariant::Filled].
250    pub fn filled(self) -> Self {
251        self.style_variant(ButtonStyleVariant::Filled)
252    }
253
254    /// Shortcut for [Self::style_variant] and [ButtonStyleVariant::Outline].
255    pub fn outline(self) -> Self {
256        self.style_variant(ButtonStyleVariant::Outline)
257    }
258
259    /// Shortcut for [Self::style_variant] and [ButtonStyleVariant::Flat].
260    pub fn flat(self) -> Self {
261        self.style_variant(ButtonStyleVariant::Flat)
262    }
263}
264
265impl CornerRadiusExt for Button {
266    fn with_corner_radius(self, corner_radius: f32) -> Self {
267        self.corner_radius(corner_radius)
268    }
269}
270
271impl Component for Button {
272    fn render(&self) -> impl IntoElement {
273        let mut hovering = use_state(|| false);
274        let focus = use_focus();
275        let focus_status = use_focus_status(focus);
276
277        let enabled = use_reactive(&self.enabled);
278        use_drop(move || {
279            if hovering() {
280                Cursor::set(CursorIcon::default());
281            }
282        });
283
284        let theme_colors = match self.style_variant {
285            ButtonStyleVariant::Normal => {
286                get_theme!(&self.theme_colors, ButtonColorsThemePreference, "button")
287            }
288            ButtonStyleVariant::Outline => get_theme!(
289                &self.theme_colors,
290                ButtonColorsThemePreference,
291                "outline_button"
292            ),
293            ButtonStyleVariant::Filled => get_theme!(
294                &self.theme_colors,
295                ButtonColorsThemePreference,
296                "filled_button"
297            ),
298            ButtonStyleVariant::Flat => get_theme!(
299                &self.theme_colors,
300                ButtonColorsThemePreference,
301                "flat_button"
302            ),
303        };
304        let theme_layout = match self.layout_variant {
305            ButtonLayoutVariant::Normal => get_theme!(
306                &self.theme_layout,
307                ButtonLayoutThemePreference,
308                "button_layout"
309            ),
310            ButtonLayoutVariant::Compact => get_theme!(
311                &self.theme_layout,
312                ButtonLayoutThemePreference,
313                "compact_button_layout"
314            ),
315            ButtonLayoutVariant::Expanded => get_theme!(
316                &self.theme_layout,
317                ButtonLayoutThemePreference,
318                "expanded_button_layout"
319            ),
320        };
321
322        let border = if focus_status() == FocusStatus::Keyboard {
323            Border::new()
324                .fill(theme_colors.focus_border_fill)
325                .width(2.)
326                .alignment(BorderAlignment::Inner)
327        } else {
328            Border::new()
329                .fill(theme_colors.border_fill.mul_if(!self.enabled, 0.9))
330                .width(1.)
331                .alignment(BorderAlignment::Inner)
332        };
333        let background = if enabled() && hovering() {
334            theme_colors.hover_background
335        } else {
336            theme_colors.background
337        };
338
339        rect()
340            .overflow(Overflow::Clip)
341            .a11y_id(focus.a11y_id())
342            .a11y_focusable(self.enabled && self.focusable)
343            .a11y_role(AccessibilityRole::Button)
344            .background(background.mul_if(!self.enabled, 0.9))
345            .border(border)
346            .padding(theme_layout.padding)
347            .corner_radius(theme_layout.corner_radius)
348            .width(theme_layout.width)
349            .height(theme_layout.height)
350            .color(theme_colors.color.mul_if(!self.enabled, 0.9))
351            .center()
352            .maybe(self.enabled, |rect| {
353                rect.map(self.on_pointer_down.clone(), |rect, on_pointer_down| {
354                    rect.on_pointer_down(move |e: Event<PointerEventData>| {
355                        on_pointer_down.call(e);
356                    })
357                })
358                .on_all_press({
359                    let on_press = self.on_press.clone();
360                    let on_secondary_down = self.on_secondary_down.clone();
361                    move |e: Event<PressEventData>| {
362                        focus.request_focus();
363                        match e.data() {
364                            PressEventData::Mouse(data) => match data.button {
365                                Some(MouseButton::Left) => {
366                                    if let Some(handler) = &on_press {
367                                        handler.call(e);
368                                    }
369                                }
370                                Some(MouseButton::Right) => {
371                                    if let Some(handler) = &on_secondary_down {
372                                        handler.call(e);
373                                    }
374                                }
375                                _ => {}
376                            },
377                            PressEventData::Touch(_) | PressEventData::Keyboard(_) => {
378                                if let Some(handler) = &on_press {
379                                    handler.call(e);
380                                }
381                            }
382                        }
383                    }
384                })
385                .on_pointer_over(move |_| {
386                    hovering.set(true);
387                })
388                .on_pointer_out(move |_| hovering.set_if_modified(false))
389            })
390            .on_pointer_enter(move |_| {
391                if enabled() {
392                    Cursor::set(CursorIcon::Pointer);
393                } else {
394                    Cursor::set(CursorIcon::NotAllowed);
395                }
396            })
397            .on_pointer_leave(move |_| {
398                Cursor::set(CursorIcon::default());
399            })
400            .children(self.elements.clone())
401    }
402
403    fn render_key(&self) -> DiffKey {
404        self.key.clone().or(self.default_key())
405    }
406}