maycoon_widgets/
button.rs

1use maycoon_core::app::context::AppContext;
2use maycoon_core::app::info::AppInfo;
3use maycoon_core::app::update::Update;
4use maycoon_core::layout;
5use maycoon_core::layout::{LayoutNode, LayoutStyle, LengthPercentage, StyleNode};
6use maycoon_core::signal::MaybeSignal;
7use maycoon_core::vgi::kurbo::{Affine, Rect, RoundedRect, RoundedRectRadii, Vec2};
8use maycoon_core::vgi::{Brush, Scene};
9use maycoon_core::widget::{BoxedWidget, Widget, WidgetChildExt, WidgetLayoutExt};
10use maycoon_core::window::{ElementState, MouseButton};
11use maycoon_theme::id::WidgetId;
12use maycoon_theme::theme::Theme;
13
14/// An interactive area with a child widget that runs a closure when pressed.
15///
16/// See the [counter](https://github.com/maycoon-ui/maycoon/blob/master/examples/counter/src/main.rs) example for how to use it in practice.
17///
18/// ### Theming
19/// Styling the button require following properties:
20/// - `color_pressed` -  The color of the button when pressed.
21/// - `color_idle` - The color of the button when not pressed and not hovered (idling).
22/// - `color_hovered` - The color of the button when hovered on.
23pub struct Button {
24    child: BoxedWidget,
25    state: ButtonState,
26    on_pressed: MaybeSignal<Update>,
27    layout_style: MaybeSignal<LayoutStyle>,
28}
29
30impl Button {
31    /// Create a new button with the given child widget.
32    pub fn new(child: impl Widget + 'static) -> Self {
33        Self {
34            child: Box::new(child),
35            state: ButtonState::Idle,
36            on_pressed: MaybeSignal::value(Update::empty()),
37            layout_style: LayoutStyle {
38                padding: layout::Rect::<LengthPercentage> {
39                    left: LengthPercentage::length(12.0),
40                    right: LengthPercentage::length(12.0),
41                    top: LengthPercentage::length(2.0),
42                    bottom: LengthPercentage::length(10.0),
43                },
44                ..Default::default()
45            }
46            .into(),
47        }
48    }
49
50    /// Sets the function to be called when the button is pressed.
51    pub fn with_on_pressed(mut self, on_pressed: impl Into<MaybeSignal<Update>>) -> Self {
52        self.on_pressed = on_pressed.into();
53        self
54    }
55}
56
57impl WidgetChildExt for Button {
58    fn set_child(&mut self, child: impl Widget + 'static) {
59        self.child = Box::new(child);
60    }
61}
62
63impl WidgetLayoutExt for Button {
64    fn set_layout_style(&mut self, layout_style: impl Into<MaybeSignal<LayoutStyle>>) {
65        self.layout_style = layout_style.into();
66    }
67}
68
69impl Widget for Button {
70    fn render(
71        &mut self,
72        scene: &mut dyn Scene,
73        theme: &mut dyn Theme,
74        layout_node: &LayoutNode,
75        info: &AppInfo,
76        context: AppContext,
77    ) {
78        let brush = if let Some(style) = theme.of(self.widget_id()) {
79            match self.state {
80                ButtonState::Idle => Brush::Solid(style.get_color("color_idle").unwrap()),
81                ButtonState::Hovered => Brush::Solid(style.get_color("color_hovered").unwrap()),
82                ButtonState::Pressed => Brush::Solid(style.get_color("color_pressed").unwrap()),
83                ButtonState::Released => Brush::Solid(style.get_color("color_hovered").unwrap()),
84            }
85        } else {
86            Brush::Solid(match self.state {
87                ButtonState::Idle => theme.defaults().interactive().inactive(),
88                ButtonState::Hovered => theme.defaults().interactive().hover(),
89                ButtonState::Pressed => theme.defaults().interactive().active(),
90                ButtonState::Released => theme.defaults().interactive().hover(),
91            })
92        };
93
94        scene.draw_rounded_rect(
95            &brush,
96            None,
97            None,
98            &RoundedRect::from_rect(
99                Rect::new(
100                    layout_node.layout.location.x as f64,
101                    layout_node.layout.location.y as f64,
102                    (layout_node.layout.location.x + layout_node.layout.size.width) as f64,
103                    (layout_node.layout.location.y + layout_node.layout.size.height) as f64,
104                ),
105                RoundedRectRadii::from_single_radius(10.0),
106            ),
107        );
108
109        {
110            theme.globals_mut().invert_text_color = true;
111
112            let mut child_scene = scene.dyn_clone();
113            child_scene.reset();
114
115            self.child.render(
116                child_scene.as_mut(),
117                theme,
118                &layout_node.children[0],
119                info,
120                context,
121            );
122
123            scene.append(
124                child_scene.as_ref(),
125                Some(Affine::translate(Vec2::new(
126                    layout_node.layout.location.x as f64,
127                    layout_node.layout.location.y as f64,
128                ))),
129            );
130
131            theme.globals_mut().invert_text_color = false;
132        }
133    }
134
135    fn layout_style(&self) -> StyleNode {
136        StyleNode {
137            style: self.layout_style.get().clone(),
138            children: vec![self.child.layout_style()],
139        }
140    }
141
142    fn update(&mut self, layout: &LayoutNode, _: AppContext, info: &AppInfo) -> Update {
143        let mut update = Update::empty();
144        let old_state = self.state;
145
146        // check for hovering
147        if let Some(cursor) = info.cursor_pos {
148            if cursor.x as f32 >= layout.layout.location.x
149                && cursor.x as f32 <= layout.layout.location.x + layout.layout.size.width
150                && cursor.y as f32 >= layout.layout.location.y
151                && cursor.y as f32 <= layout.layout.location.y + layout.layout.size.height
152            {
153                // fixes state going to hover if the button is pressed but not yet released
154                if self.state != ButtonState::Pressed {
155                    self.state = ButtonState::Hovered;
156                }
157
158                // check for click
159                for (_, btn, el) in &info.buttons {
160                    if *btn == MouseButton::Left {
161                        match el {
162                            ElementState::Pressed => {
163                                self.state = ButtonState::Pressed;
164                            },
165
166                            // actually fire the event if the button is released
167                            ElementState::Released => {
168                                self.state = ButtonState::Released;
169                                update |= *self.on_pressed.get();
170                            },
171                        }
172                    }
173                }
174            } else {
175                // cursor not in area, so button is idle
176                self.state = ButtonState::Idle;
177            }
178        } else {
179            // cursor is not in window, so button is idle
180            self.state = ButtonState::Idle;
181        }
182
183        // update on state change, due to re-coloring
184        if old_state != self.state {
185            update |= Update::DRAW;
186        }
187
188        update
189    }
190
191    fn widget_id(&self) -> WidgetId {
192        WidgetId::new("maycoon-widgets", "Button")
193    }
194}
195
196/// The internal state of the button.
197#[derive(Copy, Clone, Eq, PartialEq, Debug)]
198pub enum ButtonState {
199    /// The button is idling (inactive).
200    Idle,
201    /// The cursor is hovering over the button.
202    Hovered,
203    /// The cursor is hovering over the button and the left click button is pressed.
204    Pressed,
205    /// The cursor is hovering over the button and the left click button is released.
206    /// This is when the `on_pressed` function is called.
207    Released,
208}