1use presentar_core::{
4 widget::{AccessibleRole, FontWeight, LayoutResult, TextStyle},
5 Canvas, Color, Constraints, CornerRadius, Event, MouseButton, Point, Rect, Size, TypeId,
6 Widget,
7};
8use serde::{Deserialize, Serialize};
9use std::any::Any;
10
11#[derive(Clone, Serialize, Deserialize)]
13pub struct Button {
14 label: String,
16 background: Color,
18 background_hover: Color,
20 background_pressed: Color,
22 text_color: Color,
24 corner_radius: CornerRadius,
26 padding: f32,
28 font_size: f32,
30 disabled: bool,
32 test_id_value: Option<String>,
34 accessible_name: Option<String>,
36 #[serde(skip)]
38 hovered: bool,
39 #[serde(skip)]
41 pressed: bool,
42 #[serde(skip)]
44 bounds: Rect,
45}
46
47#[derive(Debug, Clone)]
49pub struct ButtonClicked;
50
51impl Button {
52 #[must_use]
54 pub fn new(label: impl Into<String>) -> Self {
55 Self {
56 label: label.into(),
57 background: Color::from_hex("#6366f1").unwrap_or(Color::BLACK),
58 background_hover: Color::from_hex("#4f46e5").unwrap_or(Color::BLACK),
59 background_pressed: Color::from_hex("#4338ca").unwrap_or(Color::BLACK),
60 text_color: Color::WHITE,
61 corner_radius: CornerRadius::uniform(4.0),
62 padding: 12.0,
63 font_size: 14.0,
64 disabled: false,
65 test_id_value: None,
66 accessible_name: None,
67 hovered: false,
68 pressed: false,
69 bounds: Rect::default(),
70 }
71 }
72
73 #[must_use]
75 pub const fn background(mut self, color: Color) -> Self {
76 self.background = color;
77 self
78 }
79
80 #[must_use]
82 pub const fn background_hover(mut self, color: Color) -> Self {
83 self.background_hover = color;
84 self
85 }
86
87 #[must_use]
89 pub const fn background_pressed(mut self, color: Color) -> Self {
90 self.background_pressed = color;
91 self
92 }
93
94 #[must_use]
96 pub const fn text_color(mut self, color: Color) -> Self {
97 self.text_color = color;
98 self
99 }
100
101 #[must_use]
103 pub const fn corner_radius(mut self, radius: CornerRadius) -> Self {
104 self.corner_radius = radius;
105 self
106 }
107
108 #[must_use]
110 pub const fn padding(mut self, padding: f32) -> Self {
111 self.padding = padding;
112 self
113 }
114
115 #[must_use]
117 pub const fn font_size(mut self, size: f32) -> Self {
118 self.font_size = size;
119 self
120 }
121
122 #[must_use]
124 pub const fn disabled(mut self, disabled: bool) -> Self {
125 self.disabled = disabled;
126 self
127 }
128
129 #[must_use]
131 pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
132 self.test_id_value = Some(id.into());
133 self
134 }
135
136 #[must_use]
138 pub fn with_accessible_name(mut self, name: impl Into<String>) -> Self {
139 self.accessible_name = Some(name.into());
140 self
141 }
142
143 fn current_background(&self) -> Color {
145 if self.disabled {
146 let gray = (self.background.r + self.background.g + self.background.b) / 3.0;
148 Color::rgb(gray, gray, gray)
149 } else if self.pressed {
150 self.background_pressed
151 } else if self.hovered {
152 self.background_hover
153 } else {
154 self.background
155 }
156 }
157
158 fn estimate_text_size(&self) -> Size {
160 let char_width = self.font_size * 0.6;
161 let width = self.label.len() as f32 * char_width;
162 let height = self.font_size * 1.2;
163 Size::new(width, height)
164 }
165}
166
167impl Widget for Button {
168 fn type_id(&self) -> TypeId {
169 TypeId::of::<Self>()
170 }
171
172 fn measure(&self, constraints: Constraints) -> Size {
173 let text_size = self.estimate_text_size();
174 let size = Size::new(
175 self.padding.mul_add(2.0, text_size.width),
176 self.padding.mul_add(2.0, text_size.height),
177 );
178 constraints.constrain(size)
179 }
180
181 fn layout(&mut self, bounds: Rect) -> LayoutResult {
182 self.bounds = bounds;
183 LayoutResult {
184 size: bounds.size(),
185 }
186 }
187
188 fn paint(&self, canvas: &mut dyn Canvas) {
189 canvas.fill_rect(self.bounds, self.current_background());
191
192 let text_size = self.estimate_text_size();
194 let text_pos = Point::new(
195 self.bounds.x + (self.bounds.width - text_size.width) / 2.0,
196 self.bounds.y + (self.bounds.height - text_size.height) / 2.0,
197 );
198
199 let style = TextStyle {
200 size: self.font_size,
201 color: if self.disabled {
202 Color::rgb(0.7, 0.7, 0.7)
203 } else {
204 self.text_color
205 },
206 weight: FontWeight::Medium,
207 ..Default::default()
208 };
209
210 canvas.draw_text(&self.label, text_pos, &style);
211 }
212
213 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
214 if self.disabled {
215 return None;
216 }
217
218 match event {
219 Event::MouseEnter => {
220 self.hovered = true;
221 None
222 }
223 Event::MouseLeave => {
224 self.hovered = false;
225 self.pressed = false;
226 None
227 }
228 Event::MouseDown {
229 position,
230 button: MouseButton::Left,
231 } => {
232 if self.bounds.contains_point(position) {
233 self.pressed = true;
234 }
235 None
236 }
237 Event::MouseUp {
238 position,
239 button: MouseButton::Left,
240 } => {
241 let was_pressed = self.pressed;
242 self.pressed = false;
243
244 if was_pressed && self.bounds.contains_point(position) {
245 Some(Box::new(ButtonClicked))
246 } else {
247 None
248 }
249 }
250 Event::KeyDown {
251 key: presentar_core::Key::Enter | presentar_core::Key::Space,
252 } => {
253 self.pressed = true;
254 None
255 }
256 Event::KeyUp {
257 key: presentar_core::Key::Enter | presentar_core::Key::Space,
258 } => {
259 self.pressed = false;
260 Some(Box::new(ButtonClicked))
261 }
262 _ => None,
263 }
264 }
265
266 fn children(&self) -> &[Box<dyn Widget>] {
267 &[]
268 }
269
270 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
271 &mut []
272 }
273
274 fn is_interactive(&self) -> bool {
275 !self.disabled
276 }
277
278 fn is_focusable(&self) -> bool {
279 !self.disabled
280 }
281
282 fn accessible_name(&self) -> Option<&str> {
283 self.accessible_name.as_deref().or(Some(&self.label))
284 }
285
286 fn accessible_role(&self) -> AccessibleRole {
287 AccessibleRole::Button
288 }
289
290 fn test_id(&self) -> Option<&str> {
291 self.test_id_value.as_deref()
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use presentar_core::draw::DrawCommand;
299 use presentar_core::{RecordingCanvas, Widget};
300
301 #[test]
302 fn test_button_new() {
303 let b = Button::new("Click me");
304 assert_eq!(b.label, "Click me");
305 assert!(!b.disabled);
306 }
307
308 #[test]
309 fn test_button_builder() {
310 let b = Button::new("Test")
311 .padding(20.0)
312 .font_size(18.0)
313 .disabled(true)
314 .with_test_id("my-button");
315
316 assert_eq!(b.padding, 20.0);
317 assert_eq!(b.font_size, 18.0);
318 assert!(b.disabled);
319 assert_eq!(Widget::test_id(&b), Some("my-button"));
320 }
321
322 #[test]
323 fn test_button_accessible() {
324 let b = Button::new("OK");
325 assert_eq!(Widget::accessible_name(&b), Some("OK"));
326 assert_eq!(Widget::accessible_role(&b), AccessibleRole::Button);
327 assert!(Widget::is_focusable(&b));
328 }
329
330 #[test]
331 fn test_button_disabled_not_focusable() {
332 let b = Button::new("OK").disabled(true);
333 assert!(!Widget::is_focusable(&b));
334 assert!(!Widget::is_interactive(&b));
335 }
336
337 #[test]
338 fn test_button_measure() {
339 let b = Button::new("Test");
340 let size = b.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
341 assert!(size.width > 0.0);
342 assert!(size.height > 0.0);
343 }
344
345 #[test]
348 fn test_button_paint_draws_background() {
349 let mut button = Button::new("Click");
350 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
351
352 let mut canvas = RecordingCanvas::new();
353 button.paint(&mut canvas);
354
355 assert!(canvas.command_count() >= 2);
357
358 match &canvas.commands()[0] {
360 DrawCommand::Rect { bounds, style, .. } => {
361 assert_eq!(bounds.width, 100.0);
362 assert_eq!(bounds.height, 40.0);
363 assert!(style.fill.is_some());
364 }
365 _ => panic!("Expected Rect command for background"),
366 }
367 }
368
369 #[test]
370 fn test_button_paint_draws_text() {
371 let mut button = Button::new("Hello");
372 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
373
374 let mut canvas = RecordingCanvas::new();
375 button.paint(&mut canvas);
376
377 let has_text = canvas
379 .commands()
380 .iter()
381 .any(|cmd| matches!(cmd, DrawCommand::Text { content, .. } if content == "Hello"));
382 assert!(has_text, "Should draw button label text");
383 }
384
385 #[test]
386 fn test_button_paint_disabled_uses_gray() {
387 let mut button = Button::new("Disabled").disabled(true);
388 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
389
390 let mut canvas = RecordingCanvas::new();
391 button.paint(&mut canvas);
392
393 let text_cmd = canvas
395 .commands()
396 .iter()
397 .find(|cmd| matches!(cmd, DrawCommand::Text { .. }));
398
399 if let Some(DrawCommand::Text { style, .. }) = text_cmd {
400 assert!(style.color.r > 0.5 && style.color.g > 0.5 && style.color.b > 0.5);
402 } else {
403 panic!("Expected Text command");
404 }
405 }
406
407 #[test]
408 fn test_button_paint_hovered_uses_hover_color() {
409 let mut button = Button::new("Hover")
410 .background(Color::RED)
411 .background_hover(Color::BLUE);
412 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
413
414 button.event(&Event::MouseEnter);
416
417 let mut canvas = RecordingCanvas::new();
418 button.paint(&mut canvas);
419
420 match &canvas.commands()[0] {
422 DrawCommand::Rect { style, .. } => {
423 assert_eq!(style.fill, Some(Color::BLUE));
424 }
425 _ => panic!("Expected Rect command"),
426 }
427 }
428
429 #[test]
430 fn test_button_paint_pressed_uses_pressed_color() {
431 let mut button = Button::new("Press")
432 .background(Color::RED)
433 .background_pressed(Color::GREEN);
434 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
435
436 button.event(&Event::MouseEnter);
438 button.event(&Event::MouseDown {
439 position: Point::new(50.0, 20.0),
440 button: MouseButton::Left,
441 });
442
443 let mut canvas = RecordingCanvas::new();
444 button.paint(&mut canvas);
445
446 match &canvas.commands()[0] {
448 DrawCommand::Rect { style, .. } => {
449 assert_eq!(style.fill, Some(Color::GREEN));
450 }
451 _ => panic!("Expected Rect command"),
452 }
453 }
454
455 #[test]
456 fn test_button_paint_text_centered() {
457 let mut button = Button::new("X");
458 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
459
460 let mut canvas = RecordingCanvas::new();
461 button.paint(&mut canvas);
462
463 let text_cmd = canvas
465 .commands()
466 .iter()
467 .find(|cmd| matches!(cmd, DrawCommand::Text { .. }));
468
469 if let Some(DrawCommand::Text { position, .. }) = text_cmd {
470 assert!(position.x > 10.0 && position.x < 90.0);
472 assert!(position.y > 5.0 && position.y < 35.0);
473 } else {
474 panic!("Expected Text command");
475 }
476 }
477
478 #[test]
479 fn test_button_paint_custom_colors() {
480 let mut button = Button::new("Custom")
481 .background(Color::rgb(1.0, 0.0, 0.0))
482 .text_color(Color::rgb(0.0, 1.0, 0.0));
483 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
484
485 let mut canvas = RecordingCanvas::new();
486 button.paint(&mut canvas);
487
488 match &canvas.commands()[0] {
490 DrawCommand::Rect { style, .. } => {
491 let fill = style.fill.unwrap();
492 assert!((fill.r - 1.0).abs() < 0.01);
493 assert!(fill.g < 0.01);
494 assert!(fill.b < 0.01);
495 }
496 _ => panic!("Expected Rect command"),
497 }
498
499 let text_cmd = canvas
501 .commands()
502 .iter()
503 .find(|cmd| matches!(cmd, DrawCommand::Text { .. }));
504 if let Some(DrawCommand::Text { style, .. }) = text_cmd {
505 assert!(style.color.r < 0.01);
506 assert!((style.color.g - 1.0).abs() < 0.01);
507 assert!(style.color.b < 0.01);
508 }
509 }
510
511 use presentar_core::Key;
514
515 #[test]
516 fn test_button_event_mouse_enter_sets_hovered() {
517 let mut button = Button::new("Test");
518 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
519
520 assert!(!button.hovered);
521 let result = button.event(&Event::MouseEnter);
522 assert!(button.hovered);
523 assert!(result.is_none()); }
525
526 #[test]
527 fn test_button_event_mouse_leave_clears_hovered() {
528 let mut button = Button::new("Test");
529 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
530
531 button.event(&Event::MouseEnter);
532 assert!(button.hovered);
533
534 let result = button.event(&Event::MouseLeave);
535 assert!(!button.hovered);
536 assert!(result.is_none());
537 }
538
539 #[test]
540 fn test_button_event_mouse_leave_clears_pressed() {
541 let mut button = Button::new("Test");
542 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
543
544 button.event(&Event::MouseEnter);
546 button.event(&Event::MouseDown {
547 position: Point::new(50.0, 20.0),
548 button: MouseButton::Left,
549 });
550 assert!(button.pressed);
551
552 button.event(&Event::MouseLeave);
554 assert!(!button.pressed);
555 assert!(!button.hovered);
556 }
557
558 #[test]
559 fn test_button_event_mouse_down_sets_pressed() {
560 let mut button = Button::new("Test");
561 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
562
563 assert!(!button.pressed);
564 let result = button.event(&Event::MouseDown {
565 position: Point::new(50.0, 20.0),
566 button: MouseButton::Left,
567 });
568 assert!(button.pressed);
569 assert!(result.is_none()); }
571
572 #[test]
573 fn test_button_event_mouse_down_outside_bounds_no_press() {
574 let mut button = Button::new("Test");
575 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
576
577 let result = button.event(&Event::MouseDown {
578 position: Point::new(150.0, 20.0), button: MouseButton::Left,
580 });
581 assert!(!button.pressed);
582 assert!(result.is_none());
583 }
584
585 #[test]
586 fn test_button_event_mouse_down_right_button_no_press() {
587 let mut button = Button::new("Test");
588 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
589
590 let result = button.event(&Event::MouseDown {
591 position: Point::new(50.0, 20.0),
592 button: MouseButton::Right,
593 });
594 assert!(!button.pressed);
595 assert!(result.is_none());
596 }
597
598 #[test]
599 fn test_button_event_mouse_up_emits_clicked() {
600 let mut button = Button::new("Test");
601 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
602
603 button.event(&Event::MouseDown {
605 position: Point::new(50.0, 20.0),
606 button: MouseButton::Left,
607 });
608 assert!(button.pressed);
609
610 let result = button.event(&Event::MouseUp {
612 position: Point::new(50.0, 20.0),
613 button: MouseButton::Left,
614 });
615 assert!(!button.pressed);
616 assert!(result.is_some());
617
618 let _msg: Box<ButtonClicked> = result.unwrap().downcast::<ButtonClicked>().unwrap();
620 }
621
622 #[test]
623 fn test_button_event_mouse_up_outside_bounds_no_click() {
624 let mut button = Button::new("Test");
625 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
626
627 button.event(&Event::MouseDown {
629 position: Point::new(50.0, 20.0),
630 button: MouseButton::Left,
631 });
632 assert!(button.pressed);
633
634 let result = button.event(&Event::MouseUp {
636 position: Point::new(150.0, 20.0),
637 button: MouseButton::Left,
638 });
639 assert!(!button.pressed);
640 assert!(result.is_none()); }
642
643 #[test]
644 fn test_button_event_mouse_up_without_prior_press_no_click() {
645 let mut button = Button::new("Test");
646 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
647
648 let result = button.event(&Event::MouseUp {
650 position: Point::new(50.0, 20.0),
651 button: MouseButton::Left,
652 });
653 assert!(result.is_none());
654 }
655
656 #[test]
657 fn test_button_event_mouse_up_right_button_no_effect() {
658 let mut button = Button::new("Test");
659 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
660
661 button.event(&Event::MouseDown {
663 position: Point::new(50.0, 20.0),
664 button: MouseButton::Left,
665 });
666
667 let result = button.event(&Event::MouseUp {
669 position: Point::new(50.0, 20.0),
670 button: MouseButton::Right,
671 });
672 assert!(button.pressed); assert!(result.is_none());
674 }
675
676 #[test]
677 fn test_button_event_key_down_enter_sets_pressed() {
678 let mut button = Button::new("Test");
679 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
680
681 let result = button.event(&Event::KeyDown { key: Key::Enter });
682 assert!(button.pressed);
683 assert!(result.is_none()); }
685
686 #[test]
687 fn test_button_event_key_down_space_sets_pressed() {
688 let mut button = Button::new("Test");
689 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
690
691 let result = button.event(&Event::KeyDown { key: Key::Space });
692 assert!(button.pressed);
693 assert!(result.is_none());
694 }
695
696 #[test]
697 fn test_button_event_key_up_enter_emits_clicked() {
698 let mut button = Button::new("Test");
699 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
700
701 button.event(&Event::KeyDown { key: Key::Enter });
703 assert!(button.pressed);
704
705 let result = button.event(&Event::KeyUp { key: Key::Enter });
707 assert!(!button.pressed);
708 assert!(result.is_some());
709 }
710
711 #[test]
712 fn test_button_event_key_up_space_emits_clicked() {
713 let mut button = Button::new("Test");
714 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
715
716 button.event(&Event::KeyDown { key: Key::Space });
717 let result = button.event(&Event::KeyUp { key: Key::Space });
718 assert!(!button.pressed);
719 assert!(result.is_some());
720 }
721
722 #[test]
723 fn test_button_event_key_other_no_effect() {
724 let mut button = Button::new("Test");
725 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
726
727 let result = button.event(&Event::KeyDown { key: Key::Escape });
728 assert!(!button.pressed);
729 assert!(result.is_none());
730 }
731
732 #[test]
733 fn test_button_event_disabled_blocks_mouse_enter() {
734 let mut button = Button::new("Test").disabled(true);
735 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
736
737 let result = button.event(&Event::MouseEnter);
738 assert!(!button.hovered);
739 assert!(result.is_none());
740 }
741
742 #[test]
743 fn test_button_event_disabled_blocks_mouse_down() {
744 let mut button = Button::new("Test").disabled(true);
745 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
746
747 let result = button.event(&Event::MouseDown {
748 position: Point::new(50.0, 20.0),
749 button: MouseButton::Left,
750 });
751 assert!(!button.pressed);
752 assert!(result.is_none());
753 }
754
755 #[test]
756 fn test_button_event_disabled_blocks_key_down() {
757 let mut button = Button::new("Test").disabled(true);
758 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
759
760 let result = button.event(&Event::KeyDown { key: Key::Enter });
761 assert!(!button.pressed);
762 assert!(result.is_none());
763 }
764
765 #[test]
766 fn test_button_event_disabled_blocks_key_up() {
767 let mut button = Button::new("Test").disabled(true);
768 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
769
770 let result = button.event(&Event::KeyUp { key: Key::Enter });
771 assert!(result.is_none());
772 }
773
774 #[test]
775 fn test_button_click_full_interaction_flow() {
776 let mut button = Button::new("Submit");
777 button.layout(Rect::new(10.0, 10.0, 100.0, 40.0));
778
779 button.event(&Event::MouseEnter);
781 assert!(button.hovered);
782 assert!(!button.pressed);
783
784 button.event(&Event::MouseDown {
785 position: Point::new(50.0, 25.0),
786 button: MouseButton::Left,
787 });
788 assert!(button.hovered);
789 assert!(button.pressed);
790
791 let result = button.event(&Event::MouseUp {
792 position: Point::new(50.0, 25.0),
793 button: MouseButton::Left,
794 });
795 assert!(button.hovered);
796 assert!(!button.pressed);
797 assert!(result.is_some()); button.event(&Event::MouseLeave);
800 assert!(!button.hovered);
801 assert!(!button.pressed);
802 }
803
804 #[test]
805 fn test_button_drag_out_and_release_no_click() {
806 let mut button = Button::new("Drag");
807 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
808
809 button.event(&Event::MouseEnter);
811 button.event(&Event::MouseDown {
812 position: Point::new(50.0, 20.0),
813 button: MouseButton::Left,
814 });
815 assert!(button.pressed);
816
817 button.event(&Event::MouseLeave);
819 assert!(!button.pressed); let result = button.event(&Event::MouseUp {
823 position: Point::new(150.0, 20.0),
824 button: MouseButton::Left,
825 });
826 assert!(result.is_none()); }
828
829 #[test]
830 fn test_button_event_bounds_edge_cases() {
831 let mut button = Button::new("Edge");
832 button.layout(Rect::new(10.0, 20.0, 100.0, 40.0));
833
834 button.event(&Event::MouseDown {
836 position: Point::new(10.0, 20.0),
837 button: MouseButton::Left,
838 });
839 assert!(button.pressed);
840 button.pressed = false;
841
842 button.event(&Event::MouseDown {
844 position: Point::new(109.9, 59.9),
845 button: MouseButton::Left,
846 });
847 assert!(button.pressed);
848 button.pressed = false;
849
850 button.event(&Event::MouseDown {
852 position: Point::new(111.0, 30.0),
853 button: MouseButton::Left,
854 });
855 assert!(!button.pressed);
856 }
857}