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::vg::kurbo::{Affine, Rect, RoundedRect, RoundedRectRadii, Vec2};
8use maycoon_core::vg::peniko::{Brush, Fill};
9use maycoon_core::vg::Scene;
10use maycoon_core::widget::{BoxedWidget, Widget, WidgetChildExt, WidgetLayoutExt};
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 {
25    child: BoxedWidget,
26    state: ButtonState,
27    on_pressed: MaybeSignal<Update>,
28    layout_style: MaybeSignal<LayoutStyle>,
29}
30
31impl Button {
32    /// Create a new button with the given child widget.
33    pub fn new(child: impl Widget + 'static) -> Self {
34        Self {
35            child: Box::new(child),
36            state: ButtonState::Idle,
37            on_pressed: MaybeSignal::value(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 Into<MaybeSignal<Update>>) -> Self {
53        self.on_pressed = on_pressed.into();
54        self
55    }
56}
57
58impl WidgetChildExt for Button {
59    fn set_child(&mut self, child: impl Widget + 'static) {
60        self.child = Box::new(child);
61    }
62}
63
64impl WidgetLayoutExt for Button {
65    fn set_layout_style(&mut self, layout_style: impl Into<MaybeSignal<LayoutStyle>>) {
66        self.layout_style = layout_style.into();
67    }
68}
69
70impl Widget for Button {
71    fn render(
72        &mut self,
73        scene: &mut Scene,
74        theme: &mut dyn Theme,
75        layout_node: &LayoutNode,
76        info: &AppInfo,
77        context: AppContext,
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.render(
117                &mut child_scene,
118                theme,
119                &layout_node.children[0],
120                info,
121                context,
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(&self) -> StyleNode {
137        StyleNode {
138            style: self.layout_style.get().clone(),
139            children: vec![self.child.layout_style()],
140        }
141    }
142
143    fn update(&mut self, layout: &LayoutNode, _: AppContext, info: &AppInfo) -> Update {
144        let mut update = Update::empty();
145        let old_state = self.state;
146
147        // check for hovering
148        if let Some(cursor) = info.cursor_pos {
149            if cursor.x as f32 >= layout.layout.location.x
150                && cursor.x as f32 <= layout.layout.location.x + layout.layout.size.width
151                && cursor.y as f32 >= layout.layout.location.y
152                && cursor.y as f32 <= layout.layout.location.y + layout.layout.size.height
153            {
154                // fixes state going to hover if the button is pressed but not yet released
155                if self.state != ButtonState::Pressed {
156                    self.state = ButtonState::Hovered;
157                }
158
159                // check for click
160                for (_, btn, el) in &info.buttons {
161                    if *btn == MouseButton::Left {
162                        match el {
163                            ElementState::Pressed => {
164                                self.state = ButtonState::Pressed;
165                            },
166
167                            // actually fire the event if the button is released
168                            ElementState::Released => {
169                                self.state = ButtonState::Released;
170                                update |= *self.on_pressed.get();
171                            },
172                        }
173                    }
174                }
175            } else {
176                // cursor not in area, so button is idle
177                self.state = ButtonState::Idle;
178            }
179        } else {
180            // cursor is not in window, so button is idle
181            self.state = ButtonState::Idle;
182        }
183
184        // update on state change, due to re-coloring
185        if old_state != self.state {
186            update |= Update::DRAW;
187        }
188
189        update
190    }
191
192    fn widget_id(&self) -> WidgetId {
193        WidgetId::new("maycoon-widgets", "Button")
194    }
195}
196
197/// The internal state of the button.
198#[derive(Copy, Clone, Eq, PartialEq, Debug)]
199pub enum ButtonState {
200    /// The button is idling (inactive).
201    Idle,
202    /// The cursor is hovering over the button.
203    Hovered,
204    /// The cursor is hovering over the button and the left click button is pressed.
205    Pressed,
206    /// The cursor is hovering over the button and the left click button is released.
207    /// This is when the `on_pressed` function is called.
208    Released,
209}