1use jag_draw::{Brush, ColorLinPremul, Rect, RoundedRadii, RoundedRect};
4use jag_surface::Canvas;
5
6use crate::event::{
7 ElementState, EventHandler, EventResult, KeyCode, KeyboardEvent, MouseButton, MouseClickEvent,
8 MouseMoveEvent, ScrollEvent,
9};
10use crate::focus::FocusId;
11use crate::theme::Theme;
12
13use super::Element;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ButtonLabelAlign {
18 Start,
19 Center,
20 End,
21}
22
23pub struct Button {
25 pub rect: Rect,
26 pub label: String,
27 pub label_size: f32,
28 pub label_align: ButtonLabelAlign,
29 pub bg: ColorLinPremul,
30 pub fg: ColorLinPremul,
31 pub radius: f32,
32 pub focused: bool,
33 pub focus_visible: bool,
34 pub padding: [f32; 4],
36 pub icon_path: Option<String>,
38 pub icon_size: f32,
40 pub icon_spacing: f32,
42 pub icon_only: bool,
44 pub focus_id: FocusId,
46}
47
48impl Button {
49 pub fn new(label: impl Into<String>) -> Self {
51 Self {
52 rect: Rect {
53 x: 0.0,
54 y: 0.0,
55 w: 120.0,
56 h: 36.0,
57 },
58 label: label.into(),
59 label_size: 14.0,
60 label_align: ButtonLabelAlign::Center,
61 bg: ColorLinPremul::from_srgba_u8([59, 130, 246, 255]),
62 fg: ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
63 radius: 6.0,
64 focused: false,
65 focus_visible: false,
66 padding: [8.0, 16.0, 8.0, 16.0],
67 icon_path: None,
68 icon_size: 16.0,
69 icon_spacing: 6.0,
70 icon_only: false,
71 focus_id: FocusId(0),
72 }
73 }
74
75 pub fn with_theme(label: impl Into<String>, theme: &Theme) -> Self {
77 let mut btn = Self::new(label);
78 btn.bg = theme.colors.button_bg;
79 btn.fg = theme.colors.button_fg;
80 btn.radius = theme.border_radius;
81 btn.label_size = theme.font_size;
82 btn
83 }
84
85 pub fn hit_test(&self, x: f32, y: f32) -> bool {
87 x >= self.rect.x
88 && x <= self.rect.x + self.rect.w
89 && y >= self.rect.y
90 && y <= self.rect.y + self.rect.h
91 }
92}
93
94impl Element for Button {
99 fn rect(&self) -> Rect {
100 self.rect
101 }
102
103 fn set_rect(&mut self, rect: Rect) {
104 self.rect = rect;
105 }
106
107 fn render(&self, canvas: &mut Canvas, z: i32) {
108 let rrect = RoundedRect {
110 rect: self.rect,
111 radii: RoundedRadii {
112 tl: self.radius,
113 tr: self.radius,
114 br: self.radius,
115 bl: self.radius,
116 },
117 };
118 canvas.rounded_rect(rrect, Brush::Solid(self.bg), z);
119
120 let pad_top = self.padding[0];
122 let pad_right = self.padding[1];
123 let pad_bottom = self.padding[2];
124 let pad_left = self.padding[3];
125
126 let trimmed_label = if self.icon_only {
127 ""
128 } else {
129 self.label.trim()
130 };
131
132 let label_len = trimmed_label.chars().count() as f32;
133 let approx_text_width = if label_len == 0.0 {
134 0.0
135 } else {
136 canvas.measure_text_width(trimmed_label, self.label_size) + 2.0
137 };
138
139 let content_w = (self.rect.w - pad_left - pad_right).max(0.0);
140 let content_h = (self.rect.h - pad_top - pad_bottom).max(0.0);
141 let base_x = self.rect.x + pad_left;
142
143 let has_icon = self.icon_path.is_some() && self.icon_size > 0.0;
144 let icon_w = if has_icon { self.icon_size } else { 0.0 };
145 let icon_spacing = if has_icon {
146 self.icon_spacing.max(0.0)
147 } else {
148 0.0
149 };
150 let combined_width = icon_w + icon_spacing + approx_text_width;
151
152 let origin_x = match self.label_align {
153 ButtonLabelAlign::Center => base_x + (content_w - combined_width).max(0.0) * 0.5,
154 ButtonLabelAlign::End => base_x + (content_w - combined_width).max(0.0),
155 ButtonLabelAlign::Start => base_x,
156 };
157
158 let text_x = if has_icon {
159 origin_x + icon_w + icon_spacing
160 } else {
161 origin_x
162 };
163
164 let content_center_y = self.rect.y + pad_top + content_h * 0.5;
165 let text_y = content_center_y + self.label_size * 0.35;
166
167 if !self.icon_only {
171 canvas.draw_text_run_weighted(
172 [text_x, text_y],
173 trimmed_label.to_string(),
174 self.label_size,
175 400.0,
176 self.fg,
177 z + 2,
178 );
179 }
180
181 if self.focused && self.focus_visible {
183 let focus_color = ColorLinPremul::from_srgba_u8([59, 130, 246, 180]);
184 let offset = 3.0;
185 let focus_rect = Rect {
186 x: self.rect.x - offset,
187 y: self.rect.y - offset,
188 w: self.rect.w + offset * 2.0,
189 h: self.rect.h + offset * 2.0,
190 };
191 let focus_rrect = RoundedRect {
192 rect: focus_rect,
193 radii: RoundedRadii {
194 tl: self.radius + offset,
195 tr: self.radius + offset,
196 br: self.radius + offset,
197 bl: self.radius + offset,
198 },
199 };
200 jag_surface::shapes::draw_snapped_rounded_rectangle(
201 canvas,
202 focus_rrect,
203 None,
204 Some(2.0),
205 Some(Brush::Solid(focus_color)),
206 z + 3,
207 );
208 }
209 }
210
211 fn focus_id(&self) -> Option<FocusId> {
212 Some(self.focus_id)
213 }
214}
215
216impl EventHandler for Button {
221 fn handle_mouse_click(&mut self, event: &MouseClickEvent) -> EventResult {
222 if event.button != MouseButton::Left || event.state != ElementState::Pressed {
223 return EventResult::Ignored;
224 }
225 if self.contains_point(event.x, event.y) {
226 EventResult::Handled
227 } else {
228 EventResult::Ignored
229 }
230 }
231
232 fn handle_keyboard(&mut self, event: &KeyboardEvent) -> EventResult {
233 if event.state != ElementState::Pressed || !self.focused {
234 return EventResult::Ignored;
235 }
236 match event.key {
237 KeyCode::Space | KeyCode::Enter => EventResult::Handled,
238 _ => EventResult::Ignored,
239 }
240 }
241
242 fn handle_mouse_move(&mut self, _event: &MouseMoveEvent) -> EventResult {
243 EventResult::Ignored
244 }
245
246 fn handle_scroll(&mut self, _event: &ScrollEvent) -> EventResult {
247 EventResult::Ignored
248 }
249
250 fn is_focused(&self) -> bool {
251 self.focused
252 }
253
254 fn set_focused(&mut self, focused: bool) {
255 self.focused = focused;
256 }
257
258 fn contains_point(&self, x: f32, y: f32) -> bool {
259 self.hit_test(x, y)
260 }
261}
262
263#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn button_new_defaults() {
273 let btn = Button::new("Click me");
274 assert_eq!(btn.label, "Click me");
275 assert!(!btn.focused);
276 assert!(!btn.focus_visible);
277 assert!(!btn.icon_only);
278 assert!(btn.icon_path.is_none());
279 }
280
281 #[test]
282 fn button_hit_test() {
283 let mut btn = Button::new("Test");
284 btn.set_rect(Rect {
285 x: 10.0,
286 y: 10.0,
287 w: 100.0,
288 h: 40.0,
289 });
290 assert!(btn.contains_point(50.0, 30.0));
291 assert!(!btn.contains_point(0.0, 0.0));
292 assert!(btn.contains_point(10.0, 10.0)); assert!(btn.contains_point(110.0, 50.0)); assert!(!btn.contains_point(111.0, 30.0)); }
296
297 #[test]
298 fn button_focus() {
299 let mut btn = Button::new("Focus");
300 assert!(!btn.is_focused());
301 btn.set_focused(true);
302 assert!(btn.is_focused());
303 btn.set_focused(false);
304 assert!(!btn.is_focused());
305 }
306
307 #[test]
308 fn button_with_theme() {
309 let theme = Theme::dark();
310 let btn = Button::with_theme("Themed", &theme);
311 assert_eq!(btn.bg, theme.colors.button_bg);
312 assert_eq!(btn.fg, theme.colors.button_fg);
313 assert_eq!(btn.radius, theme.border_radius);
314 assert_eq!(btn.label_size, theme.font_size);
315 }
316
317 #[test]
318 fn button_element_trait() {
319 let mut btn = Button::new("Elem");
320 let r = Rect {
321 x: 5.0,
322 y: 5.0,
323 w: 80.0,
324 h: 30.0,
325 };
326 btn.set_rect(r);
327 assert_eq!(btn.rect().x, 5.0);
328 assert_eq!(btn.rect().w, 80.0);
329 assert!(btn.focus_id().is_some());
330 }
331
332 #[test]
333 fn button_keyboard_requires_focus() {
334 let mut btn = Button::new("KB");
335 btn.focused = false;
336 let evt = KeyboardEvent {
337 key: KeyCode::Enter,
338 state: ElementState::Pressed,
339 modifiers: Default::default(),
340 text: None,
341 };
342 assert_eq!(btn.handle_keyboard(&evt), EventResult::Ignored);
343
344 btn.focused = true;
345 assert_eq!(btn.handle_keyboard(&evt), EventResult::Handled);
346 }
347}