diff --git a/codex-rs/tui/src/scroll_event_helper.rs b/codex-rs/tui/src/scroll_event_helper.rs new file mode 100644 index 0000000000..9c14cccc93 --- /dev/null +++ b/codex-rs/tui/src/scroll_event_helper.rs @@ -0,0 +1,71 @@ +use std::sync::atomic::AtomicBool; +use std::sync::atomic::AtomicI32; +use std::sync::atomic::Ordering; +use std::sync::mpsc::Sender; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +use crate::app_event::AppEvent; + +pub(crate) struct ScrollEventHelper { + app_event_tx: Sender, + scroll_delta: Arc, + timer_scheduled: Arc, +} + +/// How long to wait after the first scroll event before sending the +/// accumulated scroll delta to the main thread. +const DEBOUNCE_WINDOW: Duration = Duration::from_millis(100); + +/// Utility to debounce scroll events so we can determine estimate the +/// "magnitude" of the scroll event by accumulating them over a short window. +impl ScrollEventHelper { + pub(crate) fn new(app_event_tx: Sender) -> Self { + Self { + app_event_tx, + scroll_delta: Arc::new(AtomicI32::new(0)), + timer_scheduled: Arc::new(AtomicBool::new(false)), + } + } + + pub(crate) fn scroll_up(&self) { + self.scroll_delta.fetch_sub(1, Ordering::Relaxed); + self.schedule_notification(); + } + + pub(crate) fn scroll_down(&self) { + self.scroll_delta.fetch_add(1, Ordering::Relaxed); + self.schedule_notification(); + } + + /// Starts a one-shot timer **only once** per burst of wheel events. + fn schedule_notification(&self) { + // If the timer is already scheduled, do nothing. + if self + .timer_scheduled + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .is_err() + { + return; + } + + // Otherwise, schedule a new timer. + let tx = self.app_event_tx.clone(); + let delta = Arc::clone(&self.scroll_delta); + let timer_flag = Arc::clone(&self.timer_scheduled); + + thread::spawn(move || { + thread::sleep(DEBOUNCE_WINDOW); + + let accumulated = delta.swap(0, Ordering::SeqCst); + // Only emit if something really happened. + if accumulated != 0 { + let _ = tx.send(AppEvent::Scroll(accumulated)); + } + + // Allow a new timer to be started on the next wheel event. + timer_flag.store(false, Ordering::SeqCst); + }); + } +}