#![forbid(unsafe_code)]
use crate::event::{Event, MouseEvent, MouseEventKind};
#[derive(Debug, Clone, Default)]
pub struct EventCoalescer {
pending_mouse_move: Option<MouseEvent>,
pending_scroll: Option<ScrollState>,
closed_scrolls: Vec<ScrollState>,
}
#[derive(Debug, Clone, Copy)]
struct ScrollState {
direction: ScrollDirection,
count: u32,
modifiers: crate::event::Modifiers,
x: u16,
y: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ScrollDirection {
Up,
Down,
Left,
Right,
}
impl EventCoalescer {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, event: Event) -> Option<Event> {
match &event {
Event::Mouse(mouse) => match mouse.kind {
MouseEventKind::Moved => {
self.pending_mouse_move = Some(*mouse);
None
}
MouseEventKind::ScrollUp => self.handle_scroll(ScrollDirection::Up, mouse),
MouseEventKind::ScrollDown => self.handle_scroll(ScrollDirection::Down, mouse),
MouseEventKind::ScrollLeft => self.handle_scroll(ScrollDirection::Left, mouse),
MouseEventKind::ScrollRight => self.handle_scroll(ScrollDirection::Right, mouse),
_ => Some(event),
},
_ => Some(event),
}
}
fn handle_scroll(&mut self, direction: ScrollDirection, mouse: &MouseEvent) -> Option<Event> {
if let Some(pending) = self.pending_scroll {
if pending.direction == direction
&& pending.modifiers == mouse.modifiers
&& (pending.x, pending.y) == (mouse.x, mouse.y)
{
self.pending_scroll = Some(ScrollState {
count: pending.count.saturating_add(1),
..pending
});
} else {
self.closed_scrolls.push(pending);
self.pending_scroll = Some(ScrollState {
direction,
count: 1,
modifiers: mouse.modifiers,
x: mouse.x,
y: mouse.y,
});
}
} else {
self.pending_scroll = Some(ScrollState {
direction,
count: 1,
modifiers: mouse.modifiers,
x: mouse.x,
y: mouse.y,
});
}
None
}
fn scroll_to_event(state: ScrollState) -> Event {
let kind = match state.direction {
ScrollDirection::Up => MouseEventKind::ScrollUp,
ScrollDirection::Down => MouseEventKind::ScrollDown,
ScrollDirection::Left => MouseEventKind::ScrollLeft,
ScrollDirection::Right => MouseEventKind::ScrollRight,
};
Event::Mouse(MouseEvent::new(kind, state.x, state.y).with_modifiers(state.modifiers))
}
#[must_use]
pub fn flush(&mut self) -> Vec<Event> {
let mut events = Vec::new();
self.flush_each(|event| events.push(event));
events
}
pub fn flush_each<F>(&mut self, mut f: F)
where
F: FnMut(Event),
{
for run in self.closed_scrolls.drain(..) {
let event = Self::scroll_to_event(run);
for _ in 0..run.count {
f(event.clone());
}
}
if let Some(run) = self.pending_scroll.take() {
let event = Self::scroll_to_event(run);
for _ in 0..run.count {
f(event.clone());
}
}
if let Some(mouse) = self.pending_mouse_move.take() {
f(Event::Mouse(mouse));
}
}
#[must_use]
pub fn has_pending(&self) -> bool {
self.pending_mouse_move.is_some()
|| self.pending_scroll.is_some()
|| !self.closed_scrolls.is_empty()
}
#[must_use]
pub fn pending_scroll_count(&self) -> u32 {
self.pending_scroll.map_or(0, |s| s.count)
}
#[must_use]
pub fn pending_scroll_events(&self) -> u32 {
self.closed_scrolls
.iter()
.fold(self.pending_scroll_count(), |total, run| {
total.saturating_add(run.count)
})
}
pub fn clear(&mut self) {
self.pending_mouse_move = None;
self.pending_scroll = None;
self.closed_scrolls.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{KeyCode, KeyEvent, Modifiers, MouseButton};
#[test]
fn new_coalescer_has_no_pending() {
let coalescer = EventCoalescer::new();
assert!(!coalescer.has_pending());
assert_eq!(coalescer.pending_scroll_count(), 0);
}
#[test]
fn mouse_move_coalesces() {
let mut coalescer = EventCoalescer::new();
let result = coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 10, 10)));
assert!(result.is_none());
assert!(coalescer.has_pending());
let result = coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 20, 25)));
assert!(result.is_none());
let pending = coalescer.flush();
assert_eq!(pending.len(), 1);
if let Event::Mouse(m) = &pending[0] {
assert_eq!(m.x, 20);
assert_eq!(m.y, 25);
assert!(matches!(m.kind, MouseEventKind::Moved));
} else {
panic!("expected mouse event");
}
}
#[test]
fn mouse_move_preserves_modifiers() {
let mut coalescer = EventCoalescer::new();
let move_event =
MouseEvent::new(MouseEventKind::Moved, 5, 5).with_modifiers(Modifiers::ALT);
coalescer.push(Event::Mouse(move_event));
let pending = coalescer.flush();
if let Event::Mouse(m) = &pending[0] {
assert_eq!(m.modifiers, Modifiers::ALT);
}
}
#[test]
fn mouse_click_passes_through() {
let mut coalescer = EventCoalescer::new();
let click = Event::Mouse(MouseEvent::new(
MouseEventKind::Down(MouseButton::Left),
10,
10,
));
let result = coalescer.push(click.clone());
assert_eq!(result, Some(click));
assert!(!coalescer.has_pending());
}
#[test]
fn mouse_drag_passes_through() {
let mut coalescer = EventCoalescer::new();
let drag = Event::Mouse(MouseEvent::new(
MouseEventKind::Drag(MouseButton::Left),
10,
10,
));
let result = coalescer.push(drag.clone());
assert_eq!(result, Some(drag));
}
#[test]
fn key_event_passes_through() {
let mut coalescer = EventCoalescer::new();
let key = Event::Key(KeyEvent::new(KeyCode::Enter));
let result = coalescer.push(key.clone());
assert_eq!(result, Some(key));
}
fn push_scrolls(coalescer: &mut EventCoalescer, kind: MouseEventKind, n: usize) {
for _ in 0..n {
coalescer.push(Event::Mouse(MouseEvent::new(kind, 0, 0)));
}
}
fn scroll_kinds(events: &[Event]) -> Vec<MouseEventKind> {
events
.iter()
.filter_map(|e| match e {
Event::Mouse(m) => Some(m.kind),
_ => None,
})
.collect()
}
#[test]
fn scroll_same_direction_batches_without_losing_notches() {
let mut coalescer = EventCoalescer::new();
push_scrolls(&mut coalescer, MouseEventKind::ScrollUp, 3);
assert_eq!(coalescer.pending_scroll_count(), 3);
assert_eq!(coalescer.pending_scroll_events(), 3);
let pending = coalescer.flush();
assert_eq!(
scroll_kinds(&pending),
vec![
MouseEventKind::ScrollUp,
MouseEventKind::ScrollUp,
MouseEventKind::ScrollUp
]
);
assert!(!coalescer.has_pending());
}
#[test]
fn scroll_direction_change_keeps_both_runs_whole() {
let mut coalescer = EventCoalescer::new();
push_scrolls(&mut coalescer, MouseEventKind::ScrollUp, 2);
let result = coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollDown,
0,
0,
)));
assert!(result.is_none());
assert_eq!(coalescer.pending_scroll_count(), 1, "the open down-run");
assert_eq!(coalescer.pending_scroll_events(), 3, "both runs");
let pending = coalescer.flush();
assert_eq!(
scroll_kinds(&pending),
vec![
MouseEventKind::ScrollUp,
MouseEventKind::ScrollUp,
MouseEventKind::ScrollDown
]
);
assert!(!coalescer.has_pending());
}
#[test]
fn scroll_runs_alternating_directions_survive_in_order() {
let mut coalescer = EventCoalescer::new();
push_scrolls(&mut coalescer, MouseEventKind::ScrollDown, 2);
push_scrolls(&mut coalescer, MouseEventKind::ScrollUp, 1);
push_scrolls(&mut coalescer, MouseEventKind::ScrollDown, 3);
assert_eq!(coalescer.pending_scroll_events(), 6);
assert_eq!(
scroll_kinds(&coalescer.flush()),
vec![
MouseEventKind::ScrollDown,
MouseEventKind::ScrollDown,
MouseEventKind::ScrollUp,
MouseEventKind::ScrollDown,
MouseEventKind::ScrollDown,
MouseEventKind::ScrollDown
]
);
}
#[test]
fn clear_discards_closed_runs_too() {
let mut coalescer = EventCoalescer::new();
push_scrolls(&mut coalescer, MouseEventKind::ScrollUp, 2);
push_scrolls(&mut coalescer, MouseEventKind::ScrollDown, 2);
assert!(coalescer.has_pending());
coalescer.clear();
assert!(!coalescer.has_pending());
assert_eq!(coalescer.pending_scroll_events(), 0);
assert!(coalescer.flush().is_empty());
}
#[test]
fn scroll_preserves_modifiers() {
let mut coalescer = EventCoalescer::new();
let scroll =
MouseEvent::new(MouseEventKind::ScrollUp, 0, 0).with_modifiers(Modifiers::CTRL);
coalescer.push(Event::Mouse(scroll));
let pending = coalescer.flush();
if let Event::Mouse(m) = &pending[0] {
assert_eq!(m.modifiers, Modifiers::CTRL);
}
}
#[test]
fn flush_returns_scroll_before_move() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
0,
0,
)));
coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 10, 10)));
let pending = coalescer.flush();
assert_eq!(pending.len(), 2);
assert!(matches!(
pending[0],
Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollUp,
..
})
));
assert!(matches!(
pending[1],
Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
..
})
));
}
#[test]
fn flush_each_processes_in_order() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollDown,
0,
0,
)));
coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 5, 5)));
let mut events = Vec::new();
coalescer.flush_each(|e| events.push(e));
assert_eq!(events.len(), 2);
assert!(matches!(
events[0],
Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollDown,
..
})
));
assert!(matches!(
events[1],
Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
..
})
));
}
#[test]
fn clear_discards_pending() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 10, 10)));
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
0,
0,
)));
assert!(coalescer.has_pending());
coalescer.clear();
assert!(!coalescer.has_pending());
assert!(coalescer.flush().is_empty());
}
#[test]
fn resize_passes_through() {
let mut coalescer = EventCoalescer::new();
let resize = Event::Resize {
width: 80,
height: 24,
};
let result = coalescer.push(resize.clone());
assert_eq!(result, Some(resize));
}
#[test]
fn focus_passes_through() {
let mut coalescer = EventCoalescer::new();
let focus = Event::Focus(true);
let result = coalescer.push(focus.clone());
assert_eq!(result, Some(focus));
}
#[test]
fn many_moves_coalesce_to_one() {
let mut coalescer = EventCoalescer::new();
for i in 0..100 {
coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, i, i)));
}
let pending = coalescer.flush();
assert_eq!(pending.len(), 1);
if let Event::Mouse(m) = &pending[0] {
assert_eq!(m.x, 99);
assert_eq!(m.y, 99);
}
}
#[test]
fn scroll_count_saturates() {
let mut coalescer = EventCoalescer::new();
for _ in 0..1000 {
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
0,
0,
)));
}
assert_eq!(coalescer.pending_scroll_count(), 1000);
}
#[test]
fn horizontal_scroll_coalesces() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollLeft,
0,
0,
)));
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollLeft,
0,
0,
)));
assert_eq!(coalescer.pending_scroll_count(), 2);
let pending = coalescer.flush();
if let Event::Mouse(m) = &pending[0] {
assert!(matches!(m.kind, MouseEventKind::ScrollLeft));
}
}
#[test]
fn scroll_preserves_position() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
10,
20,
)));
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
15,
25,
)));
let pending = coalescer.flush();
let positions: Vec<_> = pending
.iter()
.map(|event| {
let Event::Mouse(m) = event else {
panic!("expected mouse event");
};
assert!(matches!(m.kind, MouseEventKind::ScrollUp));
(m.x, m.y)
})
.collect();
assert_eq!(positions, [(10, 20), (15, 25)]);
}
#[test]
fn default_coalescer_has_no_pending() {
let coalescer = EventCoalescer::default();
assert!(!coalescer.has_pending());
assert_eq!(coalescer.pending_scroll_count(), 0);
}
#[test]
fn scroll_direction_change_flushes_old() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
0,
0,
)));
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
0,
0,
)));
let result = coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollDown,
0,
0,
)));
assert!(result.is_none());
assert_eq!(coalescer.pending_scroll_count(), 1);
assert_eq!(coalescer.pending_scroll_events(), 3);
assert_eq!(
scroll_kinds(&coalescer.flush()),
vec![
MouseEventKind::ScrollUp,
MouseEventKind::ScrollUp,
MouseEventKind::ScrollDown
]
);
}
#[test]
fn pending_scroll_count_zero_after_flush() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
0,
0,
)));
assert_eq!(coalescer.pending_scroll_count(), 1);
let _ = coalescer.flush();
assert_eq!(coalescer.pending_scroll_count(), 0);
}
#[test]
fn scroll_right_coalesces() {
let mut coalescer = EventCoalescer::new();
for _ in 0..2 {
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollRight,
5,
10,
)));
}
assert_eq!(coalescer.pending_scroll_count(), 2);
let pending = coalescer.flush();
assert_eq!(pending.len(), 2);
for event in &pending {
if let Event::Mouse(m) = event {
assert!(matches!(m.kind, MouseEventKind::ScrollRight));
assert_eq!(m.x, 5);
assert_eq!(m.y, 10);
}
}
}
#[test]
fn clone_preserves_pending_state() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 7, 8)));
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
1,
2,
)));
let mut cloned = coalescer.clone();
assert!(cloned.has_pending());
assert_eq!(cloned.pending_scroll_count(), 1);
let pending = cloned.flush();
assert_eq!(pending.len(), 2);
assert!(coalescer.has_pending());
}
#[test]
fn flush_empty_returns_empty_vec() {
let mut coalescer = EventCoalescer::new();
let pending = coalescer.flush();
assert!(pending.is_empty());
}
#[test]
fn flush_each_empty_does_not_call_closure() {
let mut coalescer = EventCoalescer::new();
let mut called = false;
coalescer.flush_each(|_| called = true);
assert!(!called);
}
#[test]
fn double_flush_second_empty() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 1, 1)));
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollDown,
0,
0,
)));
let first = coalescer.flush();
assert_eq!(first.len(), 2);
let second = coalescer.flush();
assert!(second.is_empty());
assert!(!coalescer.has_pending());
}
#[test]
fn paste_event_passes_through() {
let mut coalescer = EventCoalescer::new();
let paste = Event::Paste(crate::event::PasteEvent {
text: "hello".into(),
bracketed: true,
});
let result = coalescer.push(paste.clone());
assert_eq!(result, Some(paste));
assert!(!coalescer.has_pending());
}
#[test]
fn mouse_up_passes_through() {
let mut coalescer = EventCoalescer::new();
let up = Event::Mouse(MouseEvent::new(MouseEventKind::Up(MouseButton::Left), 5, 5));
let result = coalescer.push(up.clone());
assert_eq!(result, Some(up));
}
#[test]
fn mouse_up_right_passes_through() {
let mut coalescer = EventCoalescer::new();
let up = Event::Mouse(MouseEvent::new(
MouseEventKind::Up(MouseButton::Right),
0,
0,
));
assert_eq!(coalescer.push(up.clone()), Some(up));
}
#[test]
fn mouse_down_middle_passes_through() {
let mut coalescer = EventCoalescer::new();
let down = Event::Mouse(MouseEvent::new(
MouseEventKind::Down(MouseButton::Middle),
0,
0,
));
assert_eq!(coalescer.push(down.clone()), Some(down));
}
#[test]
fn drag_right_button_passes_through() {
let mut coalescer = EventCoalescer::new();
let drag = Event::Mouse(MouseEvent::new(
MouseEventKind::Drag(MouseButton::Right),
3,
4,
));
assert_eq!(coalescer.push(drag.clone()), Some(drag));
}
#[test]
fn has_pending_move_only() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 0, 0)));
assert!(coalescer.has_pending());
assert_eq!(coalescer.pending_scroll_count(), 0);
}
#[test]
fn has_pending_scroll_only() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollDown,
0,
0,
)));
assert!(coalescer.has_pending());
assert_eq!(coalescer.pending_scroll_count(), 1);
}
#[test]
fn move_at_origin() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 0, 0)));
let pending = coalescer.flush();
assert_eq!(pending.len(), 1);
if let Event::Mouse(m) = &pending[0] {
assert_eq!((m.x, m.y), (0, 0));
}
}
#[test]
fn move_at_max_coordinates() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::Moved,
u16::MAX,
u16::MAX,
)));
let pending = coalescer.flush();
if let Event::Mouse(m) = &pending[0] {
assert_eq!(m.x, u16::MAX);
assert_eq!(m.y, u16::MAX);
}
}
#[test]
fn scroll_at_max_coordinates() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
u16::MAX,
u16::MAX,
)));
let pending = coalescer.flush();
if let Event::Mouse(m) = &pending[0] {
assert_eq!(m.x, u16::MAX);
assert_eq!(m.y, u16::MAX);
}
}
#[test]
fn move_with_all_modifiers() {
let mut coalescer = EventCoalescer::new();
let mods = Modifiers::SHIFT | Modifiers::ALT | Modifiers::CTRL | Modifiers::SUPER;
let ev = MouseEvent::new(MouseEventKind::Moved, 10, 20).with_modifiers(mods);
coalescer.push(Event::Mouse(ev));
let pending = coalescer.flush();
if let Event::Mouse(m) = &pending[0] {
assert_eq!(m.modifiers, mods);
}
}
#[test]
fn scroll_direction_change_preserves_new_modifiers() {
let mut coalescer = EventCoalescer::new();
let scroll_up =
MouseEvent::new(MouseEventKind::ScrollUp, 0, 0).with_modifiers(Modifiers::SHIFT);
coalescer.push(Event::Mouse(scroll_up));
let scroll_down =
MouseEvent::new(MouseEventKind::ScrollDown, 5, 5).with_modifiers(Modifiers::CTRL);
assert!(coalescer.push(Event::Mouse(scroll_down)).is_none());
let pending = coalescer.flush();
assert_eq!(pending.len(), 2);
let Event::Mouse(old) = &pending[0] else {
panic!("expected the closed up-run first");
};
assert!(matches!(old.kind, MouseEventKind::ScrollUp));
assert_eq!(old.modifiers, Modifiers::SHIFT);
let Event::Mouse(new) = &pending[1] else {
panic!("expected the open down-run second");
};
assert!(matches!(new.kind, MouseEventKind::ScrollDown));
assert_eq!(new.modifiers, Modifiers::CTRL);
}
#[test]
fn scroll_direction_change_keeps_each_notch_where_it_happened() {
let mut coalescer = EventCoalescer::new();
for (kind, x, y) in [
(MouseEventKind::ScrollUp, 10, 20),
(MouseEventKind::ScrollUp, 15, 25),
(MouseEventKind::ScrollDown, 30, 40),
] {
assert!(
coalescer
.push(Event::Mouse(MouseEvent::new(kind, x, y)))
.is_none()
);
}
let flushed: Vec<_> = coalescer
.flush()
.into_iter()
.map(|event| {
let Event::Mouse(m) = event else {
panic!("expected mouse event");
};
(m.kind, m.x, m.y)
})
.collect();
assert_eq!(
flushed,
[
(MouseEventKind::ScrollUp, 10, 20),
(MouseEventKind::ScrollUp, 15, 25),
(MouseEventKind::ScrollDown, 30, 40),
]
);
}
#[test]
fn four_direction_changes() {
let mut coalescer = EventCoalescer::new();
let directions = [
MouseEventKind::ScrollUp,
MouseEventKind::ScrollDown,
MouseEventKind::ScrollLeft,
MouseEventKind::ScrollRight,
];
assert!(
coalescer
.push(Event::Mouse(MouseEvent::new(directions[0], 0, 0)))
.is_none()
);
for &dir in &directions[1..] {
let flushed = coalescer.push(Event::Mouse(MouseEvent::new(dir, 0, 0)));
assert!(flushed.is_none());
}
assert_eq!(coalescer.pending_scroll_count(), 1);
assert_eq!(coalescer.pending_scroll_events(), 4);
assert_eq!(scroll_kinds(&coalescer.flush()), directions.to_vec());
}
#[test]
fn horizontal_to_vertical_direction_change() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollLeft,
0,
0,
)));
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollLeft,
0,
0,
)));
let old = coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
0,
0,
)));
assert!(old.is_none());
assert_eq!(
scroll_kinds(&coalescer.flush()),
vec![
MouseEventKind::ScrollLeft,
MouseEventKind::ScrollLeft,
MouseEventKind::ScrollUp
]
);
}
#[test]
fn push_clear_flush_empty() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 5, 5)));
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
0,
0,
)));
coalescer.clear();
assert!(coalescer.flush().is_empty());
assert_eq!(coalescer.pending_scroll_count(), 0);
}
#[test]
fn passthrough_does_not_affect_pending() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 1, 1)));
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
0,
0,
)));
let key = Event::Key(KeyEvent::new(KeyCode::Char('a')));
let result = coalescer.push(key);
assert!(result.is_some());
assert!(coalescer.has_pending());
assert_eq!(coalescer.pending_scroll_count(), 1);
let pending = coalescer.flush();
assert_eq!(pending.len(), 2);
}
#[test]
fn flush_then_reuse() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 1, 1)));
let _ = coalescer.flush();
coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 10, 20)));
let pending = coalescer.flush();
assert_eq!(pending.len(), 1);
if let Event::Mouse(m) = &pending[0] {
assert_eq!((m.x, m.y), (10, 20));
}
}
#[test]
fn scroll_count_u32_max_saturates() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
0,
0,
)));
assert_eq!(coalescer.pending_scroll_count(), 1);
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
0,
0,
)));
assert_eq!(coalescer.pending_scroll_count(), 2);
}
#[test]
fn single_scroll_count_is_one() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollDown,
0,
0,
)));
assert_eq!(coalescer.pending_scroll_count(), 1);
}
#[test]
fn scroll_notches_keep_their_own_modifiers_in_one_direction() {
let mut coalescer = EventCoalescer::new();
for modifiers in [
Modifiers::NONE,
Modifiers::NONE,
Modifiers::NONE,
Modifiers::CTRL,
] {
let notch = MouseEvent::new(MouseEventKind::ScrollUp, 0, 0).with_modifiers(modifiers);
coalescer.push(Event::Mouse(notch));
}
let flushed: Vec<_> = coalescer
.flush()
.into_iter()
.map(|event| {
let Event::Mouse(m) = event else {
panic!("expected mouse event");
};
m.modifiers
})
.collect();
assert_eq!(
flushed,
[
Modifiers::NONE,
Modifiers::NONE,
Modifiers::NONE,
Modifiers::CTRL,
]
);
}
#[test]
fn move_replaces_move_preserves_latest_modifiers() {
let mut coalescer = EventCoalescer::new();
let m1 = MouseEvent::new(MouseEventKind::Moved, 1, 1).with_modifiers(Modifiers::SHIFT);
coalescer.push(Event::Mouse(m1));
let m2 = MouseEvent::new(MouseEventKind::Moved, 2, 2).with_modifiers(Modifiers::CTRL);
coalescer.push(Event::Mouse(m2));
let pending = coalescer.flush();
if let Event::Mouse(m) = &pending[0] {
assert_eq!(m.modifiers, Modifiers::CTRL);
assert_eq!((m.x, m.y), (2, 2));
}
}
#[test]
fn flush_each_equivalent_to_flush() {
let mut c1 = EventCoalescer::new();
let mut c2 = EventCoalescer::new();
let events = [
Event::Mouse(MouseEvent::new(MouseEventKind::ScrollDown, 3, 4)),
Event::Mouse(MouseEvent::new(MouseEventKind::ScrollDown, 5, 6)),
Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 10, 20)),
];
for e in &events {
c1.push(e.clone());
c2.push(e.clone());
}
let vec_flush = c1.flush();
let mut each_flush = Vec::new();
c2.flush_each(|e| each_flush.push(e));
assert_eq!(vec_flush, each_flush);
}
#[test]
fn flush_order_scroll_then_move() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 1, 1)));
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
0,
0,
)));
let pending = coalescer.flush();
assert_eq!(pending.len(), 2);
assert!(matches!(
pending[0],
Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollUp,
..
})
));
assert!(matches!(
pending[1],
Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
..
})
));
}
#[test]
fn rapid_alternating_scroll_directions() {
let mut coalescer = EventCoalescer::new();
let mut expected = Vec::new();
for i in 0..10 {
let kind = if i % 2 == 0 {
MouseEventKind::ScrollUp
} else {
MouseEventKind::ScrollDown
};
expected.push(kind);
assert!(
coalescer
.push(Event::Mouse(MouseEvent::new(kind, 0, 0)))
.is_none()
);
}
assert_eq!(coalescer.pending_scroll_count(), 1);
assert_eq!(coalescer.pending_scroll_events(), 10);
assert_eq!(scroll_kinds(&coalescer.flush()), expected);
}
#[test]
fn resize_does_not_affect_pending() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 5, 5)));
let resize = Event::Resize {
width: 120,
height: 40,
};
let result = coalescer.push(resize.clone());
assert_eq!(result, Some(resize));
assert!(coalescer.has_pending());
let pending = coalescer.flush();
assert_eq!(pending.len(), 1);
}
#[test]
fn focus_does_not_affect_pending() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
0,
0,
)));
let result = coalescer.push(Event::Focus(false));
assert_eq!(result, Some(Event::Focus(false)));
assert_eq!(coalescer.pending_scroll_count(), 1);
}
#[test]
fn debug_format_contains_type_name() {
let coalescer = EventCoalescer::new();
let dbg = format!("{coalescer:?}");
assert!(dbg.contains("EventCoalescer"));
}
#[test]
fn flush_only_move_returns_one() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 42, 99)));
let pending = coalescer.flush();
assert_eq!(pending.len(), 1);
assert!(matches!(
pending[0],
Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
..
})
));
}
#[test]
fn flush_only_scroll_returns_one() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollLeft,
0,
0,
)));
let pending = coalescer.flush();
assert_eq!(pending.len(), 1);
assert!(matches!(
pending[0],
Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollLeft,
..
})
));
}
#[test]
fn clear_after_direction_change() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
0,
0,
)));
let _ = coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollDown,
0,
0,
)));
coalescer.clear();
assert!(!coalescer.has_pending());
assert_eq!(coalescer.pending_scroll_count(), 0);
}
#[test]
fn scroll_runs_split_where_the_position_changes() {
let mut coalescer = EventCoalescer::new();
let notches = [(0, 0), (0, 0), (100, 200), (50, 60), (50, 60)];
for (x, y) in notches {
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollDown,
x,
y,
)));
}
assert_eq!(coalescer.pending_scroll_count(), 2);
assert_eq!(coalescer.pending_scroll_events(), 5);
let positions: Vec<_> = coalescer
.flush()
.into_iter()
.map(|event| {
let Event::Mouse(m) = event else {
panic!("expected mouse event");
};
(m.x, m.y)
})
.collect();
assert_eq!(positions, notches);
assert_eq!(coalescer.pending_scroll_count(), 0);
}
#[test]
fn move_does_not_flush_pending_scroll() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
0,
0,
)));
let result = coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 1, 1)));
assert!(result.is_none());
assert_eq!(coalescer.pending_scroll_count(), 1);
}
#[test]
fn scroll_does_not_flush_pending_move() {
let mut coalescer = EventCoalescer::new();
coalescer.push(Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 1, 1)));
let result = coalescer.push(Event::Mouse(MouseEvent::new(
MouseEventKind::ScrollUp,
0,
0,
)));
assert!(result.is_none());
let pending = coalescer.flush();
assert_eq!(pending.len(), 2);
}
#[test]
fn mixed_coalescing_workflow() {
let mut coalescer = EventCoalescer::new();
let mut processed = Vec::new();
let events = vec![
Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 0, 0)),
Event::Mouse(MouseEvent::new(MouseEventKind::Moved, 5, 5)),
Event::Mouse(MouseEvent::new(
MouseEventKind::Down(MouseButton::Left),
5,
5,
)),
Event::Mouse(MouseEvent::new(
MouseEventKind::Drag(MouseButton::Left),
10,
10,
)),
Event::Mouse(MouseEvent::new(
MouseEventKind::Up(MouseButton::Left),
10,
10,
)),
Event::Mouse(MouseEvent::new(MouseEventKind::ScrollUp, 0, 0)),
Event::Mouse(MouseEvent::new(MouseEventKind::ScrollUp, 0, 0)),
Event::Key(KeyEvent::new(KeyCode::Escape)),
];
for event in events {
if let Some(e) = coalescer.push(event) {
coalescer.flush_each(|pending| processed.push(pending));
processed.push(e);
}
}
coalescer.flush_each(|e| processed.push(e));
assert_eq!(processed.len(), 7);
let move_event = processed
.iter()
.find(|e| matches!(e, Event::Mouse(m) if matches!(m.kind, MouseEventKind::Moved)));
assert!(move_event.is_some());
if let Some(Event::Mouse(m)) = move_event {
assert_eq!(m.x, 5);
assert_eq!(m.y, 5);
}
}
}