From 3df4628e6d374dd7fe8c48334243dff359794289 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sun, 13 Oct 2024 18:46:24 +0200 Subject: [PATCH] feat(event): add event handling for mouse events (#79) crossterm only, as termion does not support mouse events fixes #64 Co-authored-by: veeso --- src/core/event.rs | 83 ++++++++++- src/core/subscription.rs | 93 ++++++++++++- src/terminal/event_listener/crossterm.rs | 168 ++++++++++++++++++++++- 3 files changed, 337 insertions(+), 7 deletions(-) diff --git a/src/core/event.rs b/src/core/event.rs index c3319a7..08fd569 100644 --- a/src/core/event.rs +++ b/src/core/event.rs @@ -16,6 +16,8 @@ where { /// A keyboard event Keyboard(KeyEvent), + /// A Mouse event + Mouse(MouseEvent), /// This event is raised after the terminal window is resized WindowResize(u16, u16), /// Window focus gained @@ -45,6 +47,14 @@ where } } + pub(crate) fn is_mouse(&self) -> Option<&MouseEvent> { + if let Event::Mouse(m) = self { + Some(m) + } else { + None + } + } + pub(crate) fn is_window_resize(&self) -> bool { matches!(self, Self::WindowResize(_, _)) } @@ -234,6 +244,66 @@ pub enum MediaKeyCode { MuteVolume, } +/// A keyboard event +#[derive(Debug, Eq, PartialEq, Copy, Clone, PartialOrd, Hash)] +#[cfg_attr( + feature = "serialize", + derive(Deserialize, Serialize), + serde(tag = "type") +)] +pub struct MouseEvent { + /// The kind of mouse event that was caused + pub kind: MouseEventKind, + /// The key modifiers active when the event occurred + pub modifiers: KeyModifiers, + /// The column that the event occurred on + pub column: u16, + /// The row that the event occurred on + pub row: u16, +} + +/// A Mouse event +#[derive(Debug, Eq, PartialEq, Copy, Clone, PartialOrd, Hash)] +#[cfg_attr( + feature = "serialize", + derive(Deserialize, Serialize), + serde(tag = "type", content = "args") +)] +pub enum MouseEventKind { + /// Pressed mouse button. Contains the button that was pressed + Down(MouseButton), + /// Released mouse button. Contains the button that was released + Up(MouseButton), + /// Moved the mouse cursor while pressing the contained mouse button + Drag(MouseButton), + /// Moved / Hover changed without pressing any buttons + Moved, + /// Scrolled mouse wheel downwards + ScrollDown, + /// Scrolled mouse wheel upwards + ScrollUp, + /// Scrolled mouse wheel left + ScrollLeft, + /// Scrolled mouse wheel right + ScrollRight, +} + +/// A keyboard event +#[derive(Debug, Eq, PartialEq, Copy, Clone, PartialOrd, Hash)] +#[cfg_attr( + feature = "serialize", + derive(Deserialize, Serialize), + serde(tag = "type", content = "args") +)] +pub enum MouseButton { + /// Left mouse button. + Left, + /// Right mouse button. + Right, + /// Middle mouse button. + Middle, +} + #[cfg(test)] mod test { @@ -262,7 +332,7 @@ mod test { assert!(e.is_keyboard().is_some()); assert_eq!(e.is_window_resize(), false); assert_eq!(e.is_tick(), false); - assert_eq!(e.is_tick(), false); + assert_eq!(e.is_mouse().is_some(), false); assert!(e.is_user().is_none()); let e: Event = Event::WindowResize(0, 24); assert!(e.is_window_resize()); @@ -271,6 +341,17 @@ mod test { assert!(e.is_tick()); let e: Event = Event::User(MockEvent::Bar); assert_eq!(e.is_user().unwrap(), &MockEvent::Bar); + + let e: Event = Event::Mouse(MouseEvent { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 0, + row: 0, + }); + assert!(e.is_mouse().is_some()); + assert_eq!(e.is_keyboard().is_some(), false); + assert_eq!(e.is_tick(), false); + assert_eq!(e.is_window_resize(), false); } // -- serde diff --git a/src/core/subscription.rs b/src/core/subscription.rs index 0f77761..4865129 100644 --- a/src/core/subscription.rs +++ b/src/core/subscription.rs @@ -3,8 +3,9 @@ //! This module defines the model for the Subscriptions use std::hash::Hash; +use std::ops::Range; -use crate::event::KeyEvent; +use crate::event::{KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}; use crate::{AttrValue, Attribute, Event, State}; /// Public type to define a subscription. @@ -91,6 +92,25 @@ where } } +/// A event clause for [`MouseEvent`]s +#[derive(Debug, PartialEq, Eq)] +pub struct MouseEventClause { + /// The kind of mouse event that was caused + pub kind: MouseEventKind, + /// The key modifiers active when the event occurred + pub modifiers: KeyModifiers, + /// The column that the event occurred on + pub column: Range, + /// The row that the event occurred on + pub row: Range, +} + +impl MouseEventClause { + fn is_in_range(&self, ev: &MouseEvent) -> bool { + self.column.contains(&ev.column) && self.row.contains(&ev.row) + } +} + #[derive(Debug, PartialEq, Eq)] /// An event clause indicates on which kind of event the event must be forwarded to the `target` component. @@ -102,6 +122,8 @@ where Any, /// Check whether a certain key has been pressed Keyboard(KeyEvent), + /// Check whether a certain key has been pressed + Mouse(MouseEventClause), /// Check whether window has been resized WindowResize, /// The event will be forwarded on a tick @@ -121,6 +143,7 @@ where /// /// - Any: Forward, no matter what kind of event /// - Keyboard: everything must match + /// - Mouse: everything must match, column and row need to be within range /// - WindowResize: matches only event type, not sizes /// - Tick: matches tick event /// - None: matches None event @@ -129,6 +152,7 @@ where match self { EventClause::Any => true, EventClause::Keyboard(k) => Some(k) == ev.is_keyboard(), + EventClause::Mouse(m) => ev.is_mouse().map(|ev| m.is_in_range(ev)).unwrap_or(false), EventClause::WindowResize => ev.is_window_resize(), EventClause::Tick => ev.is_tick(), EventClause::User(u) => Some(u) == ev.is_user(), @@ -296,7 +320,7 @@ mod test { use super::*; use crate::command::Cmd; - use crate::event::Key; + use crate::event::{Key, KeyModifiers, MouseEventKind}; use crate::mock::{MockComponentId, MockEvent, MockFooInput}; use crate::{MockComponent, StateValue}; @@ -389,6 +413,71 @@ mod test { EventClause::::Keyboard(KeyEvent::from(Key::Enter)).forward(&Event::Tick), false ); + assert_eq!( + EventClause::::Keyboard(KeyEvent::from(Key::Enter)).forward(&Event::Mouse( + MouseEvent { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 0, + row: 0 + } + )), + false + ); + } + + #[test] + fn event_clause_mouse_should_forward() { + assert_eq!( + EventClause::::Mouse(MouseEventClause { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 0..10, + row: 0..10 + }) + .forward(&Event::Mouse(MouseEvent { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 0, + row: 0 + })), + true + ); + assert_eq!( + EventClause::::Mouse(MouseEventClause { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 0..10, + row: 0..10 + }) + .forward(&Event::Mouse(MouseEvent { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 20, + row: 20 + })), + false + ); + assert_eq!( + EventClause::::Mouse(MouseEventClause { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 0..10, + row: 0..10 + }) + .forward(&Event::Keyboard(KeyEvent::from(Key::Backspace))), + false + ); + assert_eq!( + EventClause::::Mouse(MouseEventClause { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 0..10, + row: 0..10 + }) + .forward(&Event::Tick), + false + ); } #[test] diff --git a/src/terminal/event_listener/crossterm.rs b/src/terminal/event_listener/crossterm.rs index b8b6f0e..1fdd2d8 100644 --- a/src/terminal/event_listener/crossterm.rs +++ b/src/terminal/event_listener/crossterm.rs @@ -4,11 +4,14 @@ use std::time::Duration; use crossterm::event::{ self as xterm, Event as XtermEvent, KeyCode as XtermKeyCode, KeyEvent as XtermKeyEvent, KeyEventKind as XtermEventKind, KeyModifiers as XtermKeyModifiers, - MediaKeyCode as XtermMediaKeyCode, + MediaKeyCode as XtermMediaKeyCode, MouseButton as XtermMouseButton, + MouseEvent as XtermMouseEvent, MouseEventKind as XtermMouseEventKind, }; use super::Event; -use crate::event::{Key, KeyEvent, KeyModifiers, MediaKeyCode}; +use crate::event::{ + Key, KeyEvent, KeyModifiers, MediaKeyCode, MouseButton, MouseEvent, MouseEventKind, +}; use crate::listener::{ListenerResult, Poll}; use crate::ListenerError; @@ -59,7 +62,7 @@ where match e { XtermEvent::Key(key) if key.kind == XtermEventKind::Press => Self::Keyboard(key.into()), XtermEvent::Key(_) => Self::None, - XtermEvent::Mouse(_) => Self::None, + XtermEvent::Mouse(ev) => Self::Mouse(ev.into()), XtermEvent::Resize(w, h) => Self::WindowResize(w, h), XtermEvent::FocusGained => Self::FocusGained, XtermEvent::FocusLost => Self::FocusLost, @@ -146,6 +149,42 @@ impl From for MediaKeyCode { } } +impl From for MouseEvent { + fn from(value: XtermMouseEvent) -> Self { + Self { + kind: value.kind.into(), + modifiers: value.modifiers.into(), + column: value.column, + row: value.row, + } + } +} + +impl From for MouseEventKind { + fn from(value: XtermMouseEventKind) -> Self { + match value { + XtermMouseEventKind::Down(b) => Self::Down(b.into()), + XtermMouseEventKind::Up(b) => Self::Up(b.into()), + XtermMouseEventKind::Drag(b) => Self::Drag(b.into()), + XtermMouseEventKind::Moved => Self::Moved, + XtermMouseEventKind::ScrollDown => Self::ScrollDown, + XtermMouseEventKind::ScrollUp => Self::ScrollUp, + XtermMouseEventKind::ScrollLeft => Self::ScrollLeft, + XtermMouseEventKind::ScrollRight => Self::ScrollRight, + } + } +} + +impl From for MouseButton { + fn from(value: XtermMouseButton) -> Self { + match value { + XtermMouseButton::Left => Self::Left, + XtermMouseButton::Right => Self::Right, + XtermMouseButton::Middle => Self::Middle, + } + } +} + #[cfg(test)] mod test { @@ -248,6 +287,122 @@ mod test { ); } + #[test] + fn should_adapt_mouse_event() { + assert_eq!( + MouseEvent::from(XtermMouseEvent { + kind: XtermMouseEventKind::Moved, + column: 1, + row: 1, + modifiers: XtermKeyModifiers::NONE + }), + MouseEvent { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 1, + row: 1 + } + ); + assert_eq!( + MouseEvent::from(XtermMouseEvent { + kind: XtermMouseEventKind::Down(XtermMouseButton::Left), + column: 1, + row: 1, + modifiers: XtermKeyModifiers::NONE + }), + MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + modifiers: KeyModifiers::NONE, + column: 1, + row: 1 + } + ); + assert_eq!( + MouseEvent::from(XtermMouseEvent { + kind: XtermMouseEventKind::Up(XtermMouseButton::Right), + column: 1, + row: 1, + modifiers: XtermKeyModifiers::NONE + }), + MouseEvent { + kind: MouseEventKind::Up(MouseButton::Right), + modifiers: KeyModifiers::NONE, + column: 1, + row: 1 + } + ); + assert_eq!( + MouseEvent::from(XtermMouseEvent { + kind: XtermMouseEventKind::Drag(XtermMouseButton::Middle), + column: 1, + row: 1, + modifiers: XtermKeyModifiers::NONE + }), + MouseEvent { + kind: MouseEventKind::Drag(MouseButton::Middle), + modifiers: KeyModifiers::NONE, + column: 1, + row: 1 + } + ); + assert_eq!( + MouseEvent::from(XtermMouseEvent { + kind: XtermMouseEventKind::ScrollUp, + column: 1, + row: 1, + modifiers: XtermKeyModifiers::NONE + }), + MouseEvent { + kind: MouseEventKind::ScrollUp, + modifiers: KeyModifiers::NONE, + column: 1, + row: 1 + } + ); + assert_eq!( + MouseEvent::from(XtermMouseEvent { + kind: XtermMouseEventKind::ScrollDown, + column: 1, + row: 1, + modifiers: XtermKeyModifiers::NONE + }), + MouseEvent { + kind: MouseEventKind::ScrollDown, + modifiers: KeyModifiers::NONE, + column: 1, + row: 1 + } + ); + assert_eq!( + MouseEvent::from(XtermMouseEvent { + kind: XtermMouseEventKind::ScrollLeft, + column: 1, + row: 1, + modifiers: XtermKeyModifiers::NONE + }), + MouseEvent { + kind: MouseEventKind::ScrollLeft, + modifiers: KeyModifiers::NONE, + column: 1, + row: 1 + } + ); + assert_eq!( + MouseEvent::from(XtermMouseEvent { + kind: XtermMouseEventKind::ScrollRight, + column: 1, + row: 1, + modifiers: XtermKeyModifiers::NONE + }), + MouseEvent { + kind: MouseEventKind::ScrollRight, + modifiers: KeyModifiers::NONE, + column: 1, + row: 1 + } + ); + } + #[test] fn adapt_crossterm_key_event() { assert_eq!( @@ -279,7 +434,12 @@ mod test { row: 0, modifiers: XtermKeyModifiers::NONE, })), - Event::None + Event::Mouse(MouseEvent { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 0, + row: 0 + }) ); assert_eq!( AppEvent::from(XtermEvent::FocusGained),