use ratatui::crossterm::event::{self, Event as CtEvent, MouseEvent, MouseEventKind};
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum DrainAction {
Count,
Skip,
StashAndStop,
}
pub(crate) fn drain_action(first: MouseEventKind, ev: &CtEvent) -> DrainAction {
let same_dir = matches!(
(first, mouse_kind(ev)),
(MouseEventKind::ScrollUp, Some(MouseEventKind::ScrollUp))
| (MouseEventKind::ScrollDown, Some(MouseEventKind::ScrollDown))
);
if same_dir {
DrainAction::Count
} else if matches!(mouse_kind(ev), Some(MouseEventKind::Moved)) {
DrainAction::Skip
} else {
DrainAction::StashAndStop
}
}
fn mouse_kind(ev: &CtEvent) -> Option<MouseEventKind> {
match ev {
CtEvent::Mouse(m) => Some(m.kind),
_ => None,
}
}
pub(crate) fn coalesce_scroll(first: &MouseEvent) -> std::io::Result<Option<MouseEvent>> {
if !matches!(
first.kind,
MouseEventKind::ScrollUp | MouseEventKind::ScrollDown
) {
return Ok(None);
}
const SCROLL_BATCH_CAP: u32 = 40;
let mut count: u32 = 1;
while count < SCROLL_BATCH_CAP {
if !event::poll(std::time::Duration::ZERO)? {
break;
}
let ev = event::read()?;
match drain_action(first.kind, &ev) {
DrainAction::Count => {
count += 1;
continue;
}
DrainAction::Skip => continue,
DrainAction::StashAndStop => {
COALESCE_LEFTOVER.with(|s| {
let mut slot = s.borrow_mut();
debug_assert!(
slot.is_none(),
"COALESCE_LEFTOVER was not drained before re-stashing"
);
*slot = Some(ev);
});
break;
}
}
}
if count <= 1 {
return Ok(None);
}
SCROLL_BATCH_COUNT.store(count, std::sync::atomic::Ordering::Relaxed);
Ok(Some(*first))
}
pub(crate) static SCROLL_BATCH_COUNT: std::sync::atomic::AtomicU32 =
std::sync::atomic::AtomicU32::new(1);
thread_local! {
static COALESCE_LEFTOVER: std::cell::RefCell<Option<CtEvent>> =
const { std::cell::RefCell::new(None) };
}
pub(crate) fn take_coalesce_leftover() -> Option<CtEvent> {
COALESCE_LEFTOVER.with(|s| s.borrow_mut().take())
}
pub(crate) fn take_scroll_batch_count() -> u32 {
SCROLL_BATCH_COUNT
.swap(1, std::sync::atomic::Ordering::Relaxed)
.max(1)
}
#[cfg(test)]
mod drain_policy_tests {
use super::*;
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
fn mouse(kind: MouseEventKind) -> CtEvent {
CtEvent::Mouse(MouseEvent {
kind,
column: 1,
row: 1,
modifiers: ratatui::crossterm::event::KeyModifiers::NONE,
})
}
#[test]
fn same_direction_scrolls_are_folded_into_the_batch() {
assert_eq!(
drain_action(
MouseEventKind::ScrollDown,
&mouse(MouseEventKind::ScrollDown)
),
DrainAction::Count
);
}
#[test]
fn a_motion_report_does_not_stop_the_drain() {
assert_eq!(
drain_action(MouseEventKind::ScrollDown, &mouse(MouseEventKind::Moved)),
DrainAction::Skip,
"a bare motion report ended the wheel drain"
);
}
#[test]
fn a_click_or_key_is_stashed_and_stops_the_drain() {
assert_eq!(
drain_action(
MouseEventKind::ScrollDown,
&mouse(MouseEventKind::Down(
ratatui::crossterm::event::MouseButton::Left
))
),
DrainAction::StashAndStop
);
assert_eq!(
drain_action(
MouseEventKind::ScrollDown,
&CtEvent::Key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE))
),
DrainAction::StashAndStop
);
}
#[test]
fn an_opposite_scroll_stops_the_drain() {
assert_eq!(
drain_action(MouseEventKind::ScrollDown, &mouse(MouseEventKind::ScrollUp)),
DrainAction::StashAndStop
);
}
}