use crate::{Event, MouseButton, Result};
use crossterm::event::{
self, Event as CrosstermEvent, MouseButton as CrosstermMouseButton, MouseEvent, MouseEventKind,
};
use std::time::Duration;
pub struct MouseHandler {
poll_rate: Duration,
track_movement: bool,
drag_detection: bool,
last_click_pos: Option<(u16, u16)>,
is_dragging: bool,
last_scroll_direction: Option<ScrollDirection>,
scroll_buffer_count: u8,
invert_scroll_vertical: bool,
invert_scroll_horizontal: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ScrollDirection {
Vertical,
Horizontal,
}
impl MouseHandler {
pub fn new() -> Self {
Self {
poll_rate: Duration::from_millis(1),
track_movement: true,
drag_detection: false,
last_click_pos: None,
is_dragging: false,
last_scroll_direction: None,
scroll_buffer_count: 0,
invert_scroll_vertical: false,
invert_scroll_horizontal: false,
}
}
pub fn set_poll_rate(&mut self, milliseconds: u64) {
self.poll_rate = Duration::from_millis(milliseconds);
}
pub fn poll_rate(&self) -> Duration {
self.poll_rate
}
pub fn set_movement_tracking(&mut self, enabled: bool) {
self.track_movement = enabled;
}
pub fn is_movement_tracking_enabled(&self) -> bool {
self.track_movement
}
pub fn enable_drag_detection(&mut self, enabled: bool) {
self.drag_detection = enabled;
if !enabled {
self.is_dragging = false;
self.last_click_pos = None;
}
}
pub fn is_drag_detection_enabled(&self) -> bool {
self.drag_detection
}
pub fn is_dragging(&self) -> bool {
self.is_dragging
}
pub fn drag_start_position(&self) -> Option<(u16, u16)> {
if self.is_dragging {
self.last_click_pos
} else {
None
}
}
pub fn set_invert_scroll_vertical(&mut self, invert: bool) {
self.invert_scroll_vertical = invert;
}
pub fn is_scroll_vertical_inverted(&self) -> bool {
self.invert_scroll_vertical
}
pub fn set_invert_scroll_horizontal(&mut self, invert: bool) {
self.invert_scroll_horizontal = invert;
}
pub fn is_scroll_horizontal_inverted(&self) -> bool {
self.invert_scroll_horizontal
}
pub fn poll(&mut self) -> Result<Option<Event>> {
if event::poll(self.poll_rate)? {
if let CrosstermEvent::Mouse(mouse_event) = event::read()? {
let converted_event = self.convert_mouse_event(mouse_event);
if self.is_scroll_event(&converted_event) {
return Ok(Some(self.coalesce_scroll_events(converted_event)?));
}
return Ok(Some(converted_event));
}
}
Ok(None)
}
pub fn get_input(&mut self, timeout: Duration) -> Result<Option<Event>> {
if event::poll(timeout)? {
if let CrosstermEvent::Mouse(mouse_event) = event::read()? {
return Ok(Some(self.convert_mouse_event(mouse_event)));
}
}
Ok(None)
}
pub fn wait_for_input(&mut self) -> Result<Event> {
loop {
if let CrosstermEvent::Mouse(mouse_event) = event::read()? {
return Ok(self.convert_mouse_event(mouse_event));
}
}
}
fn convert_mouse_event(&mut self, mouse_event: MouseEvent) -> Event {
let x = mouse_event.column;
let y = mouse_event.row;
match mouse_event.kind {
MouseEventKind::Down(button) => {
let minui_button = self.convert_mouse_button(button);
if self.drag_detection {
self.last_click_pos = Some((x, y));
self.is_dragging = false;
}
Event::MouseClick {
x,
y,
button: minui_button,
}
}
MouseEventKind::Up(button) => {
let minui_button = self.convert_mouse_button(button);
if self.drag_detection {
self.is_dragging = false;
self.last_click_pos = None;
}
Event::MouseRelease {
x,
y,
button: minui_button,
}
}
MouseEventKind::Drag(button) => {
let minui_button = self.convert_mouse_button(button);
if self.drag_detection && self.last_click_pos.is_some() {
self.is_dragging = true;
}
Event::MouseDrag {
x,
y,
button: minui_button,
}
}
MouseEventKind::Moved => {
if self.track_movement {
if self.drag_detection && self.last_click_pos.is_some() {
self.is_dragging = true;
}
Event::MouseMove { x, y }
} else {
Event::Unknown
}
}
MouseEventKind::ScrollDown => self.handle_scroll(ScrollDirection::Vertical, 1),
MouseEventKind::ScrollUp => self.handle_scroll(ScrollDirection::Vertical, -1),
MouseEventKind::ScrollLeft => self.handle_scroll(ScrollDirection::Horizontal, 1),
MouseEventKind::ScrollRight => self.handle_scroll(ScrollDirection::Horizontal, -1),
}
}
fn convert_mouse_button(&self, button: CrosstermMouseButton) -> MouseButton {
match button {
CrosstermMouseButton::Left => MouseButton::Left,
CrosstermMouseButton::Right => MouseButton::Right,
CrosstermMouseButton::Middle => MouseButton::Middle,
}
}
fn handle_scroll(&mut self, direction: ScrollDirection, delta: i8) -> Event {
const BUFFER_THRESHOLD: u8 = 2;
match self.last_scroll_direction {
None => {
self.last_scroll_direction = Some(direction);
self.scroll_buffer_count = 0;
self.emit_scroll_event(direction, delta)
}
Some(last_dir) if last_dir == direction => {
self.scroll_buffer_count = 0;
self.emit_scroll_event(direction, delta)
}
Some(_) => {
self.scroll_buffer_count += 1;
if self.scroll_buffer_count >= BUFFER_THRESHOLD {
self.last_scroll_direction = Some(direction);
self.scroll_buffer_count = 0;
self.emit_scroll_event(direction, delta)
} else {
self.emit_scroll_event(self.last_scroll_direction.unwrap(), delta)
}
}
}
}
fn emit_scroll_event(&self, direction: ScrollDirection, delta: i8) -> Event {
match direction {
ScrollDirection::Vertical => {
let final_delta = if self.invert_scroll_vertical {
-delta
} else {
delta
};
Event::MouseScroll { delta: final_delta }
}
ScrollDirection::Horizontal => {
let final_delta = if self.invert_scroll_horizontal {
-delta
} else {
delta
};
Event::MouseScrollHorizontal { delta: final_delta }
}
}
}
fn is_scroll_event(&self, event: &Event) -> bool {
matches!(
event,
Event::MouseScroll { .. } | Event::MouseScrollHorizontal { .. }
)
}
fn coalesce_scroll_events(&mut self, initial_event: Event) -> Result<Event> {
let mut total_delta = match initial_event {
Event::MouseScroll { delta } => (delta, 0),
Event::MouseScrollHorizontal { delta } => (0, delta),
_ => return Ok(initial_event),
};
while event::poll(Duration::from_millis(0))? {
if let Ok(CrosstermEvent::Mouse(mouse_event)) = event::read() {
let next_event = self.convert_mouse_event(mouse_event);
match next_event {
Event::MouseScroll { delta } => {
total_delta.0 += delta;
}
Event::MouseScrollHorizontal { delta } => {
total_delta.1 += delta;
}
_ => {
break;
}
}
} else {
break;
}
}
if total_delta.0 != 0 {
Ok(Event::MouseScroll {
delta: total_delta.0,
})
} else if total_delta.1 != 0 {
Ok(Event::MouseScrollHorizontal {
delta: total_delta.1,
})
} else {
Ok(initial_event)
}
}
pub fn process_mouse_event(&mut self, mouse_event: MouseEvent) -> Event {
self.convert_mouse_event(mouse_event)
}
}
impl Default for MouseHandler {
fn default() -> Self {
Self::new()
}
}
pub struct CombinedInputHandler {
keyboard: crate::input::KeyboardHandler,
mouse: MouseHandler,
}
impl CombinedInputHandler {
pub fn new() -> Self {
Self {
keyboard: crate::input::KeyboardHandler::new(),
mouse: MouseHandler::new(),
}
}
pub fn with_common_keybinds() -> Self {
Self {
keyboard: crate::input::KeyboardHandler::with_common_keybinds(),
mouse: MouseHandler::new(),
}
}
pub fn keyboard_mut(&mut self) -> &mut crate::input::KeyboardHandler {
&mut self.keyboard
}
pub fn mouse_mut(&mut self) -> &mut MouseHandler {
&mut self.mouse
}
pub fn poll(&mut self) -> Result<Option<Event>> {
if let Some(event) = self.keyboard.poll_with_keybinds()? {
return Ok(Some(event));
}
if let Ok(Some(event)) = self.mouse.poll() {
return Ok(Some(event));
}
Ok(None)
}
pub fn get_input(&mut self, timeout: Duration) -> Result<Option<Event>> {
let keyboard_event = self.keyboard.get_input(timeout)?;
if keyboard_event != Event::Unknown {
return Ok(Some(keyboard_event));
}
self.mouse.get_input(timeout)
}
}
impl Default for CombinedInputHandler {
fn default() -> Self {
Self::new()
}
}