input/
event_handler.rs

1use super::{Event, KeyCode, KeyModifiers};
2use crate::{key_bindings::KeyBindings, InputOptions, KeyEvent, StandardEvent};
3
4/// A handler for reading and processing events.
5#[derive(Debug)]
6pub struct EventHandler<CustomKeybinding: crate::CustomKeybinding, CustomEvent: crate::CustomEvent> {
7	key_bindings: KeyBindings<CustomKeybinding, CustomEvent>,
8}
9
10impl<CustomKeybinding: crate::CustomKeybinding, CustomEvent: crate::CustomEvent>
11	EventHandler<CustomKeybinding, CustomEvent>
12{
13	/// Create a new instance of the `EventHandler`.
14	#[inline]
15	#[must_use]
16	pub const fn new(key_bindings: KeyBindings<CustomKeybinding, CustomEvent>) -> Self {
17		Self { key_bindings }
18	}
19
20	/// Read and handle an event.
21	#[inline]
22	pub fn read_event<F>(
23		&self,
24		event: Event<CustomEvent>,
25		input_options: &InputOptions,
26		callback: F,
27	) -> Event<CustomEvent>
28	where
29		F: FnOnce(Event<CustomEvent>, &KeyBindings<CustomKeybinding, CustomEvent>) -> Event<CustomEvent>,
30	{
31		if event == Event::None {
32			return event;
33		}
34
35		if let Some(e) = Self::handle_standard_inputs(event) {
36			return e;
37		}
38
39		if input_options.contains(InputOptions::RESIZE) {
40			if let Event::Resize(..) = event {
41				return event;
42			}
43		}
44
45		if input_options.contains(InputOptions::MOVEMENT) {
46			if let Some(evt) = Self::handle_movement_inputs(&self.key_bindings, event) {
47				return evt;
48			}
49		}
50
51		if input_options.contains(InputOptions::SEARCH) {
52			if let Some(evt) = Self::handle_search(&self.key_bindings, event) {
53				return evt;
54			}
55		}
56
57		if input_options.contains(InputOptions::HELP) && self.key_bindings.help.contains(&event) {
58			return Event::from(StandardEvent::Help);
59		}
60
61		if input_options.contains(InputOptions::UNDO_REDO) {
62			if let Some(evt) = Self::handle_undo_redo(&self.key_bindings, event) {
63				return evt;
64			}
65		}
66
67		callback(event, &self.key_bindings)
68	}
69
70	#[allow(clippy::wildcard_enum_match_arm)]
71	fn handle_standard_inputs(event: Event<CustomEvent>) -> Option<Event<CustomEvent>> {
72		match event {
73			Event::Key(KeyEvent {
74				code: KeyCode::Char('c'),
75				modifiers: KeyModifiers::CONTROL,
76			}) => Some(Event::from(StandardEvent::Kill)),
77			_ => None,
78		}
79	}
80
81	#[allow(clippy::wildcard_enum_match_arm)]
82	fn handle_movement_inputs(
83		key_bindings: &KeyBindings<CustomKeybinding, CustomEvent>,
84		event: Event<CustomEvent>,
85	) -> Option<Event<CustomEvent>> {
86		Some(match event {
87			e if key_bindings.scroll_down.contains(&e) => Event::from(StandardEvent::ScrollDown),
88			e if key_bindings.scroll_end.contains(&e) => Event::from(StandardEvent::ScrollBottom),
89			e if key_bindings.scroll_home.contains(&e) => Event::from(StandardEvent::ScrollTop),
90			e if key_bindings.scroll_left.contains(&e) => Event::from(StandardEvent::ScrollLeft),
91			e if key_bindings.scroll_right.contains(&e) => Event::from(StandardEvent::ScrollRight),
92			e if key_bindings.scroll_up.contains(&e) => Event::from(StandardEvent::ScrollUp),
93			e if key_bindings.scroll_step_down.contains(&e) => Event::from(StandardEvent::ScrollJumpDown),
94			e if key_bindings.scroll_step_up.contains(&e) => Event::from(StandardEvent::ScrollJumpUp),
95			// these are required, since in some contexts (like editing), other keybindings will not work
96			Event::Key(KeyEvent {
97				code: KeyCode::Up,
98				modifiers: KeyModifiers::NONE,
99			}) => Event::from(StandardEvent::ScrollUp),
100			Event::Key(KeyEvent {
101				code: KeyCode::Down,
102				modifiers: KeyModifiers::NONE,
103			}) => Event::from(StandardEvent::ScrollDown),
104			Event::Key(KeyEvent {
105				code: KeyCode::Left,
106				modifiers: KeyModifiers::NONE,
107			}) => Event::from(StandardEvent::ScrollLeft),
108			Event::Key(KeyEvent {
109				code: KeyCode::Right,
110				modifiers: KeyModifiers::NONE,
111			}) => Event::from(StandardEvent::ScrollRight),
112			Event::Key(KeyEvent {
113				code: KeyCode::PageUp,
114				modifiers: KeyModifiers::NONE,
115			}) => Event::from(StandardEvent::ScrollJumpUp),
116			Event::Key(KeyEvent {
117				code: KeyCode::PageDown,
118				modifiers: KeyModifiers::NONE,
119			}) => Event::from(StandardEvent::ScrollJumpDown),
120			Event::Key(KeyEvent {
121				code: KeyCode::Home,
122				modifiers: KeyModifiers::NONE,
123			}) => Event::from(StandardEvent::ScrollTop),
124			Event::Key(KeyEvent {
125				code: KeyCode::End,
126				modifiers: KeyModifiers::NONE,
127			}) => Event::from(StandardEvent::ScrollBottom),
128			_ => return None,
129		})
130	}
131
132	fn handle_search(
133		key_bindings: &KeyBindings<CustomKeybinding, CustomEvent>,
134		event: Event<CustomEvent>,
135	) -> Option<Event<CustomEvent>> {
136		match event {
137			e if key_bindings.search_next.contains(&e) => Some(Event::from(StandardEvent::SearchNext)),
138			e if key_bindings.search_previous.contains(&e) => Some(Event::from(StandardEvent::SearchPrevious)),
139			e if key_bindings.search_start.contains(&e) => Some(Event::from(StandardEvent::SearchStart)),
140			_ => None,
141		}
142	}
143
144	fn handle_undo_redo(
145		key_bindings: &KeyBindings<CustomKeybinding, CustomEvent>,
146		event: Event<CustomEvent>,
147	) -> Option<Event<CustomEvent>> {
148		if key_bindings.undo.contains(&event) {
149			Some(Event::from(StandardEvent::Undo))
150		}
151		else if key_bindings.redo.contains(&event) {
152			Some(Event::from(StandardEvent::Redo))
153		}
154		else {
155			None
156		}
157	}
158}
159
160#[cfg(test)]
161mod tests {
162	use rstest::rstest;
163
164	use super::*;
165	use crate::{
166		map_keybindings,
167		testutil::local::{create_test_keybindings, Event, EventHandler},
168	};
169
170	#[rstest]
171	#[case::standard(Event::Key(KeyEvent {
172		code: KeyCode::Char('c'),
173		modifiers: KeyModifiers::CONTROL,
174	}), true)]
175	#[case::resize(Event::Resize(100, 100), false)]
176	#[case::movement(Event::from(KeyCode::Up), false)]
177	#[case::undo_redo(Event::Key(KeyEvent {
178		code: KeyCode::Char('z'),
179		modifiers: KeyModifiers::CONTROL,
180	}), false)]
181	#[case::other(Event::from('a'), false)]
182	fn read_event_options_disabled(#[case] event: Event, #[case] handled: bool) {
183		let event_handler = EventHandler::new(create_test_keybindings());
184		let result = event_handler.read_event(event, &InputOptions::empty(), |_, _| Event::from(KeyCode::Null));
185
186		if handled {
187			assert_ne!(result, Event::from(KeyCode::Null));
188		}
189		else {
190			assert_eq!(result, Event::from(KeyCode::Null));
191		}
192	}
193
194	#[rstest]
195	#[case::standard(Event::Key(KeyEvent {
196		code: KeyCode::Char('c'),
197		modifiers: KeyModifiers::CONTROL,
198	}), true)]
199	#[case::resize(Event::Resize(100, 100), true)]
200	#[case::movement(Event::from(KeyCode::Up), true)]
201	#[case::undo_redo(Event::Key(KeyEvent {
202		code: KeyCode::Char('z'),
203		modifiers: KeyModifiers::CONTROL,
204	}), true)]
205	#[case::other(Event::from('a'), false)]
206	fn read_event_enabled(#[case] event: Event, #[case] handled: bool) {
207		let event_handler = EventHandler::new(create_test_keybindings());
208		let result = event_handler.read_event(event, &InputOptions::all(), |_, _| Event::from(KeyCode::Null));
209
210		if handled {
211			assert_ne!(result, Event::from(KeyCode::Null));
212		}
213		else {
214			assert_eq!(result, Event::from(KeyCode::Null));
215		}
216	}
217
218	#[test]
219	fn none_event() {
220		let event_handler = EventHandler::new(create_test_keybindings());
221		let result = event_handler.read_event(Event::None, &InputOptions::empty(), |_, _| Event::from(KeyCode::Null));
222		assert_eq!(result, Event::None);
223	}
224
225	#[rstest]
226	#[case::standard(Event::Key(KeyEvent {
227		code: KeyCode::Char('c'),
228		modifiers: KeyModifiers::CONTROL,
229	}), Event::from(StandardEvent::Kill))]
230	#[case::other(Event::from('a'), Event::from(KeyCode::Null))]
231	fn standard_inputs(#[case] event: Event, #[case] expected: Event) {
232		let event_handler = EventHandler::new(create_test_keybindings());
233		let result = event_handler.read_event(event, &InputOptions::empty(), |_, _| Event::from(KeyCode::Null));
234		assert_eq!(result, expected);
235	}
236
237	#[rstest]
238	#[case::standard(Event::from(KeyCode::Up), Event::from(StandardEvent::ScrollUp))]
239	#[case::standard(Event::from(KeyCode::Down), Event::from(StandardEvent::ScrollDown))]
240	#[case::standard(Event::from(KeyCode::Left), Event::from(StandardEvent::ScrollLeft))]
241	#[case::standard(Event::from(KeyCode::Right), Event::from(StandardEvent::ScrollRight))]
242	#[case::standard(Event::from(KeyCode::PageUp), Event::from(StandardEvent::ScrollJumpUp))]
243	#[case::standard(Event::from(KeyCode::PageDown), Event::from(StandardEvent::ScrollJumpDown))]
244	#[case::standard(Event::from(KeyCode::Home), Event::from(StandardEvent::ScrollTop))]
245	#[case::standard(Event::from(KeyCode::End), Event::from(StandardEvent::ScrollBottom))]
246	#[case::other(Event::from('a'), Event::from(KeyCode::Null))]
247	fn movement_inputs(#[case] event: Event, #[case] expected: Event) {
248		let event_handler = EventHandler::new(create_test_keybindings());
249		let result = event_handler.read_event(event, &InputOptions::MOVEMENT, |_, _| Event::from(KeyCode::Null));
250		assert_eq!(result, expected);
251	}
252
253	#[rstest]
254	#[case::standard(Event::from(KeyCode::Up), Event::from(StandardEvent::ScrollUp))]
255	#[case::standard(Event::from(KeyCode::Down), Event::from(StandardEvent::ScrollDown))]
256	#[case::standard(Event::from(KeyCode::Left), Event::from(StandardEvent::ScrollLeft))]
257	#[case::standard(Event::from(KeyCode::Right), Event::from(StandardEvent::ScrollRight))]
258	#[case::standard(Event::from(KeyCode::PageUp), Event::from(StandardEvent::ScrollJumpUp))]
259	#[case::standard(Event::from(KeyCode::PageDown), Event::from(StandardEvent::ScrollJumpDown))]
260	#[case::standard(Event::from(KeyCode::Home), Event::from(StandardEvent::ScrollTop))]
261	#[case::standard(Event::from(KeyCode::End), Event::from(StandardEvent::ScrollBottom))]
262	#[case::other(Event::from('a'), Event::from(KeyCode::Null))]
263	fn default_movement_inputs(#[case] event: Event, #[case] expected: Event) {
264		let mut bindings = create_test_keybindings();
265		bindings.scroll_down = map_keybindings(&[String::from("x")]);
266		bindings.scroll_end = map_keybindings(&[String::from("x")]);
267		bindings.scroll_home = map_keybindings(&[String::from("x")]);
268		bindings.scroll_left = map_keybindings(&[String::from("x")]);
269		bindings.scroll_right = map_keybindings(&[String::from("x")]);
270		bindings.scroll_up = map_keybindings(&[String::from("x")]);
271		bindings.scroll_step_down = map_keybindings(&[String::from("x")]);
272		bindings.scroll_step_up = map_keybindings(&[String::from("x")]);
273		let event_handler = EventHandler::new(bindings);
274		let result = event_handler.read_event(event, &InputOptions::MOVEMENT, |_, _| Event::from(KeyCode::Null));
275		assert_eq!(result, expected);
276	}
277
278	#[rstest]
279	#[case::search_next(Event::from('n'), Event::from(StandardEvent::SearchNext))]
280	#[case::search_previous(Event::from('N'), Event::from(StandardEvent::SearchPrevious))]
281	#[case::search_start(Event::from('/'), Event::from(StandardEvent::SearchStart))]
282	#[case::other(Event::from('a'), Event::from(KeyCode::Null))]
283	fn search_inputs(#[case] event: Event, #[case] expected: Event) {
284		let event_handler = EventHandler::new(create_test_keybindings());
285		let result = event_handler.read_event(event, &InputOptions::SEARCH, |_, _| Event::from(KeyCode::Null));
286		assert_eq!(result, expected);
287	}
288
289	#[test]
290	fn help_event() {
291		let event_handler = EventHandler::new(create_test_keybindings());
292		let result = event_handler.read_event(Event::from('?'), &InputOptions::HELP, |_, _| Event::from(KeyCode::Null));
293		assert_eq!(result, Event::from(StandardEvent::Help));
294	}
295
296	#[rstest]
297	#[case::standard(Event::Key(KeyEvent {
298		code: KeyCode::Char('z'),
299		modifiers: KeyModifiers::CONTROL,
300	}), Event::from(StandardEvent::Undo))]
301	#[case::standard(Event::Key(KeyEvent {
302		code: KeyCode::Char('y'),
303		modifiers: KeyModifiers::CONTROL,
304	}), Event::from(StandardEvent::Redo))]
305	#[case::other(Event::from('a'), Event::from(KeyCode::Null))]
306	fn undo_redo_inputs(#[case] event: Event, #[case] expected: Event) {
307		let event_handler = EventHandler::new(create_test_keybindings());
308		let result = event_handler.read_event(event, &InputOptions::UNDO_REDO, |_, _| Event::from(KeyCode::Null));
309		assert_eq!(result, expected);
310	}
311}