use crate::{Event, MouseButton, Result};
use crossterm::event::{
self, Event as CrosstermEvent, MouseButton as CrosstermMouseButton, MouseEvent, MouseEventKind,
};
use std::time::{Duration, Instant};
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,
click_tracker: ClickTracker,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ScrollDirection {
Vertical,
Horizontal,
}
pub struct ClickTracker {
last_click: Instant,
last_pos: (u16, u16),
double_click_threshold: Duration,
double_click_distance: u16,
}
impl ClickTracker {
pub fn new() -> Self {
Self {
last_click: Instant::now() - Duration::from_secs(1), last_pos: (0, 0),
double_click_threshold: Duration::from_millis(500),
double_click_distance: 3,
}
}
pub fn with_threshold(mut self, threshold: Duration) -> Self {
self.double_click_threshold = threshold;
self
}
pub fn with_distance(mut self, distance: u16) -> Self {
self.double_click_distance = distance;
self
}
pub fn is_double_click(&mut self, x: u16, y: u16) -> bool {
let now = Instant::now();
let time_diff = now.duration_since(self.last_click);
let pos_diff_x = if x > self.last_pos.0 {
x - self.last_pos.0
} else {
self.last_pos.0 - x
};
let pos_diff_y = if y > self.last_pos.1 {
y - self.last_pos.1
} else {
self.last_pos.1 - y
};
let is_double = time_diff < self.double_click_threshold
&& pos_diff_x <= self.double_click_distance
&& pos_diff_y <= self.double_click_distance;
self.last_click = now;
self.last_pos = (x, y);
is_double
}
pub fn last_position(&self) -> (u16, u16) {
self.last_pos
}
pub fn time_since_last_click(&self) -> Duration {
self.last_click.elapsed()
}
}
impl Default for ClickTracker {
fn default() -> Self {
Self::new()
}
}
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,
click_tracker: ClickTracker::new(),
}
}
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 click_tracker(&self) -> &ClickTracker {
&self.click_tracker
}
pub fn click_tracker_mut(&mut self) -> &mut ClickTracker {
&mut self.click_tracker
}
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()? {
return Ok(Some(self.convert_mouse_event(mouse_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 }
}
}
}
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 {
fn process_input_event(&mut self, input: CrosstermEvent) -> Option<Event> {
match input {
CrosstermEvent::Key(key_event) => Some(self.keyboard.process_key_event(key_event)),
CrosstermEvent::Mouse(mouse_event) => Some(self.mouse.process_mouse_event(mouse_event)),
CrosstermEvent::Paste(text) => Some(Event::Paste(text)),
CrosstermEvent::Resize(width, height) => Some(Event::Resize { width, height }),
_ => None,
}
}
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 !event::poll(Duration::ZERO)? {
return Ok(None);
}
Ok(self.process_input_event(event::read()?))
}
pub fn get_input(&mut self, timeout: Duration) -> Result<Option<Event>> {
if !event::poll(timeout)? {
return Ok(None);
}
Ok(self.process_input_event(event::read()?))
}
}
impl Default for CombinedInputHandler {
fn default() -> Self {
Self::new()
}
}