yakui_widgets/widgets/
button.rs

1use std::borrow::Cow;
2
3use yakui_core::event::{EventInterest, EventResponse, WidgetEvent};
4use yakui_core::geometry::Color;
5use yakui_core::input::MouseButton;
6use yakui_core::widget::{EventContext, Widget};
7use yakui_core::{Alignment, Response};
8
9use crate::colors;
10use crate::style::{TextAlignment, TextStyle};
11use crate::util::widget;
12use crate::widgets::Pad;
13
14use super::{RenderText, RoundRect};
15
16/**
17A button containing some text.
18
19Responds with [ButtonResponse].
20
21Shorthand:
22```rust
23# let _handle = yakui_widgets::DocTest::start();
24if yakui::button("Hello").clicked {
25    println!("The button was clicked");
26}
27```
28*/
29#[derive(Debug)]
30#[non_exhaustive]
31#[must_use = "yakui widgets do nothing if you don't `show` them"]
32pub struct Button {
33    pub text: Cow<'static, str>,
34    pub padding: Pad,
35    pub border_radius: f32,
36    pub style: DynamicButtonStyle,
37    pub hover_style: DynamicButtonStyle,
38    pub down_style: DynamicButtonStyle,
39}
40
41/// Contains styles that can vary based on the state of the button.
42#[derive(Debug, Clone)]
43#[non_exhaustive]
44pub struct DynamicButtonStyle {
45    pub text: TextStyle,
46    pub fill: Color,
47}
48
49impl Default for DynamicButtonStyle {
50    fn default() -> Self {
51        let mut text = TextStyle::label();
52        text.align = TextAlignment::Center;
53
54        Self {
55            text,
56            fill: Color::GRAY,
57        }
58    }
59}
60
61impl Button {
62    pub fn unstyled(text: impl Into<Cow<'static, str>>) -> Self {
63        Self {
64            text: text.into(),
65            padding: Pad::ZERO,
66            border_radius: 0.0,
67            style: DynamicButtonStyle::default(),
68            hover_style: DynamicButtonStyle::default(),
69            down_style: DynamicButtonStyle::default(),
70        }
71    }
72
73    pub fn styled(text: impl Into<Cow<'static, str>>) -> Self {
74        let style = DynamicButtonStyle {
75            fill: colors::BACKGROUND_3,
76            ..Default::default()
77        };
78
79        let hover_style = DynamicButtonStyle {
80            fill: colors::BACKGROUND_3.adjust(1.2),
81            ..Default::default()
82        };
83
84        let down_style = DynamicButtonStyle {
85            fill: colors::BACKGROUND_3.adjust(0.8),
86            ..Default::default()
87        };
88
89        let mut text_style = TextStyle::label();
90        text_style.align = TextAlignment::Center;
91
92        Self {
93            text: text.into(),
94            padding: Pad::balanced(20.0, 10.0),
95            border_radius: 6.0,
96            style,
97            hover_style,
98            down_style,
99        }
100    }
101
102    pub fn show(self) -> Response<ButtonResponse> {
103        widget::<ButtonWidget>(self)
104    }
105}
106
107#[derive(Debug)]
108pub struct ButtonWidget {
109    props: Button,
110    hovering: bool,
111    mouse_down: bool,
112    clicked: bool,
113}
114
115#[derive(Debug)]
116pub struct ButtonResponse {
117    pub hovering: bool,
118    pub clicked: bool,
119}
120
121impl Widget for ButtonWidget {
122    type Props<'a> = Button;
123    type Response = ButtonResponse;
124
125    fn new() -> Self {
126        Self {
127            props: Button::unstyled(Cow::Borrowed("")),
128            hovering: false,
129            mouse_down: false,
130            clicked: false,
131        }
132    }
133
134    fn update(&mut self, props: Self::Props<'_>) -> Self::Response {
135        self.props = props;
136
137        let mut color = self.props.style.fill;
138        let mut text_style = self.props.style.text.clone();
139
140        if self.mouse_down {
141            let style = &self.props.down_style;
142            color = style.fill;
143            text_style = style.text.clone();
144        } else if self.hovering {
145            let style = &self.props.hover_style;
146            color = style.fill;
147            text_style = style.text.clone();
148        }
149
150        let alignment = match text_style.align {
151            TextAlignment::Start => Alignment::CENTER_LEFT,
152            TextAlignment::Center => Alignment::CENTER,
153            TextAlignment::End => Alignment::CENTER_RIGHT,
154        };
155
156        let mut container = RoundRect::new(self.props.border_radius);
157        container.color = color;
158        container.show_children(|| {
159            crate::pad(self.props.padding, || {
160                crate::align(alignment, || {
161                    let mut text = RenderText::label(self.props.text.clone());
162                    text.style = text_style;
163                    text.show();
164                });
165            });
166        });
167
168        let clicked = self.clicked;
169        self.clicked = false;
170
171        Self::Response {
172            hovering: self.hovering,
173            clicked,
174        }
175    }
176
177    fn event_interest(&self) -> EventInterest {
178        EventInterest::MOUSE_INSIDE | EventInterest::MOUSE_OUTSIDE
179    }
180
181    fn event(&mut self, _ctx: EventContext<'_>, event: &WidgetEvent) -> EventResponse {
182        match event {
183            WidgetEvent::MouseEnter => {
184                self.hovering = true;
185                EventResponse::Sink
186            }
187            WidgetEvent::MouseLeave => {
188                self.hovering = false;
189                EventResponse::Sink
190            }
191            WidgetEvent::MouseButtonChanged {
192                button: MouseButton::One,
193                down,
194                inside,
195                ..
196            } => {
197                if *inside {
198                    if *down {
199                        self.mouse_down = true;
200                        EventResponse::Sink
201                    } else if self.mouse_down {
202                        self.mouse_down = false;
203                        self.clicked = true;
204                        EventResponse::Sink
205                    } else {
206                        EventResponse::Bubble
207                    }
208                } else {
209                    if !*down {
210                        self.mouse_down = false;
211                    }
212
213                    EventResponse::Bubble
214                }
215            }
216            _ => EventResponse::Bubble,
217        }
218    }
219}