//! Adaptive poll-rate event loop: polls faster for IDLE_THRESHOLD_MS //! after any activity, then slows down to conserve CPU. //! //! Flow: `mark_active()` sets a fast-poll deadline; `poll_interval()` //! checks if the deadline is still in the future and returns either //! `FAST_POLL_MS` (8 ms) or `SLOW_POLL_MS` (100 ms). `is_idle()` reports //! whether the deadline has expired. use std::collections::VecDeque; use std::time::{Duration, Instant}; use zesdex_utils::CastOr; use crate::app::state::runtime::TurnEvent; use tracing; const FAST_POLL_MS: u64 = 8; const SLOW_POLL_MS: u64 = 100; const IDLE_THRESHOLD_MS: u64 = 500; /// Tracks whether the app has been active vs idle to adjust the TUI poll /// rate, balancing responsiveness against CPU usage. pub struct EventLoop { last_activity: Instant, fast_poll_until: Option, } impl EventLoop { /// Create an `EventLoop` with the current instant as the last activity. pub fn new() -> Self { EventLoop { last_activity: Instant::now(), fast_poll_until: None, } } /// Return the appropriate polling delay based on activity state. /// /// Flow: if `fast_poll_until` is set and the deadline hasn't expired, /// return `FAST_POLL_MS`; otherwise return `SLOW_POLL_MS`. pub fn poll_interval(&self) -> Duration { if let Some(fast_until) = self.fast_poll_until { if Instant::now() < fast_until { return Duration::from_millis(FAST_POLL_MS); } } Duration::from_millis(SLOW_POLL_MS) } /// Mark the current time as the last activity and arm the fast-poll /// window for the next `IDLE_THRESHOLD_MS`. /// /// Called by the event loop whenever a TurnEvent arrives, keeping the /// UI responsive during bursts of activity. pub fn mark_active(&mut self) { self.last_activity = Instant::now(); let deadline = Duration::from_millis(IDLE_THRESHOLD_MS); self.fast_poll_until = Some(Instant::now() + deadline); tracing::debug!( "[event-loop] marked active — fast-poll armed for next {}ms", IDLE_THRESHOLD_MS, ); } /// Return `true` if the app has been idle for more than `IDLE_THRESHOLD_MS`. pub fn is_idle(&self) -> bool { let elapsed: u64 = self.last_activity.elapsed().as_millis().cast_or(u64::MAX); elapsed > IDLE_THRESHOLD_MS } /// Drain all pending `TurnEvent`s from the shared mutex queue. /// /// Flow: acquire the mutex lock → drain the VecDeque into a Vec → release. /// Returns an empty Vec if the lock is poisoned. /// /// Return: a `Vec` of all events that were in the queue (may be empty). pub fn drain_events( events: &std::sync::Mutex>, ) -> Vec { let drained: Vec = events .lock() .map(|mut q| q.drain(..).collect()) .unwrap_or_default(); if !drained.is_empty() { tracing::debug!( "[event-loop] drained {} event(s)", drained.len(), ); } drained } } impl Default for EventLoop { fn default() -> Self { Self::new() } }