Skip to main content

bexa_ui_core/widgets/
button.rs

1use glyphon::Metrics;
2use glyphon::cosmic_text::Align;
3use taffy::prelude::*;
4use winit::event::{ElementState, MouseButton, WindowEvent};
5
6use crate::framework::{DrawContext, EventContext, Widget};
7
8pub struct Button {
9    label: String,
10    metrics: Metrics,
11    padding: f32,
12    border_radius: f32,
13    bg_color: [f32; 3],
14    hover_color: [f32; 3],
15    active_color: [f32; 3],
16    focus_color: [f32; 3],
17    text_color: [u8; 3],
18    hover_text_color: [u8; 3],
19    active_text_color: [u8; 3],
20    hover: bool,
21    active: bool,
22    focus: bool,
23    on_click: Option<Box<dyn FnMut()>>,
24}
25
26impl Button {
27    pub fn new(label: impl Into<String>, metrics: Metrics) -> Self {
28        Self {
29            label: label.into(),
30            metrics,
31            padding: 16.0,
32            border_radius: 0.0,
33            bg_color: [0.20, 0.65, 0.85],
34            hover_color: [0.35, 0.75, 0.92],
35            active_color: [0.15, 0.55, 0.75],
36            focus_color: [0.18, 0.60, 0.82],
37            text_color: [25, 25, 25],
38            hover_text_color: [15, 15, 15],
39            active_text_color: [250, 250, 250],
40            hover: false,
41            active: false,
42            focus: false,
43            on_click: None,
44        }
45    }
46
47    pub fn with_colors(
48        mut self,
49        bg: [f32; 3],
50        hover: [f32; 3],
51        active: [f32; 3],
52        focus: [f32; 3],
53    ) -> Self {
54        self.bg_color = bg;
55        self.hover_color = hover;
56        self.active_color = active;
57        self.focus_color = focus;
58        self
59    }
60
61    pub fn with_text_colors(mut self, normal: [u8; 3], hover: [u8; 3], active: [u8; 3]) -> Self {
62        self.text_color = normal;
63        self.hover_text_color = hover;
64        self.active_text_color = active;
65        self
66    }
67
68    pub fn with_padding(mut self, padding: f32) -> Self {
69        self.padding = padding;
70        self
71    }
72
73    pub fn with_border_radius(mut self, radius: f32) -> Self {
74        self.border_radius = radius;
75        self
76    }
77
78    pub fn set_on_click(&mut self, handler: impl FnMut() + 'static) {
79        self.on_click = Some(Box::new(handler));
80    }
81
82    fn current_color(&self) -> [f32; 3] {
83        if self.active {
84            self.active_color
85        } else if self.hover {
86            self.hover_color
87        } else if self.focus {
88            self.focus_color
89        } else {
90            self.bg_color
91        }
92    }
93
94    fn current_text_color(&self) -> [u8; 3] {
95        if self.active {
96            self.active_text_color
97        } else if self.hover {
98            self.hover_text_color
99        } else {
100            self.text_color
101        }
102    }
103
104    fn hit_test(&self, layout: &Layout, x: f32, y: f32) -> bool {
105        x >= layout.location.x
106            && x <= layout.location.x + layout.size.width
107            && y >= layout.location.y
108            && y <= layout.location.y + layout.size.height
109    }
110
111    fn click(&mut self) {
112        if let Some(handler) = self.on_click.as_mut() {
113            handler();
114        }
115    }
116}
117
118impl Widget for Button {
119    fn style(&self) -> Style {
120        Style {
121            flex_grow: 1.0,
122            ..Default::default()
123        }
124    }
125
126    fn draw(&self, ctx: &mut DrawContext) {
127        let layout = ctx.layout;
128        let color = self.current_color();
129        ctx.renderer.fill_rect_rounded(
130            (
131                layout.location.x,
132                layout.location.y,
133                layout.size.width,
134                layout.size.height,
135            ),
136            [color[0], color[1], color[2], 1.0],
137            self.border_radius,
138        );
139
140        let inner_width = (layout.size.width - self.padding * 2.0).max(0.0);
141        let inner_height = (layout.size.height - self.padding * 2.0).max(0.0);
142        let baseline_nudge = 2.0;
143        let vertical_offset = ((inner_height - self.metrics.font_size).max(0.0)) * 0.5 + baseline_nudge;
144        let text_left = layout.location.x + self.padding;
145        let text_top = layout.location.y + self.padding + vertical_offset;
146
147        ctx.renderer.draw_text(
148            &self.label,
149            (text_left, text_top),
150            self.current_text_color(),
151            (inner_width, inner_height),
152            self.metrics,
153            Align::Center,
154        );
155    }
156
157    fn handle_event(&mut self, ctx: &mut EventContext) -> bool {
158        let layout = ctx.layout;
159        let mut changed = false;
160        match ctx.event {
161            WindowEvent::CursorMoved { position, .. } => {
162                let over = self.hit_test(layout, position.x as f32, position.y as f32);
163                if over != self.hover {
164                    self.hover = over;
165                    changed = true;
166                }
167            }
168            WindowEvent::MouseInput {
169                state: ElementState::Pressed,
170                button: MouseButton::Left,
171                ..
172            } => {
173                if self.hover {
174                    self.active = !self.active;
175                    self.click();
176                    changed = true;
177                }
178            }
179            _ => {}
180        }
181
182        changed
183    }
184
185    fn is_focusable(&self) -> bool {
186        true
187    }
188
189    fn set_focus(&mut self, focused: bool) {
190        self.focus = focused;
191    }
192
193    fn activate(&mut self) {
194        self.active = !self.active;
195        self.click();
196    }
197
198    fn clear_active(&mut self) {
199        self.active = false;
200    }
201}