fresh/view/
popup_input.rs1use super::popup::PopupManager;
7use crate::input::handler::{DeferredAction, InputContext, InputHandler, InputResult};
8use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
9
10impl InputHandler for PopupManager {
11 fn handle_key_event(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
12 if !self.is_visible() {
14 return InputResult::Ignored;
15 }
16
17 match event.code {
18 KeyCode::Enter => {
20 ctx.defer(DeferredAction::ConfirmPopup);
21 InputResult::Consumed
22 }
23 KeyCode::Esc => {
24 ctx.defer(DeferredAction::ClosePopup);
25 InputResult::Consumed
26 }
27
28 KeyCode::Up | KeyCode::Char('k') if event.modifiers.is_empty() => {
30 if let Some(popup) = self.top_mut() {
31 popup.select_prev();
32 }
33 InputResult::Consumed
34 }
35 KeyCode::Down | KeyCode::Char('j') if event.modifiers.is_empty() => {
36 if let Some(popup) = self.top_mut() {
37 popup.select_next();
38 }
39 InputResult::Consumed
40 }
41 KeyCode::PageUp => {
42 if let Some(popup) = self.top_mut() {
43 popup.page_up();
44 }
45 InputResult::Consumed
46 }
47 KeyCode::PageDown => {
48 if let Some(popup) = self.top_mut() {
49 popup.page_down();
50 }
51 InputResult::Consumed
52 }
53 KeyCode::Home => {
54 if let Some(popup) = self.top_mut() {
55 popup.select_first();
56 }
57 InputResult::Consumed
58 }
59 KeyCode::End => {
60 if let Some(popup) = self.top_mut() {
61 popup.select_last();
62 }
63 InputResult::Consumed
64 }
65
66 KeyCode::Tab if event.modifiers.is_empty() => {
68 if let Some(popup) = self.top_mut() {
69 popup.select_next();
70 }
71 InputResult::Consumed
72 }
73 KeyCode::BackTab => {
74 if let Some(popup) = self.top_mut() {
75 popup.select_prev();
76 }
77 InputResult::Consumed
78 }
79
80 KeyCode::Char(c) if event.modifiers.is_empty() => {
82 if self.is_completion_popup() {
84 ctx.defer(DeferredAction::PopupTypeChar(c));
85 }
86 InputResult::Consumed
87 }
88
89 KeyCode::Backspace if event.modifiers.is_empty() => {
91 if self.is_completion_popup() {
92 ctx.defer(DeferredAction::PopupBackspace);
93 }
94 InputResult::Consumed
95 }
96
97 KeyCode::Char('c') if event.modifiers == KeyModifiers::CONTROL => {
99 if let Some(popup) = self.top() {
100 if popup.has_selection() {
101 if let Some(text) = popup.get_selected_text() {
102 ctx.defer(DeferredAction::CopyToClipboard(text));
103 }
104 }
105 }
106 InputResult::Consumed
107 }
108
109 _ => InputResult::Consumed,
111 }
112 }
113
114 fn is_modal(&self) -> bool {
115 self.is_visible()
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::view::popup::{Popup, PopupListItem};
123 use crate::view::theme;
124 use crate::view::theme::Theme;
125 use crossterm::event::KeyModifiers;
126
127 fn key(code: KeyCode) -> KeyEvent {
128 KeyEvent::new(code, KeyModifiers::NONE)
129 }
130
131 fn create_popup_with_items(count: usize) -> PopupManager {
132 let theme = Theme::load_builtin(theme::THEME_DARK).unwrap();
133 let items: Vec<PopupListItem> = (0..count)
134 .map(|i| PopupListItem::new(format!("Item {}", i)))
135 .collect();
136 let popup = Popup::list(items, &theme);
137 let mut manager = PopupManager::new();
138 manager.show(popup);
139 manager
140 }
141
142 #[test]
143 fn test_popup_navigation() {
144 let mut manager = create_popup_with_items(5);
145 let mut ctx = InputContext::new();
146
147 assert_eq!(
149 manager.top().unwrap().selected_item().unwrap().text,
150 "Item 0"
151 );
152
153 manager.handle_key_event(&key(KeyCode::Down), &mut ctx);
155 assert_eq!(
156 manager.top().unwrap().selected_item().unwrap().text,
157 "Item 1"
158 );
159
160 manager.handle_key_event(&key(KeyCode::Up), &mut ctx);
162 assert_eq!(
163 manager.top().unwrap().selected_item().unwrap().text,
164 "Item 0"
165 );
166
167 manager.handle_key_event(
169 &KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE),
170 &mut ctx,
171 );
172 assert_eq!(
173 manager.top().unwrap().selected_item().unwrap().text,
174 "Item 1"
175 );
176
177 manager.handle_key_event(
178 &KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE),
179 &mut ctx,
180 );
181 assert_eq!(
182 manager.top().unwrap().selected_item().unwrap().text,
183 "Item 0"
184 );
185 }
186
187 #[test]
188 fn test_popup_enter_confirms() {
189 let mut manager = create_popup_with_items(3);
190 let mut ctx = InputContext::new();
191
192 manager.handle_key_event(&key(KeyCode::Enter), &mut ctx);
193 assert!(ctx
194 .deferred_actions
195 .iter()
196 .any(|a| matches!(a, DeferredAction::ConfirmPopup)));
197 }
198
199 #[test]
200 fn test_popup_escape_cancels() {
201 let mut manager = create_popup_with_items(3);
202 let mut ctx = InputContext::new();
203
204 manager.handle_key_event(&key(KeyCode::Esc), &mut ctx);
205 assert!(ctx
206 .deferred_actions
207 .iter()
208 .any(|a| matches!(a, DeferredAction::ClosePopup)));
209 }
210
211 #[test]
212 fn test_popup_is_modal_when_visible() {
213 let mut manager = PopupManager::new();
214 assert!(!manager.is_modal());
215
216 let theme = Theme::load_builtin(theme::THEME_DARK).unwrap();
217 manager.show(Popup::text(vec!["test".to_string()], &theme));
218 assert!(manager.is_modal());
219 }
220
221 #[test]
222 fn test_popup_ignored_when_empty() {
223 let mut manager = PopupManager::new();
224 let mut ctx = InputContext::new();
225
226 let result = manager.handle_key_event(&key(KeyCode::Down), &mut ctx);
227 assert_eq!(result, InputResult::Ignored);
228 }
229}