Skip to main content

fresh/view/controls/button/
input.rs

1//! Button input handling
2
3use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
4
5use super::{ButtonLayout, ButtonState, FocusState};
6
7/// Events that can be returned from button input handling
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ButtonEvent {
10    /// Button was clicked (mouse released over button)
11    Clicked,
12    /// Mouse is hovering over the button
13    Hovered,
14    /// Mouse left the button area
15    Left,
16}
17
18impl ButtonState {
19    /// Handle a mouse event for this button
20    ///
21    /// # Arguments
22    /// * `event` - The mouse event to handle
23    /// * `layout` - The button's rendered layout for hit testing
24    ///
25    /// # Returns
26    /// * `Some(ButtonEvent)` if the event was consumed and an action should be taken
27    /// * `None` if the event was not relevant to this button
28    pub fn handle_mouse(
29        &mut self,
30        event: MouseEvent,
31        layout: &ButtonLayout,
32    ) -> Option<ButtonEvent> {
33        if !self.is_enabled() {
34            return None;
35        }
36
37        let inside = layout.contains(event.column, event.row);
38
39        match event.kind {
40            MouseEventKind::Down(MouseButton::Left) if inside => {
41                self.pressed = true;
42                None // Wait for release to trigger click
43            }
44            MouseEventKind::Up(MouseButton::Left) => {
45                let was_pressed = self.pressed;
46                self.pressed = false;
47
48                if inside && was_pressed {
49                    Some(ButtonEvent::Clicked)
50                } else {
51                    None
52                }
53            }
54            MouseEventKind::Moved => {
55                if inside {
56                    if self.focus != FocusState::Focused {
57                        self.focus = FocusState::Hovered;
58                    }
59                    Some(ButtonEvent::Hovered)
60                } else if self.focus == FocusState::Hovered {
61                    self.focus = FocusState::Normal;
62                    Some(ButtonEvent::Left)
63                } else {
64                    None
65                }
66            }
67            _ => None,
68        }
69    }
70
71    /// Handle a keyboard event for this button (when focused)
72    ///
73    /// # Returns
74    /// * `Some(ButtonEvent::Clicked)` if Enter or Space was pressed
75    /// * `None` otherwise
76    pub fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> Option<ButtonEvent> {
77        use crossterm::event::KeyCode;
78
79        if !self.is_enabled() || self.focus != FocusState::Focused {
80            return None;
81        }
82
83        match key.code {
84            KeyCode::Enter | KeyCode::Char(' ') => Some(ButtonEvent::Clicked),
85            _ => None,
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crossterm::event::{KeyCode, KeyModifiers};
94    use ratatui::layout::Rect;
95
96    fn make_layout() -> ButtonLayout {
97        ButtonLayout {
98            button_area: Rect::new(0, 0, 10, 1),
99        }
100    }
101
102    fn mouse_down(x: u16, y: u16) -> MouseEvent {
103        MouseEvent {
104            kind: MouseEventKind::Down(MouseButton::Left),
105            column: x,
106            row: y,
107            modifiers: crossterm::event::KeyModifiers::empty(),
108        }
109    }
110
111    fn mouse_up(x: u16, y: u16) -> MouseEvent {
112        MouseEvent {
113            kind: MouseEventKind::Up(MouseButton::Left),
114            column: x,
115            row: y,
116            modifiers: crossterm::event::KeyModifiers::empty(),
117        }
118    }
119
120    fn mouse_move(x: u16, y: u16) -> MouseEvent {
121        MouseEvent {
122            kind: MouseEventKind::Moved,
123            column: x,
124            row: y,
125            modifiers: crossterm::event::KeyModifiers::empty(),
126        }
127    }
128
129    #[test]
130    fn test_click_inside_button() {
131        let mut state = ButtonState::new("Test");
132        let layout = make_layout();
133
134        // Mouse down inside
135        let result = state.handle_mouse(mouse_down(5, 0), &layout);
136        assert!(result.is_none());
137        assert!(state.pressed);
138
139        // Mouse up inside - should click
140        let result = state.handle_mouse(mouse_up(5, 0), &layout);
141        assert_eq!(result, Some(ButtonEvent::Clicked));
142        assert!(!state.pressed);
143    }
144
145    #[test]
146    fn test_click_outside_button() {
147        let mut state = ButtonState::new("Test");
148        let layout = make_layout();
149
150        // Mouse down inside
151        state.handle_mouse(mouse_down(5, 0), &layout);
152        assert!(state.pressed);
153
154        // Mouse up outside - should not click
155        let result = state.handle_mouse(mouse_up(15, 0), &layout);
156        assert!(result.is_none());
157        assert!(!state.pressed);
158    }
159
160    #[test]
161    fn test_hover() {
162        let mut state = ButtonState::new("Test");
163        let layout = make_layout();
164
165        // Move inside
166        let result = state.handle_mouse(mouse_move(5, 0), &layout);
167        assert_eq!(result, Some(ButtonEvent::Hovered));
168        assert_eq!(state.focus, FocusState::Hovered);
169
170        // Move outside
171        let result = state.handle_mouse(mouse_move(15, 0), &layout);
172        assert_eq!(result, Some(ButtonEvent::Left));
173        assert_eq!(state.focus, FocusState::Normal);
174    }
175
176    #[test]
177    fn test_disabled_button_ignores_input() {
178        let mut state = ButtonState::new("Test").with_focus(FocusState::Disabled);
179        let layout = make_layout();
180
181        let result = state.handle_mouse(mouse_down(5, 0), &layout);
182        assert!(result.is_none());
183        assert!(!state.pressed);
184    }
185
186    #[test]
187    fn test_keyboard_activation() {
188        let mut state = ButtonState::new("Test").with_focus(FocusState::Focused);
189
190        let enter = crossterm::event::KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
191        let result = state.handle_key(enter);
192        assert_eq!(result, Some(ButtonEvent::Clicked));
193
194        let space = crossterm::event::KeyEvent::new(KeyCode::Char(' '), KeyModifiers::empty());
195        let result = state.handle_key(space);
196        assert_eq!(result, Some(ButtonEvent::Clicked));
197    }
198
199    #[test]
200    fn test_unfocused_button_ignores_keyboard() {
201        let mut state = ButtonState::new("Test"); // Normal focus
202
203        let enter = crossterm::event::KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
204        let result = state.handle_key(enter);
205        assert!(result.is_none());
206    }
207}