presentar_widgets/
button.rs

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