use cranpose_ui_graphics::Point;
use std::cell::Cell;
use std::rc::Rc;
pub type PointerId = u64;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PointerPhase {
Start,
Move,
End,
Cancel,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PointerEventKind {
Down,
Move,
Up,
Cancel,
Scroll,
Zoom,
Enter,
Exit,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum PointerSource {
Mouse,
Touch,
Stylus,
#[default]
Unknown,
}
impl PointerSource {
pub fn is_touch_like(self) -> bool {
matches!(self, PointerSource::Touch | PointerSource::Stylus)
}
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PointerButton {
Primary = 0,
Secondary = 1,
Middle = 2,
Back = 3,
Forward = 4,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PointerButtons(u8);
impl PointerButtons {
pub const NONE: Self = Self(0);
pub fn new() -> Self {
Self::NONE
}
pub fn with(mut self, button: PointerButton) -> Self {
self.insert(button);
self
}
pub fn insert(&mut self, button: PointerButton) {
self.0 |= 1 << (button as u8);
}
pub fn remove(&mut self, button: PointerButton) {
self.0 &= !(1 << (button as u8));
}
pub fn contains(&self, button: PointerButton) -> bool {
(self.0 & (1 << (button as u8))) != 0
}
}
impl Default for PointerButtons {
fn default() -> Self {
Self::NONE
}
}
#[derive(Clone, Debug)]
pub struct PointerEvent {
pub id: PointerId,
pub kind: PointerEventKind,
pub phase: PointerPhase,
pub position: Point,
pub global_position: Point,
pub scroll_delta: Point,
pub buttons: PointerButtons,
pub time_ms: Option<i64>,
pub animation_time_nanos: Option<u64>,
pub zoom_delta: f32,
pub source: PointerSource,
consumed: Rc<Cell<bool>>,
}
impl PointerEvent {
pub fn new(kind: PointerEventKind, position: Point, global_position: Point) -> Self {
Self {
id: 0,
kind,
phase: match kind {
PointerEventKind::Down => PointerPhase::Start,
PointerEventKind::Move | PointerEventKind::Enter | PointerEventKind::Exit => {
PointerPhase::Move
}
PointerEventKind::Up => PointerPhase::End,
PointerEventKind::Cancel => PointerPhase::Cancel,
PointerEventKind::Scroll | PointerEventKind::Zoom => PointerPhase::Move,
},
position,
global_position,
scroll_delta: Point { x: 0.0, y: 0.0 },
buttons: PointerButtons::NONE,
time_ms: None,
animation_time_nanos: None,
zoom_delta: 1.0,
source: PointerSource::Unknown,
consumed: Rc::new(Cell::new(false)),
}
}
pub fn with_id(mut self, id: PointerId) -> Self {
self.id = id;
self
}
pub fn with_zoom_delta(mut self, zoom_delta: f32) -> Self {
self.zoom_delta = zoom_delta;
self
}
pub fn with_scroll_delta(mut self, scroll_delta: Point) -> Self {
self.scroll_delta = scroll_delta;
self
}
pub fn with_time_ms(mut self, time_ms: Option<i64>) -> Self {
self.time_ms = time_ms;
self
}
pub fn with_animation_time_nanos(mut self, time_nanos: u64) -> Self {
self.animation_time_nanos = Some(time_nanos);
self
}
pub fn with_buttons(mut self, buttons: PointerButtons) -> Self {
self.buttons = buttons;
self
}
pub fn with_source(mut self, source: PointerSource) -> Self {
self.source = source;
self
}
pub fn consume(&self) {
self.consumed.set(true);
}
pub fn is_consumed(&self) -> bool {
self.consumed.get()
}
pub fn copy_with_local_position(&self, position: Point) -> Self {
Self {
id: self.id,
kind: self.kind,
phase: self.phase,
position,
global_position: self.global_position,
scroll_delta: self.scroll_delta,
buttons: self.buttons,
time_ms: self.time_ms,
animation_time_nanos: self.animation_time_nanos,
zoom_delta: self.zoom_delta,
source: self.source,
consumed: self.consumed.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn point(x: f32, y: f32) -> Point {
Point { x, y }
}
#[test]
fn pointer_event_clones_share_consumed_state() {
let event = PointerEvent::new(PointerEventKind::Move, point(1.0, 2.0), point(3.0, 4.0));
let cloned = event.clone();
assert!(!event.is_consumed());
assert!(!cloned.is_consumed());
cloned.consume();
assert!(event.is_consumed());
assert!(cloned.is_consumed());
}
#[test]
fn pointer_event_source_defaults_unknown_and_threads_through_copy() {
let event = PointerEvent::new(PointerEventKind::Down, point(1.0, 1.0), point(1.0, 1.0));
assert_eq!(event.source, PointerSource::Unknown);
assert!(!PointerSource::Unknown.is_touch_like());
let touch = event.with_source(PointerSource::Touch);
assert_eq!(touch.source, PointerSource::Touch);
assert!(PointerSource::Touch.is_touch_like());
assert!(PointerSource::Stylus.is_touch_like());
assert!(!PointerSource::Mouse.is_touch_like());
let local = touch.copy_with_local_position(point(5.0, 5.0));
assert_eq!(local.source, PointerSource::Touch);
}
#[test]
fn pointer_event_copy_with_local_position_preserves_consumption_state() {
let event = PointerEvent::new(PointerEventKind::Down, point(4.0, 5.0), point(4.0, 5.0))
.with_time_ms(Some(123))
.with_animation_time_nanos(456_000_000);
let local = event.copy_with_local_position(point(1.0, 1.0));
assert_eq!(local.position, point(1.0, 1.0));
assert_eq!(local.global_position, event.global_position);
assert_eq!(local.time_ms, Some(123));
assert_eq!(local.animation_time_nanos, Some(456_000_000));
assert!(!local.is_consumed());
event.consume();
assert!(local.is_consumed());
}
}