presentar_widgets/
button.rs

1//! Button widget for user interactions.
2
3use 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/// Button widget with label and click handling.
12#[derive(Clone, Serialize, Deserialize)]
13pub struct Button {
14    /// Button label
15    label: String,
16    /// Background color (normal state)
17    background: Color,
18    /// Background color (hover state)
19    background_hover: Color,
20    /// Background color (pressed state)
21    background_pressed: Color,
22    /// Text color
23    text_color: Color,
24    /// Corner radius
25    corner_radius: CornerRadius,
26    /// Padding
27    padding: f32,
28    /// Font size
29    font_size: f32,
30    /// Whether button is disabled
31    disabled: bool,
32    /// Test ID
33    test_id_value: Option<String>,
34    /// Accessible name (overrides label)
35    accessible_name: Option<String>,
36    /// Current hover state
37    #[serde(skip)]
38    hovered: bool,
39    /// Current pressed state
40    #[serde(skip)]
41    pressed: bool,
42    /// Cached bounds
43    #[serde(skip)]
44    bounds: Rect,
45}
46
47/// Message emitted when button is clicked.
48#[derive(Debug, Clone)]
49pub struct ButtonClicked;
50
51impl Button {
52    /// Create a new button with label.
53    #[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    /// Set background color.
74    #[must_use]
75    pub const fn background(mut self, color: Color) -> Self {
76        self.background = color;
77        self
78    }
79
80    /// Set hover background color.
81    #[must_use]
82    pub const fn background_hover(mut self, color: Color) -> Self {
83        self.background_hover = color;
84        self
85    }
86
87    /// Set pressed background color.
88    #[must_use]
89    pub const fn background_pressed(mut self, color: Color) -> Self {
90        self.background_pressed = color;
91        self
92    }
93
94    /// Set text color.
95    #[must_use]
96    pub const fn text_color(mut self, color: Color) -> Self {
97        self.text_color = color;
98        self
99    }
100
101    /// Set corner radius.
102    #[must_use]
103    pub const fn corner_radius(mut self, radius: CornerRadius) -> Self {
104        self.corner_radius = radius;
105        self
106    }
107
108    /// Set padding.
109    #[must_use]
110    pub const fn padding(mut self, padding: f32) -> Self {
111        self.padding = padding;
112        self
113    }
114
115    /// Set font size.
116    #[must_use]
117    pub const fn font_size(mut self, size: f32) -> Self {
118        self.font_size = size;
119        self
120    }
121
122    /// Set disabled state.
123    #[must_use]
124    pub const fn disabled(mut self, disabled: bool) -> Self {
125        self.disabled = disabled;
126        self
127    }
128
129    /// Set test ID.
130    #[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    /// Set accessible name.
137    #[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    /// Get the current background color based on state.
144    fn current_background(&self) -> Color {
145        if self.disabled {
146            // Desaturated version
147            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    /// Estimate text size.
159    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        // Draw background
190        canvas.fill_rect(self.bounds, self.current_background());
191
192        // Draw text centered
193        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    // ===== Paint Tests =====
346
347    #[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        // Should have at least 2 commands: background rect + text
356        assert!(canvas.command_count() >= 2);
357
358        // First command should be the background rect
359        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        // Should have text command
378        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        // Check text color is gray (disabled)
394        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            // Disabled text should be grayish
401            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        // Simulate hover
415        button.event(&Event::MouseEnter);
416
417        let mut canvas = RecordingCanvas::new();
418        button.paint(&mut canvas);
419
420        // Background should use hover color
421        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        // Simulate press
437        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        // Background should use pressed color
447        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        // Text should be roughly centered
464        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            // Text should be somewhere in the middle, not at edge
471            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        // Check background color
489        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        // Check text color
500        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    // ===== Event Handling Tests =====
512
513    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()); // No message emitted
524    }
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        // Enter and press
545        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        // Leave should clear pressed
553        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()); // MouseDown doesn't emit click
570    }
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), // Outside bounds
579            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        // Press down
604        button.event(&Event::MouseDown {
605            position: Point::new(50.0, 20.0),
606            button: MouseButton::Left,
607        });
608        assert!(button.pressed);
609
610        // Release inside bounds
611        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        // Verify it's a ButtonClicked message
619        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        // Press down inside
628        button.event(&Event::MouseDown {
629            position: Point::new(50.0, 20.0),
630            button: MouseButton::Left,
631        });
632        assert!(button.pressed);
633
634        // Release outside bounds
635        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()); // No click emitted
641    }
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        // Mouse up without prior press
649        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        // Press with left button
662        button.event(&Event::MouseDown {
663            position: Point::new(50.0, 20.0),
664            button: MouseButton::Left,
665        });
666
667        // Release with right button (should not trigger click)
668        let result = button.event(&Event::MouseUp {
669            position: Point::new(50.0, 20.0),
670            button: MouseButton::Right,
671        });
672        assert!(button.pressed); // Still pressed
673        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()); // KeyDown doesn't emit click
684    }
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        // Key down first
702        button.event(&Event::KeyDown { key: Key::Enter });
703        assert!(button.pressed);
704
705        // Key up emits click
706        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        // Full click flow: enter -> down -> up -> leave
780        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()); // Click emitted
798
799        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        // Press inside
810        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        // Leave while pressed
818        button.event(&Event::MouseLeave);
819        assert!(!button.pressed); // Cleared by leave
820
821        // Release outside
822        let result = button.event(&Event::MouseUp {
823            position: Point::new(150.0, 20.0),
824            button: MouseButton::Left,
825        });
826        assert!(result.is_none()); // No click
827    }
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        // Click at top-left corner (inside)
835        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        // Click at bottom-right corner (inside, at edge)
843        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        // Click just outside right edge
851        button.event(&Event::MouseDown {
852            position: Point::new(111.0, 30.0),
853            button: MouseButton::Left,
854        });
855        assert!(!button.pressed);
856    }
857}