hojicha_runtime/program/
event_processor.rs1use crossterm::event::{Event as CrosstermEvent, KeyEventKind};
4use hojicha_core::event::{Event, KeyEvent};
5use std::sync::mpsc;
6use std::time::Duration;
7
8pub struct EventProcessor;
10
11impl EventProcessor {
12 pub fn process_crossterm_event(event: CrosstermEvent) -> Option<Event<()>> {
14 match event {
15 CrosstermEvent::Key(key) if key.kind == KeyEventKind::Press => {
16 Some(Event::Key(key.into()))
17 }
18 CrosstermEvent::Mouse(mouse) => Some(Event::Mouse(mouse.into())),
19 CrosstermEvent::Resize(width, height) => Some(Event::Resize { width, height }),
20 CrosstermEvent::Paste(data) => Some(Event::Paste(data)),
21 CrosstermEvent::FocusGained => Some(Event::Focus),
22 CrosstermEvent::FocusLost => Some(Event::Blur),
23 _ => None,
24 }
25 }
26
27 pub fn coalesce_resize_events(
29 initial_width: u16,
30 initial_height: u16,
31 rx: &mpsc::Receiver<CrosstermEvent>,
32 ) -> (u16, u16) {
33 let mut width = initial_width;
34 let mut height = initial_height;
35
36 while let Ok(CrosstermEvent::Resize(w, h)) = rx.try_recv() {
38 width = w;
39 height = h;
40 }
41
42 (width, height)
43 }
44
45 pub fn is_quit_event<M>(event: &Event<M>) -> bool {
47 if let Event::Key(KeyEvent {
48 key: hojicha_core::event::Key::Char('q'),
49 modifiers,
50 }) = event
51 {
52 return modifiers.contains(crossterm::event::KeyModifiers::CONTROL);
53 }
54 false
55 }
56
57 pub fn is_suspend_event<M>(event: &Event<M>) -> bool {
59 if let Event::Key(KeyEvent {
60 key: hojicha_core::event::Key::Char('z'),
61 modifiers,
62 }) = event
63 {
64 return modifiers.contains(crossterm::event::KeyModifiers::CONTROL);
65 }
66 false
67 }
68
69 pub fn prioritize_events<M>(
71 message_rx: &mpsc::Receiver<Event<M>>,
72 crossterm_rx: &mpsc::Receiver<CrosstermEvent>,
73 tick_rate: Duration,
74 ) -> Option<Event<M>>
75 where
76 M: Clone,
77 {
78 if let Ok(msg) = message_rx.try_recv() {
80 return Some(msg);
81 }
82
83 match crossterm_rx.recv_timeout(tick_rate) {
85 Ok(ct_event) => {
86 if let CrosstermEvent::Resize(width, height) = ct_event {
88 let (final_width, final_height) =
89 Self::coalesce_resize_events(width, height, crossterm_rx);
90 return Some(Event::Resize {
91 width: final_width,
92 height: final_height,
93 });
94 }
95
96 Self::process_crossterm_event(ct_event)
98 .map(|e| unsafe { std::mem::transmute_copy(&e) })
99 }
100 Err(mpsc::RecvTimeoutError::Timeout) => {
101 if let Ok(msg) = message_rx.try_recv() {
103 Some(msg)
104 } else {
105 Some(Event::Tick)
106 }
107 }
108 _ => None,
109 }
110 }
111
112 pub fn prioritize_events_headless<M>(
114 message_rx: &mpsc::Receiver<Event<M>>,
115 tick_rate: Duration,
116 ) -> Option<Event<M>>
117 where
118 M: Clone,
119 {
120 match message_rx.recv_timeout(tick_rate) {
122 Ok(msg) => Some(msg),
123 Err(mpsc::RecvTimeoutError::Timeout) => Some(Event::Tick),
124 _ => None,
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crossterm::event::{KeyCode, KeyModifiers, MouseButton, MouseEventKind};
133
134 #[test]
135 fn test_process_key_event() {
136 let key_event = CrosstermEvent::Key(crossterm::event::KeyEvent {
137 code: KeyCode::Char('a'),
138 modifiers: KeyModifiers::empty(),
139 kind: KeyEventKind::Press,
140 state: crossterm::event::KeyEventState::empty(),
141 });
142
143 let result = EventProcessor::process_crossterm_event(key_event);
144 assert!(result.is_some());
145 if let Some(Event::Key(key)) = result {
146 assert_eq!(key.key, hojicha_core::event::Key::Char('a'));
147 } else {
148 panic!("Expected Key event");
149 }
150 }
151
152 #[test]
153 fn test_process_mouse_event() {
154 let mouse_event = CrosstermEvent::Mouse(crossterm::event::MouseEvent {
155 kind: MouseEventKind::Down(MouseButton::Left),
156 column: 10,
157 row: 20,
158 modifiers: KeyModifiers::empty(),
159 });
160
161 let result = EventProcessor::process_crossterm_event(mouse_event);
162 assert!(result.is_some());
163 if let Some(Event::Mouse(mouse)) = result {
164 assert_eq!(mouse.column, 10);
165 assert_eq!(mouse.row, 20);
166 } else {
167 panic!("Expected Mouse event");
168 }
169 }
170
171 #[test]
172 fn test_process_resize_event() {
173 let resize_event = CrosstermEvent::Resize(80, 24);
174
175 let result = EventProcessor::process_crossterm_event(resize_event);
176 assert!(result.is_some());
177 if let Some(Event::Resize { width, height }) = result {
178 assert_eq!(width, 80);
179 assert_eq!(height, 24);
180 } else {
181 panic!("Expected Resize event");
182 }
183 }
184
185 #[test]
186 fn test_process_paste_event() {
187 let paste_event = CrosstermEvent::Paste("test".to_string());
188
189 let result = EventProcessor::process_crossterm_event(paste_event);
190 assert!(result.is_some());
191 if let Some(Event::Paste(text)) = result {
192 assert_eq!(text, "test");
193 } else {
194 panic!("Expected Paste event");
195 }
196 }
197
198 #[test]
199 fn test_process_focus_events() {
200 let focus_gained = CrosstermEvent::FocusGained;
201 let result = EventProcessor::process_crossterm_event(focus_gained);
202 assert!(matches!(result, Some(Event::Focus)));
203
204 let focus_lost = CrosstermEvent::FocusLost;
205 let result = EventProcessor::process_crossterm_event(focus_lost);
206 assert!(matches!(result, Some(Event::Blur)));
207 }
208
209 #[test]
210 fn test_coalesce_resize_events() {
211 let (tx, rx) = mpsc::channel();
212
213 tx.send(CrosstermEvent::Resize(100, 30)).unwrap();
215 tx.send(CrosstermEvent::Resize(110, 35)).unwrap();
216 tx.send(CrosstermEvent::Resize(120, 40)).unwrap();
217
218 let (width, height) = EventProcessor::coalesce_resize_events(80, 24, &rx);
219
220 assert_eq!(width, 120);
222 assert_eq!(height, 40);
223 }
224
225 #[test]
226 fn test_is_quit_event() {
227 let quit_event: Event<()> = Event::Key(KeyEvent {
228 key: hojicha_core::event::Key::Char('q'),
229 modifiers: KeyModifiers::CONTROL,
230 });
231 assert!(EventProcessor::is_quit_event(&quit_event));
232
233 let non_quit_event: Event<()> = Event::Key(KeyEvent {
234 key: hojicha_core::event::Key::Char('q'),
235 modifiers: KeyModifiers::empty(),
236 });
237 assert!(!EventProcessor::is_quit_event(&non_quit_event));
238 }
239
240 #[test]
241 fn test_is_suspend_event() {
242 let suspend_event: Event<()> = Event::Key(KeyEvent {
243 key: hojicha_core::event::Key::Char('z'),
244 modifiers: KeyModifiers::CONTROL,
245 });
246 assert!(EventProcessor::is_suspend_event(&suspend_event));
247
248 let non_suspend_event: Event<()> = Event::Key(KeyEvent {
249 key: hojicha_core::event::Key::Char('z'),
250 modifiers: KeyModifiers::empty(),
251 });
252 assert!(!EventProcessor::is_suspend_event(&non_suspend_event));
253 }
254}