Skip to main content

minifb_ui/ui/
button.rs

1#[derive(Default)]
2pub struct Button {
3    pub label: Option<crate::ui::text::Text>,
4    pub label_size: f32,
5    pub text_alignment: Alignment,
6
7    /// The Button's horizontal position in pixels
8    pub pos_x: usize,
9    /// The Button's vertical position in pixels
10    pub pos_y: usize,
11
12    /// The Button's width in pixels
13    pub width: usize,
14    /// The Button's height in pixels
15    pub height: usize,
16
17    /// The Button's border size for when the Button is idle
18    pub border_size_idle: usize,
19    /// The Button's border size for when the Button is hovered
20    pub border_size_hovered: usize,
21    /// The Button's border size for when the Button is clicked
22    pub border_size_clicked: usize,
23
24    /// Button-wide shadow size
25    pub shadow_size_idle: usize,
26    /// Button-wide shadow size when hoverd
27    pub shadow_size_hovered: usize,
28    /// Button-wide shadow size when clicked
29    pub shadow_size_clicked: usize,
30    /// How intense the shadows should be
31    pub shadow_intensity_idle: u8,
32    /// How intense the shadows should be when hovered
33    pub shadow_intensity_hovered: u8,
34    /// How intense the shadows should be when clicked
35    pub shadow_intensity_clicked: u8,
36
37    /// The color of the Button's label
38    pub label_col_idle: crate::color::Color,
39    /// The color of the Button's border
40    pub border_col_idle: crate::color::Color,
41    /// The color of the Button's background
42    pub bg_col_idle: crate::color::Color,
43
44    /// The color of the Button's label when hovered
45    pub label_col_hovered: crate::color::Color,
46    /// The color of the Button's border when hovered
47    pub border_col_hovered: crate::color::Color,
48    /// The color of the Button's background when hovered
49    pub bg_col_hovered: crate::color::Color,
50
51    /// The color of the Button's label when clicked
52    pub label_col_clicked: crate::color::Color,
53    /// The color of the Button's border when clicked
54    pub border_col_clicked: crate::color::Color,
55    /// The color of the Button's background when clicked
56    pub bg_col_clicked: crate::color::Color,
57
58    /// Whether the Button is a push button or a toggle button
59    pub button_type: ButtonType,
60
61    /// The current state of the Button. Can be Idle, Hovered or Clicked
62    pub state: ButtonState,
63
64    /// Whether the toggle button is currently toggled on (only relevant for ButtonType::Toggle)
65    pub toggled: bool,
66
67    /// Corner radius for rounded edges
68    pub radius: usize,
69}
70
71impl Button {
72    /// Sets the text to be displayed inside the button
73    pub fn label(mut self, text: &str, font: crate::ttf::Font, size: f32) -> Self {
74        self.label = Some(crate::ui::text::Text::new(text, font));
75        self.label_size = size;
76        self
77    }
78
79    /// Determines whether the button label is left-aligned, right-aligned or centered
80    pub fn label_alignment(mut self, alignment: Alignment) -> Self {
81        self.text_alignment = alignment;
82        self
83    }
84
85    /// Sets the position of the button inside the window in pixel coordinates
86    pub fn position(mut self, x: usize, y: usize) -> Self {
87        self.pos_x = x;
88        self.pos_y = y;
89        self
90    }
91
92    /// Sets the size of the button in pixels
93    pub fn size(mut self, width: usize, height: usize) -> Self {
94        self.width = width;
95        self.height = height;
96        self
97    }
98
99    /// Sets the thickness of the Button's borders in pixels
100    pub fn border(mut self, size: usize) -> Self {
101        self.border_size_idle = size;
102        self.border_size_hovered = size;
103        self.border_size_clicked = size;
104        self
105    }
106
107    /// Determines whether to draw shadows and how large and intense they should be
108    /// Sets values for idle, hovered and clicked state
109    pub fn shadow(mut self, size: usize, intensity: u8) -> Self {
110        self.shadow_size_idle = size;
111        self.shadow_size_hovered = size;
112        self.shadow_size_clicked = size;
113        self.shadow_intensity_idle = intensity;
114        self.shadow_intensity_hovered = intensity;
115        self.shadow_intensity_clicked = intensity;
116        self
117    }
118
119    /// Specifically sets the idle shadow size and intensity
120    pub fn idle_shadow(mut self, size: usize, intensity: u8) -> Self {
121        self.shadow_size_idle = size;
122        self.shadow_intensity_idle = intensity;
123        self
124    }
125
126    /// Specifically sets the shadow size and intensity for when the Button is hovered
127    pub fn hover_shadow(mut self, size: usize, intensity: u8) -> Self {
128        self.shadow_size_hovered = size;
129        self.shadow_intensity_hovered = intensity;
130        self
131    }
132
133    /// Specifically sets the shadow size and intensity for when the Button is clicked
134    pub fn click_shadow(mut self, size: usize, intensity: u8) -> Self {
135        self.shadow_size_clicked = size;
136        self.shadow_intensity_clicked = intensity;
137        self
138    }
139
140    /// Sets the label text color for idle, hovered and clicked state
141    pub fn label_color(mut self, color: crate::color::Color) -> Self {
142        self.label_col_hovered = color.clone();
143        self.label_col_clicked = color.clone();
144        self.label_col_idle = color;
145        self
146    }
147
148    /// Specifically sets the label color for when the Button is idle
149    pub fn idle_label_col(mut self, color: crate::color::Color) -> Self {
150        self.label_col_idle = color;
151        self
152    }
153
154    /// Specifically sets the label color for when the Button is hovered
155    pub fn hover_label_col(mut self, color: crate::color::Color) -> Self {
156        self.label_col_hovered = color;
157        self
158    }
159
160    /// Specifically sets the label color for when the Button is clicked
161    pub fn click_label_col(mut self, color: crate::color::Color) -> Self {
162        self.label_col_clicked = color;
163        self
164    }
165
166    /// Sets the border color for idle, hovered and clicked state
167    pub fn border_color(mut self, color: crate::color::Color) -> Self {
168        self.border_col_hovered = color.clone();
169        self.border_col_clicked = color.clone();
170        self.border_col_idle = color;
171        self
172    }
173
174    /// Sets the button's background color for idle, hovered and clicked state
175    pub fn background(mut self, color: crate::color::Color) -> Self {
176        self.bg_col_hovered = color.clone();
177        self.bg_col_clicked = color.clone();
178        self.bg_col_idle = color;
179        self
180    }
181
182    /// Specifically sets the background color for when the Button is idle
183    pub fn idle_bg(mut self, color: crate::color::Color) -> Self {
184        self.bg_col_idle = color;
185        self
186    }
187
188    /// Specifically sets the background color for when the Button is hovered
189    pub fn hover_bg(mut self, color: crate::color::Color) -> Self {
190        self.bg_col_hovered = color;
191        self
192    }
193
194    /// Specifically sets the background color for when the Button is clicked
195    pub fn click_bg(mut self, color: crate::color::Color) -> Self {
196        self.bg_col_clicked = color;
197        self
198    }
199
200    /// Determines whether the button is a push button or toggle button
201    pub fn button_type(mut self, button_type: ButtonType) -> Self {
202        self.button_type = button_type;
203        self
204    }
205
206    /// Sets the corner radius for rounded edges
207    pub fn radius(mut self, radius: usize) -> Self {
208        self.radius = radius;
209        self
210    }
211
212    fn draw_shadow(&self, window: &mut crate::window::Window) {
213        let shadow_depth;
214        let shadow_size;
215        let border_size;
216
217        match self.state {
218            ButtonState::Idle => {
219                shadow_depth = self.shadow_intensity_idle;
220                shadow_size = self.shadow_size_idle;
221                border_size = self.border_size_idle;
222            },
223            ButtonState::Hovered => {
224                shadow_depth = self.shadow_intensity_hovered;
225                shadow_size = self.shadow_size_hovered;
226                border_size = self.border_size_hovered;
227            },
228            ButtonState::Clicked => {
229                shadow_depth = self.shadow_intensity_clicked;
230                shadow_size = self.shadow_size_clicked;
231                border_size = self.border_size_clicked;
232            }
233        }
234
235        // Draw inset shadow as concentric rounded-rect rings
236        let inner_x = self.pos_x + border_size;
237        let inner_y = self.pos_y + border_size;
238        let inner_w = self.width.saturating_sub(border_size * 2);
239        let inner_h = self.height.saturating_sub(border_size * 2);
240        let inner_r = self.radius.saturating_sub(border_size) as f32;
241
242        let cx = inner_x as f32 + inner_w as f32 / 2.0;
243        let cy = inner_y as f32 + inner_h as f32 / 2.0;
244        let hw = inner_w as f32 / 2.0;
245        let hh = inner_h as f32 / 2.0;
246
247        for py in inner_y..inner_y + inner_h {
248            for px in inner_x..inner_x + inner_w {
249                // Signed distance from the inner edge (negative = inside)
250                let pfx = px as f32 + 0.5;
251                let pfy = py as f32 + 0.5;
252                let dx = (pfx - cx).abs() - (hw - inner_r);
253                let dy = (pfy - cy).abs() - (hh - inner_r);
254                let outside = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
255                let inside = dx.max(dy).min(0.0);
256                let dist = outside + inside - inner_r; // negative inside
257
258                // dist is negative inside; the edge is at dist=0
259                // Shadow starts at the edge and fades inward
260                let depth = -dist; // positive near edge, grows toward center
261                if depth > 0.0 && depth <= shadow_size as f32 {
262                    let t = 1.0 - (depth / shadow_size as f32);
263                    let blend = (shadow_depth as f32 * t) as i32;
264                    if blend > 0 {
265                        darken_pixel(window, px, py, blend);
266                    }
267                }
268            }
269        }
270    }
271
272    /// Internal helper
273    fn draw_button(&self, window: &mut crate::window::Window) {
274        let bg_col: &crate::color::Color;
275        let border_col: &crate::color::Color;
276        let label_col: &crate::color::Color;
277        let border_size;
278
279        match self.state {
280            ButtonState::Idle => {
281                bg_col = &self.bg_col_idle;
282                border_col = &self.border_col_idle;
283                label_col = &self.label_col_idle;
284                border_size = self.border_size_idle;
285            }
286            ButtonState::Hovered =>{
287                bg_col = &self.bg_col_hovered;
288                border_col = &self.border_col_hovered;
289                label_col = &self.label_col_hovered;
290                border_size = self.border_size_hovered;
291            },
292            ButtonState::Clicked => {
293                bg_col = &self.bg_col_clicked;
294                border_col = &self.border_col_clicked;
295                label_col = &self.label_col_clicked;
296                border_size = self.border_size_clicked;
297            },
298        }
299
300        window.draw_rect_f(
301            self.pos_x,
302            self.pos_y,
303            self.width,
304            self.height,
305            self.radius,
306            bg_col,
307            0,
308        );
309
310        for i in 0..border_size {
311            window.draw_rect(
312                self.pos_x + i,
313                self.pos_y + i,
314                self.width - i * 2,
315                self.height - i * 2,
316                self.radius.saturating_sub(i),
317                border_col,
318            );
319        }
320
321        if let Some(label) = &self.label {
322            let lm = label.font.font.horizontal_line_metrics(self.label_size).unwrap();
323
324            let y_pos = (self.pos_y as f32 + (self.height as f32 / 2.0) - (lm.ascent / 2.0)
325                + (lm.descent / 3.0))
326                .max(0.0) as usize;
327
328            let text_width = label.get_width_precise(self.label_size);
329
330            match self.text_alignment {
331                Alignment::Left => {
332                    window.draw_text(
333                        self.pos_x + border_size + 4,
334                        y_pos,
335                        &label,
336                        self.label_size,
337                        label_col,
338                    );
339                }
340                Alignment::Right => {
341                    let x = (self.pos_x + self.width) as f32 - text_width - 4.0;
342                    window.draw_text(
343                        x.max(0.0) as usize,
344                        y_pos,
345                        &label,
346                        self.label_size,
347                        label_col,
348                    );
349                }
350                Alignment::Center => {
351                    let x = self.pos_x as f32 + (self.width as f32 / 2.0) - (text_width / 2.0);
352                    window.draw_text(
353                        x.max(0.0) as usize,
354                        y_pos,
355                        &label,
356                        self.label_size,
357                        label_col,
358                    );
359                }
360            }
361        }
362
363    }
364
365    pub fn is_hovered(&self, window: &crate::window::Window) -> bool {
366        let state = window.get_mouse_state();
367        (state.pos_x as usize) > self.pos_x
368            && (state.pos_y as usize) > self.pos_y
369            && (state.pos_x as usize) < self.pos_x + self.width
370            && (state.pos_y as usize) < self.pos_y + self.height
371    }
372
373    pub fn is_left_clicked(&self, window: &crate::window::Window) -> bool {
374        let state = window.get_mouse_state();
375        self.is_hovered(window) && state.lmb_clicked
376    }
377
378    pub fn is_right_clicked(&self, window: &crate::window::Window) -> bool {
379        let state = window.get_mouse_state();
380        self.is_hovered(window) && state.rmb_clicked
381    }
382
383    fn update(&mut self, window: &mut crate::window::Window) {
384        let mouse = window.get_mouse_state();
385        let hovered = (mouse.pos_x as usize) > self.pos_x
386            && (mouse.pos_y as usize) > self.pos_y
387            && (mouse.pos_x as usize) < self.pos_x + self.width
388            && (mouse.pos_y as usize) < self.pos_y + self.height;
389        let clicked = hovered && (mouse.lmb_clicked || mouse.rmb_clicked);
390
391        match self.button_type {
392            ButtonType::Push => {
393                self.state = if clicked {
394                    ButtonState::Clicked
395                } else if hovered {
396                    ButtonState::Hovered
397                } else {
398                    ButtonState::Idle
399                };
400            }
401            ButtonType::Toggle => {
402                if clicked && !self.toggled {
403                    self.toggled = true;
404                } else if clicked && self.toggled {
405                    self.toggled = false;
406                }
407                self.state = if self.toggled {
408                    ButtonState::Clicked
409                } else if hovered {
410                    ButtonState::Hovered
411                } else {
412                    ButtonState::Idle
413                };
414            }
415        }
416    }
417
418    /// Draws the button to a window
419    pub fn draw(&mut self, window: &mut crate::window::Window) {
420        self.update(window);
421        self.draw_button(window);
422        self.draw_shadow(window);
423    }
424}
425
426#[derive(Default)]
427pub enum ButtonType {
428    #[default]
429    Push,
430    Toggle,
431}
432
433#[derive(Default)]
434pub enum Alignment {
435    Left,
436    Right,
437    #[default]
438    Center,
439}
440
441#[derive(Default)]
442pub enum ButtonState {
443    #[default]
444    Idle,
445    Hovered,
446    Clicked,
447}
448
449fn darken_pixel(window: &mut crate::window::Window, x: usize, y: usize, blend: i32) {
450    let existing = window.get_pixel(x, y);
451    let er = ((existing >> 16) & 0xFF) as i32;
452    let eg = ((existing >> 8) & 0xFF) as i32;
453    let eb = ((existing) & 0xFF) as i32;
454
455    let r = (er - blend).clamp(0, 255) as u32;
456    let g = (eg - blend).clamp(0, 255) as u32;
457    let b = (eb - blend).clamp(0, 255) as u32;
458
459    window.draw_pixel(
460        x,
461        y,
462        &crate::color::Color::from(0xFF000000 | (r << 16) | (g << 8) | b),
463    );
464}