fresh/view/controls/dropdown/
input.rs1use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
4
5use super::{DropdownLayout, DropdownState, FocusState};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum DropdownEvent {
10 Opened,
12 Closed,
14 SelectionChanged(usize),
16 Cancelled,
18 Hovered,
20 Left,
22}
23
24impl DropdownState {
25 pub fn handle_mouse(
35 &mut self,
36 event: MouseEvent,
37 layout: &DropdownLayout,
38 ) -> Option<DropdownEvent> {
39 if !self.is_enabled() {
40 return None;
41 }
42
43 match event.kind {
44 MouseEventKind::Down(MouseButton::Left) => {
45 if self.open {
46 if let Some(index) = layout.option_at(event.column, event.row) {
48 self.select(index);
49 return Some(DropdownEvent::SelectionChanged(index));
50 }
51 if layout.is_button(event.column, event.row) {
53 self.toggle_open();
54 return Some(DropdownEvent::Closed);
55 }
56 self.cancel();
58 return Some(DropdownEvent::Cancelled);
59 } else {
60 if layout.is_button(event.column, event.row) {
62 self.toggle_open();
63 return Some(DropdownEvent::Opened);
64 }
65 }
66 None
67 }
68 MouseEventKind::Moved => {
69 let inside = layout.is_button(event.column, event.row)
70 || layout.option_at(event.column, event.row).is_some();
71
72 if inside {
73 if self.focus != FocusState::Focused && self.focus != FocusState::Hovered {
74 self.focus = FocusState::Hovered;
75 }
76 Some(DropdownEvent::Hovered)
77 } else if self.focus == FocusState::Hovered && !self.open {
78 self.focus = FocusState::Normal;
79 Some(DropdownEvent::Left)
80 } else {
81 None
82 }
83 }
84 MouseEventKind::ScrollUp => {
85 if self.open {
86 self.scroll_by(-3);
87 Some(DropdownEvent::SelectionChanged(self.selected))
88 } else {
89 None
90 }
91 }
92 MouseEventKind::ScrollDown => {
93 if self.open {
94 self.scroll_by(3);
95 Some(DropdownEvent::SelectionChanged(self.selected))
96 } else {
97 None
98 }
99 }
100 _ => None,
101 }
102 }
103
104 pub fn handle_key(&mut self, key: KeyEvent) -> Option<DropdownEvent> {
110 if !self.is_enabled() {
111 return None;
112 }
113
114 if self.focus != FocusState::Focused && !self.open {
116 return None;
117 }
118
119 match key.code {
120 KeyCode::Enter | KeyCode::Char(' ') => {
121 if self.open {
122 self.confirm();
123 Some(DropdownEvent::Closed)
124 } else {
125 self.toggle_open();
126 Some(DropdownEvent::Opened)
127 }
128 }
129 KeyCode::Esc => {
130 if self.open {
131 self.cancel();
132 Some(DropdownEvent::Cancelled)
133 } else {
134 None
135 }
136 }
137 KeyCode::Up | KeyCode::Char('k') => {
138 self.select_prev();
139 Some(DropdownEvent::SelectionChanged(self.selected))
140 }
141 KeyCode::Down | KeyCode::Char('j') => {
142 self.select_next();
143 Some(DropdownEvent::SelectionChanged(self.selected))
144 }
145 KeyCode::Home => {
146 if !self.options.is_empty() {
147 self.selected = 0;
148 self.ensure_visible();
149 Some(DropdownEvent::SelectionChanged(0))
150 } else {
151 None
152 }
153 }
154 KeyCode::End => {
155 if !self.options.is_empty() {
156 self.selected = self.options.len() - 1;
157 self.ensure_visible();
158 Some(DropdownEvent::SelectionChanged(self.selected))
159 } else {
160 None
161 }
162 }
163 _ => None,
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crossterm::event::KeyModifiers;
172 use ratatui::layout::Rect;
173
174 fn make_layout(open: bool) -> DropdownLayout {
175 let mut layout = DropdownLayout {
176 button_area: Rect::new(10, 0, 15, 1),
177 option_areas: Vec::new(),
178 full_area: Rect::new(0, 0, 25, 1),
179 scroll_offset: 0,
180 };
181 if open {
182 layout.option_areas = vec![
183 Rect::new(10, 1, 15, 1),
184 Rect::new(10, 2, 15, 1),
185 Rect::new(10, 3, 15, 1),
186 ];
187 }
188 layout
189 }
190
191 fn mouse_down(x: u16, y: u16) -> MouseEvent {
192 MouseEvent {
193 kind: MouseEventKind::Down(MouseButton::Left),
194 column: x,
195 row: y,
196 modifiers: KeyModifiers::empty(),
197 }
198 }
199
200 #[test]
201 fn test_click_opens() {
202 let mut state = DropdownState::new(
203 vec!["A".to_string(), "B".to_string(), "C".to_string()],
204 "Test",
205 );
206 let layout = make_layout(false);
207
208 let result = state.handle_mouse(mouse_down(12, 0), &layout);
209 assert_eq!(result, Some(DropdownEvent::Opened));
210 assert!(state.open);
211 }
212
213 #[test]
214 fn test_click_option_selects() {
215 let mut state = DropdownState::new(
216 vec!["A".to_string(), "B".to_string(), "C".to_string()],
217 "Test",
218 );
219 state.open = true;
220 let layout = make_layout(true);
221
222 let result = state.handle_mouse(mouse_down(12, 2), &layout);
223 assert_eq!(result, Some(DropdownEvent::SelectionChanged(1)));
224 assert_eq!(state.selected, 1);
225 assert!(!state.open);
226 }
227
228 #[test]
229 fn test_click_outside_cancels() {
230 let mut state = DropdownState::new(
231 vec!["A".to_string(), "B".to_string(), "C".to_string()],
232 "Test",
233 )
234 .with_selected(1);
235 state.toggle_open();
236 state.select_next();
237 assert_eq!(state.selected, 2);
238
239 let layout = make_layout(true);
240
241 let result = state.handle_mouse(mouse_down(0, 5), &layout);
242 assert_eq!(result, Some(DropdownEvent::Cancelled));
243 assert!(!state.open);
244 assert_eq!(state.selected, 1); }
246
247 #[test]
248 fn test_keyboard_navigation() {
249 let mut state = DropdownState::new(
250 vec!["A".to_string(), "B".to_string(), "C".to_string()],
251 "Test",
252 )
253 .with_focus(FocusState::Focused);
254
255 let down = KeyEvent::new(KeyCode::Down, KeyModifiers::empty());
256 let result = state.handle_key(down);
257 assert_eq!(result, Some(DropdownEvent::SelectionChanged(1)));
258
259 let up = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
260 let result = state.handle_key(up);
261 assert_eq!(result, Some(DropdownEvent::SelectionChanged(0)));
262 }
263
264 #[test]
265 fn test_enter_toggles() {
266 let mut state = DropdownState::new(vec!["A".to_string(), "B".to_string()], "Test")
267 .with_focus(FocusState::Focused);
268
269 let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
270 let result = state.handle_key(enter);
271 assert_eq!(result, Some(DropdownEvent::Opened));
272 assert!(state.open);
273
274 let result = state.handle_key(enter);
275 assert_eq!(result, Some(DropdownEvent::Closed));
276 assert!(!state.open);
277 }
278
279 #[test]
280 fn test_escape_cancels() {
281 let mut state = DropdownState::new(
282 vec!["A".to_string(), "B".to_string(), "C".to_string()],
283 "Test",
284 )
285 .with_focus(FocusState::Focused);
286
287 state.toggle_open();
288 state.select_next();
289 assert_eq!(state.selected, 1);
290
291 let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
292 let result = state.handle_key(esc);
293 assert_eq!(result, Some(DropdownEvent::Cancelled));
294 assert!(!state.open);
295 assert_eq!(state.selected, 0); }
297}