fresh/view/ui/
menu_input.rs1use super::menu::MenuState;
7use crate::config::Menu;
8use crate::input::handler::{DeferredAction, InputContext, InputHandler, InputResult};
9use crossterm::event::{KeyCode, KeyEvent};
10
11pub struct MenuInputHandler<'a> {
13 pub state: &'a mut MenuState,
14 pub menus: &'a [Menu],
15}
16
17impl<'a> MenuInputHandler<'a> {
18 pub fn new(state: &'a mut MenuState, menus: &'a [Menu]) -> Self {
19 Self { state, menus }
20 }
21}
22
23impl InputHandler for MenuInputHandler<'_> {
24 fn handle_key_event(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
25 if self.state.active_menu.is_none() {
27 return InputResult::Ignored;
28 }
29
30 match event.code {
31 KeyCode::Esc => {
33 ctx.defer(DeferredAction::CloseMenu);
34 InputResult::Consumed
35 }
36
37 KeyCode::Enter => {
39 if self.state.is_highlighted_submenu(self.menus) {
41 self.state.open_submenu(self.menus);
42 return InputResult::Consumed;
43 }
44
45 if let Some((action, args)) = self.state.get_highlighted_action(self.menus) {
47 ctx.defer(DeferredAction::ExecuteMenuAction { action, args });
48 ctx.defer(DeferredAction::CloseMenu);
49 }
50 InputResult::Consumed
51 }
52
53 KeyCode::Up | KeyCode::Char('k') if event.modifiers.is_empty() => {
55 if let Some(active_idx) = self.state.active_menu {
56 if let Some(menu) = self.menus.get(active_idx) {
57 self.state.prev_item(menu);
58 }
59 }
60 InputResult::Consumed
61 }
62 KeyCode::Down | KeyCode::Char('j') if event.modifiers.is_empty() => {
63 if let Some(active_idx) = self.state.active_menu {
64 if let Some(menu) = self.menus.get(active_idx) {
65 self.state.next_item(menu);
66 }
67 }
68 InputResult::Consumed
69 }
70 KeyCode::Left | KeyCode::Char('h') if event.modifiers.is_empty() => {
71 if !self.state.close_submenu() {
74 self.state.prev_menu(self.menus);
75 }
76 InputResult::Consumed
77 }
78 KeyCode::Right | KeyCode::Char('l') if event.modifiers.is_empty() => {
79 if !self.state.open_submenu(self.menus) {
82 self.state.next_menu(self.menus);
83 }
84 InputResult::Consumed
85 }
86
87 KeyCode::Home => {
89 self.state.highlighted_item = Some(0);
90 InputResult::Consumed
91 }
92 KeyCode::End => {
93 if let Some(active_idx) = self.state.active_menu {
94 if let Some(menu) = self.menus.get(active_idx) {
95 if let Some(items) = self.state.get_current_items_cloned(menu) {
96 if !items.is_empty() {
97 self.state.highlighted_item = Some(items.len() - 1);
98 }
99 }
100 }
101 }
102 InputResult::Consumed
103 }
104
105 _ => InputResult::Consumed,
107 }
108 }
109
110 fn is_modal(&self) -> bool {
111 self.state.active_menu.is_some()
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::config::MenuItem;
119 use crossterm::event::KeyModifiers;
120 use std::collections::HashMap;
121
122 fn key(code: KeyCode) -> KeyEvent {
123 KeyEvent::new(code, KeyModifiers::NONE)
124 }
125
126 fn create_test_menus() -> Vec<Menu> {
127 vec![
128 Menu {
129 id: None,
130 label: "File".to_string(),
131 items: vec![
132 MenuItem::Action {
133 label: "New".to_string(),
134 action: "new_file".to_string(),
135 args: HashMap::new(),
136 when: None,
137 checkbox: None,
138 },
139 MenuItem::Separator { separator: true },
140 MenuItem::Action {
141 label: "Save".to_string(),
142 action: "save".to_string(),
143 args: HashMap::new(),
144 when: None,
145 checkbox: None,
146 },
147 ],
148 when: None,
149 },
150 Menu {
151 id: None,
152 label: "Edit".to_string(),
153 items: vec![
154 MenuItem::Action {
155 label: "Undo".to_string(),
156 action: "undo".to_string(),
157 args: HashMap::new(),
158 when: None,
159 checkbox: None,
160 },
161 MenuItem::Action {
162 label: "Redo".to_string(),
163 action: "redo".to_string(),
164 args: HashMap::new(),
165 when: None,
166 checkbox: None,
167 },
168 ],
169 when: None,
170 },
171 ]
172 }
173
174 #[test]
175 fn test_menu_navigation_down() {
176 let menus = create_test_menus();
177 let mut state = MenuState::for_testing();
178 state.open_menu(0);
179
180 let mut handler = MenuInputHandler::new(&mut state, &menus);
181 let mut ctx = InputContext::new();
182
183 assert_eq!(handler.state.highlighted_item, Some(0));
185
186 handler.handle_key_event(&key(KeyCode::Down), &mut ctx);
188 assert_eq!(handler.state.highlighted_item, Some(2)); }
190
191 #[test]
192 fn test_menu_navigation_between_menus() {
193 let menus = create_test_menus();
194 let mut state = MenuState::for_testing();
195 state.open_menu(0);
196
197 let mut handler = MenuInputHandler::new(&mut state, &menus);
198 let mut ctx = InputContext::new();
199
200 assert_eq!(handler.state.active_menu, Some(0));
202
203 handler.handle_key_event(&key(KeyCode::Right), &mut ctx);
205 assert_eq!(handler.state.active_menu, Some(1));
206
207 handler.handle_key_event(&key(KeyCode::Left), &mut ctx);
209 assert_eq!(handler.state.active_menu, Some(0));
210 }
211
212 #[test]
213 fn test_menu_escape_closes() {
214 let menus = create_test_menus();
215 let mut state = MenuState::for_testing();
216 state.open_menu(0);
217
218 let mut handler = MenuInputHandler::new(&mut state, &menus);
219 let mut ctx = InputContext::new();
220
221 handler.handle_key_event(&key(KeyCode::Esc), &mut ctx);
222 assert!(ctx
223 .deferred_actions
224 .iter()
225 .any(|a| matches!(a, DeferredAction::CloseMenu)));
226 }
227
228 #[test]
229 fn test_menu_enter_executes() {
230 let menus = create_test_menus();
231 let mut state = MenuState::for_testing();
232 state.open_menu(0);
233 state.highlighted_item = Some(0); let mut handler = MenuInputHandler::new(&mut state, &menus);
236 let mut ctx = InputContext::new();
237
238 handler.handle_key_event(&key(KeyCode::Enter), &mut ctx);
239 assert!(ctx.deferred_actions.iter().any(|a| matches!(
240 a,
241 DeferredAction::ExecuteMenuAction { action, .. } if action == "new_file"
242 )));
243 }
244
245 #[test]
246 fn test_menu_is_modal_when_active() {
247 let menus = create_test_menus();
248 let mut state = MenuState::for_testing();
249
250 let handler = MenuInputHandler::new(&mut state, &menus);
251 assert!(!handler.is_modal());
252
253 state.open_menu(0);
254 let handler = MenuInputHandler::new(&mut state, &menus);
255 assert!(handler.is_modal());
256 }
257
258 #[test]
259 fn test_menu_ignored_when_inactive() {
260 let menus = create_test_menus();
261 let mut state = MenuState::for_testing();
262
263 let mut handler = MenuInputHandler::new(&mut state, &menus);
264 let mut ctx = InputContext::new();
265
266 let result = handler.handle_key_event(&key(KeyCode::Down), &mut ctx);
267 assert_eq!(result, InputResult::Ignored);
268 }
269}