1use presentar_core::{
4 widget::{AccessibleRole, FontWeight, LayoutResult, TextStyle},
5 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints,
6 CornerRadius, Event, MouseButton, Point, Rect, Size, TypeId, Widget,
7};
8use serde::{Deserialize, Serialize};
9use std::any::Any;
10use std::time::Duration;
11
12#[derive(Clone, Serialize, Deserialize)]
14pub struct Button {
15 label: String,
17 background: Color,
19 background_hover: Color,
21 background_pressed: Color,
23 text_color: Color,
25 corner_radius: CornerRadius,
27 padding: f32,
29 font_size: f32,
31 disabled: bool,
33 test_id_value: Option<String>,
35 accessible_name: Option<String>,
37 #[serde(skip)]
39 hovered: bool,
40 #[serde(skip)]
42 pressed: bool,
43 #[serde(skip)]
45 bounds: Rect,
46}
47
48#[derive(Debug, Clone)]
50pub struct ButtonClicked;
51
52impl Button {
53 #[must_use]
55 pub fn new(label: impl Into<String>) -> Self {
56 Self {
57 label: label.into(),
58 background: Color::from_hex("#4f46e5").unwrap_or(Color::BLACK),
61 background_hover: Color::from_hex("#4338ca").unwrap_or(Color::BLACK),
62 background_pressed: Color::from_hex("#3730a3").unwrap_or(Color::BLACK),
63 text_color: Color::WHITE,
64 corner_radius: CornerRadius::uniform(4.0),
65 padding: 12.0,
66 font_size: 14.0,
67 disabled: false,
68 test_id_value: None,
69 accessible_name: None,
70 hovered: false,
71 pressed: false,
72 bounds: Rect::default(),
73 }
74 }
75
76 #[must_use]
78 pub const fn background(mut self, color: Color) -> Self {
79 self.background = color;
80 self
81 }
82
83 #[must_use]
85 pub const fn background_hover(mut self, color: Color) -> Self {
86 self.background_hover = color;
87 self
88 }
89
90 #[must_use]
92 pub const fn background_pressed(mut self, color: Color) -> Self {
93 self.background_pressed = color;
94 self
95 }
96
97 #[must_use]
99 pub const fn text_color(mut self, color: Color) -> Self {
100 self.text_color = color;
101 self
102 }
103
104 #[must_use]
106 pub const fn corner_radius(mut self, radius: CornerRadius) -> Self {
107 self.corner_radius = radius;
108 self
109 }
110
111 #[must_use]
113 pub const fn padding(mut self, padding: f32) -> Self {
114 self.padding = padding;
115 self
116 }
117
118 #[must_use]
120 pub const fn font_size(mut self, size: f32) -> Self {
121 self.font_size = size;
122 self
123 }
124
125 #[must_use]
127 pub const fn disabled(mut self, disabled: bool) -> Self {
128 self.disabled = disabled;
129 self
130 }
131
132 #[must_use]
134 pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
135 self.test_id_value = Some(id.into());
136 self
137 }
138
139 #[must_use]
141 pub fn with_accessible_name(mut self, name: impl Into<String>) -> Self {
142 self.accessible_name = Some(name.into());
143 self
144 }
145
146 fn current_background(&self) -> Color {
148 if self.disabled {
149 let gray = (self.background.r + self.background.g + self.background.b) / 3.0;
151 Color::rgb(gray, gray, gray)
152 } else if self.pressed {
153 self.background_pressed
154 } else if self.hovered {
155 self.background_hover
156 } else {
157 self.background
158 }
159 }
160
161 fn estimate_text_size(&self) -> Size {
163 let char_width = self.font_size * 0.6;
164 let width = self.label.len() as f32 * char_width;
165 let height = self.font_size * 1.2;
166 Size::new(width, height)
167 }
168}
169
170impl Widget for Button {
171 fn type_id(&self) -> TypeId {
172 TypeId::of::<Self>()
173 }
174
175 fn measure(&self, constraints: Constraints) -> Size {
176 let text_size = self.estimate_text_size();
177 let size = Size::new(
178 self.padding.mul_add(2.0, text_size.width),
179 self.padding.mul_add(2.0, text_size.height),
180 );
181 constraints.constrain(size)
182 }
183
184 fn layout(&mut self, bounds: Rect) -> LayoutResult {
185 self.bounds = bounds;
186 LayoutResult {
187 size: bounds.size(),
188 }
189 }
190
191 fn paint(&self, canvas: &mut dyn Canvas) {
192 canvas.fill_rect(self.bounds, self.current_background());
194
195 let text_size = self.estimate_text_size();
197 let text_pos = Point::new(
198 self.bounds.x + (self.bounds.width - text_size.width) / 2.0,
199 self.bounds.y + (self.bounds.height - text_size.height) / 2.0,
200 );
201
202 let style = TextStyle {
203 size: self.font_size,
204 color: if self.disabled {
205 Color::rgb(0.7, 0.7, 0.7)
206 } else {
207 self.text_color
208 },
209 weight: FontWeight::Medium,
210 ..Default::default()
211 };
212
213 canvas.draw_text(&self.label, text_pos, &style);
214 }
215
216 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
217 if self.disabled {
218 return None;
219 }
220
221 match event {
222 Event::MouseEnter => {
223 self.hovered = true;
224 None
225 }
226 Event::MouseLeave => {
227 self.hovered = false;
228 self.pressed = false;
229 None
230 }
231 Event::MouseDown {
232 position,
233 button: MouseButton::Left,
234 } => {
235 if self.bounds.contains_point(position) {
236 self.pressed = true;
237 }
238 None
239 }
240 Event::MouseUp {
241 position,
242 button: MouseButton::Left,
243 } => {
244 let was_pressed = self.pressed;
245 self.pressed = false;
246
247 if was_pressed && self.bounds.contains_point(position) {
248 Some(Box::new(ButtonClicked))
249 } else {
250 None
251 }
252 }
253 Event::KeyDown {
254 key: presentar_core::Key::Enter | presentar_core::Key::Space,
255 ..
256 } => {
257 self.pressed = true;
258 None
259 }
260 Event::KeyUp {
261 key: presentar_core::Key::Enter | presentar_core::Key::Space,
262 ..
263 } => {
264 self.pressed = false;
265 Some(Box::new(ButtonClicked))
266 }
267 _ => None,
268 }
269 }
270
271 fn children(&self) -> &[Box<dyn Widget>] {
272 &[]
273 }
274
275 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
276 &mut []
277 }
278
279 fn is_interactive(&self) -> bool {
280 !self.disabled
281 }
282
283 fn is_focusable(&self) -> bool {
284 !self.disabled
285 }
286
287 fn accessible_name(&self) -> Option<&str> {
288 self.accessible_name.as_deref().or(Some(&self.label))
289 }
290
291 fn accessible_role(&self) -> AccessibleRole {
292 AccessibleRole::Button
293 }
294
295 fn test_id(&self) -> Option<&str> {
296 self.test_id_value.as_deref()
297 }
298}
299
300impl Brick for Button {
302 fn brick_name(&self) -> &'static str {
303 "Button"
304 }
305
306 fn assertions(&self) -> &[BrickAssertion] {
307 &[
309 BrickAssertion::TextVisible,
310 BrickAssertion::ContrastRatio(4.5), ]
312 }
313
314 fn budget(&self) -> BrickBudget {
315 BrickBudget::uniform(16) }
317
318 fn verify(&self) -> BrickVerification {
319 let mut passed = Vec::new();
320 let mut failed = Vec::new();
321
322 let bg = self.current_background();
324 let contrast = bg.contrast_ratio(&self.text_color);
325 if self.disabled || contrast >= 4.5 {
329 passed.push(BrickAssertion::ContrastRatio(4.5));
330 } else {
331 failed.push((
332 BrickAssertion::ContrastRatio(4.5),
333 format!("Contrast ratio {contrast:.2}:1 < 4.5:1"),
334 ));
335 }
336
337 if self.label.is_empty() {
339 failed.push((BrickAssertion::TextVisible, "Button has no label".into()));
340 } else {
341 passed.push(BrickAssertion::TextVisible);
342 }
343
344 BrickVerification {
345 passed,
346 failed,
347 verification_time: Duration::from_micros(10),
348 }
349 }
350
351 fn to_html(&self) -> String {
352 let disabled = if self.disabled { " disabled" } else { "" };
353 let test_id = self.test_id_value.as_deref().unwrap_or("button");
354 format!(
355 r#"<button class="brick-button" data-testid="{}" aria-label="{}"{}>{}</button>"#,
356 test_id,
357 self.accessible_name.as_deref().unwrap_or(&self.label),
358 disabled,
359 self.label
360 )
361 }
362
363 fn to_css(&self) -> String {
364 format!(
365 r".brick-button {{
366 background: {};
367 color: {};
368 padding: {}px;
369 font-size: {}px;
370 border: none;
371 border-radius: {}px;
372 cursor: pointer;
373}}
374.brick-button:hover {{ background: {}; }}
375.brick-button:active {{ background: {}; }}
376.brick-button:disabled {{ opacity: 0.5; cursor: not-allowed; }}",
377 self.background.to_hex(),
378 self.text_color.to_hex(),
379 self.padding,
380 self.font_size,
381 self.corner_radius.top_left,
382 self.background_hover.to_hex(),
383 self.background_pressed.to_hex(),
384 )
385 }
386
387 fn test_id(&self) -> Option<&str> {
388 self.test_id_value.as_deref()
389 }
390}
391
392#[cfg(test)]
393#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
394mod tests {
395 use super::*;
396
397 #[test]
400 fn a_default_button_passes_its_own_verification_in_every_state() {
401 let mut button = Button::new("+");
402 assert!(button.verify().failed.is_empty(), "idle");
403 button.hovered = true;
404 assert!(button.verify().failed.is_empty(), "hovered");
405 button.pressed = true;
406 assert!(button.verify().failed.is_empty(), "pressed");
407 let mut disabled = Button::new("+").disabled(true);
408 assert!(disabled.verify().failed.is_empty(), "disabled");
409 disabled.hovered = true;
410 assert!(disabled.verify().failed.is_empty(), "disabled+hovered");
411 for background in [
412 button.background,
413 button.background_hover,
414 button.background_pressed,
415 ] {
416 assert!(background.contrast_ratio(&button.text_color) >= 4.5);
417 }
418 }
419 use presentar_core::draw::DrawCommand;
420 use presentar_core::{RecordingCanvas, Widget};
421
422 #[test]
423 fn test_button_new() {
424 let b = Button::new("Click me");
425 assert_eq!(b.label, "Click me");
426 assert!(!b.disabled);
427 }
428
429 #[test]
430 fn test_button_builder() {
431 let b = Button::new("Test")
432 .padding(20.0)
433 .font_size(18.0)
434 .disabled(true)
435 .with_test_id("my-button");
436
437 assert_eq!(b.padding, 20.0);
438 assert_eq!(b.font_size, 18.0);
439 assert!(b.disabled);
440 assert_eq!(Widget::test_id(&b), Some("my-button"));
441 }
442
443 #[test]
444 fn test_button_accessible() {
445 let b = Button::new("OK");
446 assert_eq!(Widget::accessible_name(&b), Some("OK"));
447 assert_eq!(Widget::accessible_role(&b), AccessibleRole::Button);
448 assert!(Widget::is_focusable(&b));
449 }
450
451 #[test]
452 fn test_button_disabled_not_focusable() {
453 let b = Button::new("OK").disabled(true);
454 assert!(!Widget::is_focusable(&b));
455 assert!(!Widget::is_interactive(&b));
456 }
457
458 #[test]
459 fn test_button_measure() {
460 let b = Button::new("Test");
461 let size = b.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
462 assert!(size.width > 0.0);
463 assert!(size.height > 0.0);
464 }
465
466 #[test]
469 fn test_button_paint_draws_background() {
470 let mut button = Button::new("Click");
471 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
472
473 let mut canvas = RecordingCanvas::new();
474 button.paint(&mut canvas);
475
476 assert!(canvas.command_count() >= 2);
478
479 match &canvas.commands()[0] {
481 DrawCommand::Rect { bounds, style, .. } => {
482 assert_eq!(bounds.width, 100.0);
483 assert_eq!(bounds.height, 40.0);
484 assert!(style.fill.is_some());
485 }
486 _ => panic!("Expected Rect command for background"),
487 }
488 }
489
490 #[test]
491 fn test_button_paint_draws_text() {
492 let mut button = Button::new("Hello");
493 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
494
495 let mut canvas = RecordingCanvas::new();
496 button.paint(&mut canvas);
497
498 let has_text = canvas
500 .commands()
501 .iter()
502 .any(|cmd| matches!(cmd, DrawCommand::Text { content, .. } if content == "Hello"));
503 assert!(has_text, "Should draw button label text");
504 }
505
506 #[test]
507 fn test_button_paint_disabled_uses_gray() {
508 let mut button = Button::new("Disabled").disabled(true);
509 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
510
511 let mut canvas = RecordingCanvas::new();
512 button.paint(&mut canvas);
513
514 let text_cmd = canvas
516 .commands()
517 .iter()
518 .find(|cmd| matches!(cmd, DrawCommand::Text { .. }));
519
520 if let Some(DrawCommand::Text { style, .. }) = text_cmd {
521 assert!(style.color.r > 0.5 && style.color.g > 0.5 && style.color.b > 0.5);
523 } else {
524 panic!("Expected Text command");
525 }
526 }
527
528 #[test]
529 fn test_button_paint_hovered_uses_hover_color() {
530 let mut button = Button::new("Hover")
531 .background(Color::RED)
532 .background_hover(Color::BLUE);
533 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
534
535 button.event(&Event::MouseEnter);
537
538 let mut canvas = RecordingCanvas::new();
539 button.paint(&mut canvas);
540
541 match &canvas.commands()[0] {
543 DrawCommand::Rect { style, .. } => {
544 assert_eq!(style.fill, Some(Color::BLUE));
545 }
546 _ => panic!("Expected Rect command"),
547 }
548 }
549
550 #[test]
551 fn test_button_paint_pressed_uses_pressed_color() {
552 let mut button = Button::new("Press")
553 .background(Color::RED)
554 .background_pressed(Color::GREEN);
555 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
556
557 button.event(&Event::MouseEnter);
559 button.event(&Event::MouseDown {
560 position: Point::new(50.0, 20.0),
561 button: MouseButton::Left,
562 });
563
564 let mut canvas = RecordingCanvas::new();
565 button.paint(&mut canvas);
566
567 match &canvas.commands()[0] {
569 DrawCommand::Rect { style, .. } => {
570 assert_eq!(style.fill, Some(Color::GREEN));
571 }
572 _ => panic!("Expected Rect command"),
573 }
574 }
575
576 #[test]
577 fn test_button_paint_text_centered() {
578 let mut button = Button::new("X");
579 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
580
581 let mut canvas = RecordingCanvas::new();
582 button.paint(&mut canvas);
583
584 let text_cmd = canvas
586 .commands()
587 .iter()
588 .find(|cmd| matches!(cmd, DrawCommand::Text { .. }));
589
590 if let Some(DrawCommand::Text { position, .. }) = text_cmd {
591 assert!(position.x > 10.0 && position.x < 90.0);
593 assert!(position.y > 5.0 && position.y < 35.0);
594 } else {
595 panic!("Expected Text command");
596 }
597 }
598
599 #[test]
600 fn test_button_paint_custom_colors() {
601 let mut button = Button::new("Custom")
602 .background(Color::rgb(1.0, 0.0, 0.0))
603 .text_color(Color::rgb(0.0, 1.0, 0.0));
604 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
605
606 let mut canvas = RecordingCanvas::new();
607 button.paint(&mut canvas);
608
609 match &canvas.commands()[0] {
611 DrawCommand::Rect { style, .. } => {
612 let fill = style.fill.unwrap();
613 assert!((fill.r - 1.0).abs() < 0.01);
614 assert!(fill.g < 0.01);
615 assert!(fill.b < 0.01);
616 }
617 _ => panic!("Expected Rect command"),
618 }
619
620 let text_cmd = canvas
622 .commands()
623 .iter()
624 .find(|cmd| matches!(cmd, DrawCommand::Text { .. }));
625 if let Some(DrawCommand::Text { style, .. }) = text_cmd {
626 assert!(style.color.r < 0.01);
627 assert!((style.color.g - 1.0).abs() < 0.01);
628 assert!(style.color.b < 0.01);
629 }
630 }
631
632 use presentar_core::Key;
635
636 #[test]
637 fn test_button_event_mouse_enter_sets_hovered() {
638 let mut button = Button::new("Test");
639 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
640
641 assert!(!button.hovered);
642 let result = button.event(&Event::MouseEnter);
643 assert!(button.hovered);
644 assert!(result.is_none()); }
646
647 #[test]
648 fn test_button_event_mouse_leave_clears_hovered() {
649 let mut button = Button::new("Test");
650 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
651
652 button.event(&Event::MouseEnter);
653 assert!(button.hovered);
654
655 let result = button.event(&Event::MouseLeave);
656 assert!(!button.hovered);
657 assert!(result.is_none());
658 }
659
660 #[test]
661 fn test_button_event_mouse_leave_clears_pressed() {
662 let mut button = Button::new("Test");
663 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
664
665 button.event(&Event::MouseEnter);
667 button.event(&Event::MouseDown {
668 position: Point::new(50.0, 20.0),
669 button: MouseButton::Left,
670 });
671 assert!(button.pressed);
672
673 button.event(&Event::MouseLeave);
675 assert!(!button.pressed);
676 assert!(!button.hovered);
677 }
678
679 #[test]
680 fn test_button_event_mouse_down_sets_pressed() {
681 let mut button = Button::new("Test");
682 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
683
684 assert!(!button.pressed);
685 let result = button.event(&Event::MouseDown {
686 position: Point::new(50.0, 20.0),
687 button: MouseButton::Left,
688 });
689 assert!(button.pressed);
690 assert!(result.is_none()); }
692
693 #[test]
694 fn test_button_event_mouse_down_outside_bounds_no_press() {
695 let mut button = Button::new("Test");
696 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
697
698 let result = button.event(&Event::MouseDown {
699 position: Point::new(150.0, 20.0), button: MouseButton::Left,
701 });
702 assert!(!button.pressed);
703 assert!(result.is_none());
704 }
705
706 #[test]
707 fn test_button_event_mouse_down_right_button_no_press() {
708 let mut button = Button::new("Test");
709 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
710
711 let result = button.event(&Event::MouseDown {
712 position: Point::new(50.0, 20.0),
713 button: MouseButton::Right,
714 });
715 assert!(!button.pressed);
716 assert!(result.is_none());
717 }
718
719 #[test]
720 fn test_button_event_mouse_up_emits_clicked() {
721 let mut button = Button::new("Test");
722 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
723
724 button.event(&Event::MouseDown {
726 position: Point::new(50.0, 20.0),
727 button: MouseButton::Left,
728 });
729 assert!(button.pressed);
730
731 let result = button.event(&Event::MouseUp {
733 position: Point::new(50.0, 20.0),
734 button: MouseButton::Left,
735 });
736 assert!(!button.pressed);
737 assert!(result.is_some());
738
739 let _msg: Box<ButtonClicked> = result.unwrap().downcast::<ButtonClicked>().unwrap();
741 }
742
743 #[test]
744 fn test_button_event_mouse_up_outside_bounds_no_click() {
745 let mut button = Button::new("Test");
746 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
747
748 button.event(&Event::MouseDown {
750 position: Point::new(50.0, 20.0),
751 button: MouseButton::Left,
752 });
753 assert!(button.pressed);
754
755 let result = button.event(&Event::MouseUp {
757 position: Point::new(150.0, 20.0),
758 button: MouseButton::Left,
759 });
760 assert!(!button.pressed);
761 assert!(result.is_none()); }
763
764 #[test]
765 fn test_button_event_mouse_up_without_prior_press_no_click() {
766 let mut button = Button::new("Test");
767 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
768
769 let result = button.event(&Event::MouseUp {
771 position: Point::new(50.0, 20.0),
772 button: MouseButton::Left,
773 });
774 assert!(result.is_none());
775 }
776
777 #[test]
778 fn test_button_event_mouse_up_right_button_no_effect() {
779 let mut button = Button::new("Test");
780 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
781
782 button.event(&Event::MouseDown {
784 position: Point::new(50.0, 20.0),
785 button: MouseButton::Left,
786 });
787
788 let result = button.event(&Event::MouseUp {
790 position: Point::new(50.0, 20.0),
791 button: MouseButton::Right,
792 });
793 assert!(button.pressed); assert!(result.is_none());
795 }
796
797 #[test]
798 fn test_button_event_key_down_enter_sets_pressed() {
799 let mut button = Button::new("Test");
800 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
801
802 let result = button.event(&Event::key_down(Key::Enter));
803 assert!(button.pressed);
804 assert!(result.is_none()); }
806
807 #[test]
808 fn test_button_event_key_down_space_sets_pressed() {
809 let mut button = Button::new("Test");
810 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
811
812 let result = button.event(&Event::key_down(Key::Space));
813 assert!(button.pressed);
814 assert!(result.is_none());
815 }
816
817 #[test]
818 fn test_button_event_key_up_enter_emits_clicked() {
819 let mut button = Button::new("Test");
820 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
821
822 button.event(&Event::key_down(Key::Enter));
824 assert!(button.pressed);
825
826 let result = button.event(&Event::key_up(Key::Enter));
828 assert!(!button.pressed);
829 assert!(result.is_some());
830 }
831
832 #[test]
833 fn test_button_event_key_up_space_emits_clicked() {
834 let mut button = Button::new("Test");
835 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
836
837 button.event(&Event::key_down(Key::Space));
838 let result = button.event(&Event::key_up(Key::Space));
839 assert!(!button.pressed);
840 assert!(result.is_some());
841 }
842
843 #[test]
844 fn test_button_event_key_other_no_effect() {
845 let mut button = Button::new("Test");
846 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
847
848 let result = button.event(&Event::key_down(Key::Escape));
849 assert!(!button.pressed);
850 assert!(result.is_none());
851 }
852
853 #[test]
854 fn test_button_event_disabled_blocks_mouse_enter() {
855 let mut button = Button::new("Test").disabled(true);
856 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
857
858 let result = button.event(&Event::MouseEnter);
859 assert!(!button.hovered);
860 assert!(result.is_none());
861 }
862
863 #[test]
864 fn test_button_event_disabled_blocks_mouse_down() {
865 let mut button = Button::new("Test").disabled(true);
866 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
867
868 let result = button.event(&Event::MouseDown {
869 position: Point::new(50.0, 20.0),
870 button: MouseButton::Left,
871 });
872 assert!(!button.pressed);
873 assert!(result.is_none());
874 }
875
876 #[test]
877 fn test_button_event_disabled_blocks_key_down() {
878 let mut button = Button::new("Test").disabled(true);
879 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
880
881 let result = button.event(&Event::key_down(Key::Enter));
882 assert!(!button.pressed);
883 assert!(result.is_none());
884 }
885
886 #[test]
887 fn test_button_event_disabled_blocks_key_up() {
888 let mut button = Button::new("Test").disabled(true);
889 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
890
891 let result = button.event(&Event::key_up(Key::Enter));
892 assert!(result.is_none());
893 }
894
895 #[test]
896 fn test_button_click_full_interaction_flow() {
897 let mut button = Button::new("Submit");
898 button.layout(Rect::new(10.0, 10.0, 100.0, 40.0));
899
900 button.event(&Event::MouseEnter);
902 assert!(button.hovered);
903 assert!(!button.pressed);
904
905 button.event(&Event::MouseDown {
906 position: Point::new(50.0, 25.0),
907 button: MouseButton::Left,
908 });
909 assert!(button.hovered);
910 assert!(button.pressed);
911
912 let result = button.event(&Event::MouseUp {
913 position: Point::new(50.0, 25.0),
914 button: MouseButton::Left,
915 });
916 assert!(button.hovered);
917 assert!(!button.pressed);
918 assert!(result.is_some()); button.event(&Event::MouseLeave);
921 assert!(!button.hovered);
922 assert!(!button.pressed);
923 }
924
925 #[test]
926 fn test_button_drag_out_and_release_no_click() {
927 let mut button = Button::new("Drag");
928 button.layout(Rect::new(0.0, 0.0, 100.0, 40.0));
929
930 button.event(&Event::MouseEnter);
932 button.event(&Event::MouseDown {
933 position: Point::new(50.0, 20.0),
934 button: MouseButton::Left,
935 });
936 assert!(button.pressed);
937
938 button.event(&Event::MouseLeave);
940 assert!(!button.pressed); let result = button.event(&Event::MouseUp {
944 position: Point::new(150.0, 20.0),
945 button: MouseButton::Left,
946 });
947 assert!(result.is_none()); }
949
950 #[test]
951 fn test_button_event_bounds_edge_cases() {
952 let mut button = Button::new("Edge");
953 button.layout(Rect::new(10.0, 20.0, 100.0, 40.0));
954
955 button.event(&Event::MouseDown {
957 position: Point::new(10.0, 20.0),
958 button: MouseButton::Left,
959 });
960 assert!(button.pressed);
961 button.pressed = false;
962
963 button.event(&Event::MouseDown {
965 position: Point::new(109.9, 59.9),
966 button: MouseButton::Left,
967 });
968 assert!(button.pressed);
969 button.pressed = false;
970
971 button.event(&Event::MouseDown {
973 position: Point::new(111.0, 30.0),
974 button: MouseButton::Left,
975 });
976 assert!(!button.pressed);
977 }
978}