Skip to main content

fresh/view/controls/toggle/
input.rs

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