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.
23///
24/// The [WidgetId] is equal to `maycoon-widgets:Button`.
25pub struct Button {
26    child: BoxedWidget,
27    state: ButtonState,
28    on_pressed: MaybeSignal<Update>,
29    layout_style: MaybeSignal<LayoutStyle>,
30}
31
32impl Button {
33    /// Create a new button with the given child widget.
34    #[inline(always)]
35    pub fn new(child: impl Widget + 'static) -> Self {
36        Self {
37            child: Box::new(child),
38            state: ButtonState::Idle,
39            on_pressed: MaybeSignal::value(Update::empty()),
40            layout_style: LayoutStyle {
41                padding: layout::Rect::<LengthPercentage> {
42                    left: LengthPercentage::length(12.0),
43                    right: LengthPercentage::length(12.0),
44                    top: LengthPercentage::length(2.0),
45                    bottom: LengthPercentage::length(10.0),
46                },
47                ..Default::default()
48            }
49            .into(),
50        }
51    }
52
53    /// Sets the function to be called when the button is pressed.
54    #[inline(always)]
55    pub fn with_on_pressed(mut self, on_pressed: impl Into<MaybeSignal<Update>>) -> Self {
56        self.on_pressed = on_pressed.into();
57        self
58    }
59}
60
61impl WidgetChildExt for Button {
62    #[inline(always)]
63    fn set_child(&mut self, child: impl Widget + 'static) {
64        self.child = Box::new(child);
65    }
66}
67
68impl WidgetLayoutExt for Button {
69    #[inline(always)]
70    fn set_layout_style(&mut self, layout_style: impl Into<MaybeSignal<LayoutStyle>>) {
71        self.layout_style = layout_style.into();
72    }
73}
74
75impl Widget for Button {
76    fn render(
77        &mut self,
78        scene: &mut dyn Scene,
79        theme: &mut dyn Theme,
80        layout_node: &LayoutNode,
81        info: &AppInfo,
82        context: AppContext,
83    ) {
84        let brush = if let Some(style) = theme.of(self.widget_id()) {
85            match self.state {
86                ButtonState::Idle => Brush::Solid(style.get_color("color_idle").unwrap()),
87                ButtonState::Hovered => Brush::Solid(style.get_color("color_hovered").unwrap()),
88                ButtonState::Pressed => Brush::Solid(style.get_color("color_pressed").unwrap()),
89                ButtonState::Released => Brush::Solid(style.get_color("color_hovered").unwrap()),
90            }
91        } else {
92            Brush::Solid(match self.state {
93                ButtonState::Idle => theme.defaults().interactive().inactive(),
94                ButtonState::Hovered => theme.defaults().interactive().hover(),
95                ButtonState::Pressed => theme.defaults().interactive().active(),
96                ButtonState::Released => theme.defaults().interactive().hover(),
97            })
98        };
99
100        scene.draw_rounded_rect(
101            &brush,
102            None,
103            None,
104            &RoundedRect::from_rect(
105                Rect::new(
106                    layout_node.layout.location.x as f64,
107                    layout_node.layout.location.y as f64,
108                    (layout_node.layout.location.x + layout_node.layout.size.width) as f64,
109                    (layout_node.layout.location.y + layout_node.layout.size.height) as f64,
110                ),
111                RoundedRectRadii::from_single_radius(10.0),
112            ),
113        );
114
115        {
116            theme.globals_mut().invert_text_color = true;
117
118            let mut child_scene = scene.dyn_clone();
119            child_scene.reset();
120
121            self.child.render(
122                child_scene.as_mut(),
123                theme,
124                &layout_node.children[0],
125                info,
126                context,
127            );
128
129            scene.append(
130                child_scene.as_ref(),
131                Some(Affine::translate(Vec2::new(
132                    layout_node.layout.location.x as f64,
133                    layout_node.layout.location.y as f64,
134                ))),
135            );
136
137            theme.globals_mut().invert_text_color = false;
138        }
139    }
140
141    #[inline(always)]
142    fn layout_style(&self) -> StyleNode {
143        StyleNode {
144            style: self.layout_style.get().clone(),
145            children: vec![self.child.layout_style()],
146        }
147    }
148
149    fn update(&mut self, layout: &LayoutNode, _: AppContext, info: &AppInfo) -> Update {
150        let mut update = Update::empty();
151        let old_state = self.state;
152
153        // check for hovering
154        if let Some(cursor) = info.cursor_pos
155            && layout::intersects(cursor, &layout.layout)
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.get();
174                        },
175                    }
176                }
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    #[inline(always)]
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}