maycoon_widgets/
button.rs

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