use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
use crate::hooks::use_input::{clear_input_handlers, dispatch_key_event};
use crate::hooks::use_mouse::dispatch_mouse_event;
use crate::renderer::Terminal;
use super::filter::FilterChain;
use super::frame_rate::FrameRateController;
use super::registry::{AppRuntime, AppSink};
pub(crate) struct EventLoop {
runtime: Arc<AppRuntime>,
should_exit: Arc<AtomicBool>,
frame_rate: FrameRateController,
exit_on_ctrl_c: bool,
filter_chain: FilterChain,
cancel_flag: Option<Arc<AtomicBool>>,
render_rx: Option<mpsc::UnboundedReceiver<()>>,
}
impl EventLoop {
pub(crate) fn with_filters(
runtime: Arc<AppRuntime>,
should_exit: Arc<AtomicBool>,
frame_rate: FrameRateController,
exit_on_ctrl_c: bool,
filter_chain: FilterChain,
) -> Self {
Self {
runtime,
should_exit,
frame_rate,
exit_on_ctrl_c,
filter_chain,
cancel_flag: None,
render_rx: None,
}
}
pub(crate) fn with_cancel_flag(mut self, flag: Arc<AtomicBool>) -> Self {
self.cancel_flag = Some(flag);
self
}
pub(crate) fn with_render_rx(mut self, rx: mpsc::UnboundedReceiver<()>) -> Self {
self.render_rx = Some(rx);
self
}
pub(crate) fn run<F>(&mut self, mut on_render: F) -> std::io::Result<()>
where
F: FnMut() -> std::io::Result<()>,
{
let start = Instant::now();
on_render()?;
self.frame_rate.record_frame(start.elapsed());
loop {
if let Some(event) = Terminal::poll_event(Duration::from_millis(10))? {
if let Some(filtered_event) = self.filter_chain.apply(event) {
self.handle_event(filtered_event);
}
}
if self.should_exit.load(Ordering::SeqCst) {
break;
}
if let Some(ref cancel_flag) = self.cancel_flag {
if cancel_flag.load(Ordering::SeqCst) {
self.should_exit.store(true, Ordering::SeqCst);
break;
}
}
if self.runtime.suspend_requested() {
return Ok(());
}
self.drain_render_notifications();
let render_requested = self.runtime.render_requested();
let time_elapsed = self.frame_rate.should_render();
if render_requested && time_elapsed {
self.runtime.clear_render_request();
let start = Instant::now();
on_render()?;
self.frame_rate.record_frame(start.elapsed());
}
}
clear_input_handlers();
Ok(())
}
fn handle_event(&mut self, event: Event) {
match event {
Event::Key(key_event) => {
if key_event.kind != KeyEventKind::Press {
return;
}
if self.exit_on_ctrl_c && Terminal::is_ctrl_c(&Event::Key(key_event)) {
self.should_exit.store(true, Ordering::SeqCst);
return;
}
#[cfg(unix)]
if key_event.modifiers.contains(KeyModifiers::CONTROL)
&& key_event.code == KeyCode::Char('z')
{
self.runtime.request_suspend();
return;
}
dispatch_key_event(&key_event);
self.runtime.request_render();
}
Event::Mouse(mouse_event) => {
dispatch_mouse_event(&mouse_event);
self.runtime.request_render();
}
Event::Resize(_new_width, _new_height) => {
self.runtime.request_render();
}
_ => {}
}
}
fn drain_render_notifications(&mut self) {
if let Some(rx) = self.render_rx.as_mut() {
let mut requested = false;
while rx.try_recv().is_ok() {
requested = true;
}
if requested {
self.runtime.request_render();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hooks::use_input::{clear_input_handlers, register_input_handler};
use crate::hooks::use_mouse::{clear_mouse_handlers, register_mouse_handler};
use crate::renderer::frame_rate::FrameRateConfig;
use crate::renderer::registry::{AppRuntime, AppSink, ModeSwitch, Printable};
use crossterm::event::{KeyEvent, KeyEventKind, MouseEvent, MouseEventKind};
use std::sync::atomic::AtomicBool;
fn create_event_loop(runtime: Arc<AppRuntime>, should_exit: Arc<AtomicBool>) -> EventLoop {
let frame_rate = FrameRateController::new(FrameRateConfig::new(60));
EventLoop::with_filters(runtime, should_exit, frame_rate, true, FilterChain::new())
}
#[test]
fn test_event_loop_creation() {
let runtime = AppRuntime::new(false);
let should_exit = Arc::new(AtomicBool::new(false));
let event_loop = create_event_loop(runtime, should_exit);
assert_eq!(event_loop.frame_rate.current_fps(), 60);
assert!(event_loop.exit_on_ctrl_c);
}
#[test]
fn test_event_loop_mode_switch() {
let runtime = AppRuntime::new(false);
let should_exit = Arc::new(AtomicBool::new(false));
let _event_loop = create_event_loop(runtime.clone(), should_exit);
runtime.enter_alt_screen();
let switch = runtime.take_mode_switch_request();
assert_eq!(switch, Some(ModeSwitch::EnterAltScreen));
}
#[test]
fn test_event_loop_println_messages() {
let runtime = AppRuntime::new(false);
let should_exit = Arc::new(AtomicBool::new(false));
let _event_loop = create_event_loop(runtime.clone(), should_exit);
runtime.println(Printable::Text("test".to_string()));
let messages = runtime.take_println_messages();
assert_eq!(messages.len(), 1);
}
#[test]
fn test_event_loop_exit_flag() {
let runtime = AppRuntime::new(false);
let should_exit = Arc::new(AtomicBool::new(false));
let _event_loop = create_event_loop(runtime, should_exit.clone());
assert!(!should_exit.load(Ordering::SeqCst));
should_exit.store(true, Ordering::SeqCst);
assert!(should_exit.load(Ordering::SeqCst));
}
#[test]
fn test_event_loop_key_dispatch_requests_render() {
let runtime = AppRuntime::new(false);
runtime.clear_render_request();
let should_exit = Arc::new(AtomicBool::new(false));
let mut event_loop = create_event_loop(runtime.clone(), should_exit);
let hit = Arc::new(AtomicBool::new(false));
let hit_clone = hit.clone();
register_input_handler(move |input, _| {
if input == "x" {
hit_clone.store(true, Ordering::SeqCst);
}
});
let event = Event::Key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
event_loop.handle_event(event);
assert!(hit.load(Ordering::SeqCst));
assert!(runtime.render_requested());
clear_input_handlers();
}
#[test]
fn test_event_loop_ignores_key_release_events() {
let runtime = AppRuntime::new(false);
runtime.clear_render_request();
let should_exit = Arc::new(AtomicBool::new(false));
let mut event_loop = create_event_loop(runtime.clone(), should_exit);
let hit = Arc::new(AtomicBool::new(false));
let hit_clone = hit.clone();
register_input_handler(move |_input, _| {
hit_clone.store(true, Ordering::SeqCst);
});
let mut key_event = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
key_event.kind = KeyEventKind::Release;
event_loop.handle_event(Event::Key(key_event));
assert!(!hit.load(Ordering::SeqCst));
assert!(!runtime.render_requested());
clear_input_handlers();
}
#[test]
fn test_event_loop_mouse_dispatch_requests_render() {
let runtime = AppRuntime::new(false);
runtime.clear_render_request();
let should_exit = Arc::new(AtomicBool::new(false));
let mut event_loop = create_event_loop(runtime.clone(), should_exit);
let hit = Arc::new(AtomicBool::new(false));
let hit_clone = hit.clone();
register_mouse_handler(move |_mouse| {
hit_clone.store(true, Ordering::SeqCst);
});
let mouse_event = MouseEvent {
kind: MouseEventKind::Moved,
column: 1,
row: 1,
modifiers: KeyModifiers::NONE,
};
event_loop.handle_event(Event::Mouse(mouse_event));
assert!(hit.load(Ordering::SeqCst));
assert!(runtime.render_requested());
clear_mouse_handlers();
}
#[test]
fn test_event_loop_render_rx_requests_render() {
let runtime = AppRuntime::new(false);
runtime.clear_render_request();
let should_exit = Arc::new(AtomicBool::new(false));
let frame_rate = FrameRateController::new(FrameRateConfig::new(60));
let (tx, rx) = mpsc::unbounded_channel();
let mut event_loop = EventLoop::with_filters(
runtime.clone(),
should_exit,
frame_rate,
true,
FilterChain::new(),
)
.with_render_rx(rx);
tx.send(()).unwrap();
event_loop.drain_render_notifications();
assert!(runtime.render_requested());
}
}