mod dispatch;
pub use dispatch::{
EventHandler, OnPointerEntered, OnPointerExited, OnPointerMoved, OnPointerPressed,
OnPointerReleased, Phase, PointerEventHandler, build_bubble_path, dispatch_event_for_handler,
dispatch_pointer_events,
};
use bevy_ecs::prelude::*;
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::time::Instant;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PhysicalPoint {
pub x: i32,
pub y: i32,
}
impl PhysicalPoint {
#[inline]
pub fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DoubleClick {
#[default]
None,
Left,
Right,
Middle,
XButton1,
XButton2,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct WheelDelta {
pub vertical: i16,
pub horizontal: i16,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CursorVelocity {
pub x: f32,
pub y: f32,
pub magnitude: f32,
}
impl CursorVelocity {
#[inline]
pub fn new(x: f32, y: f32) -> Self {
Self {
x,
y,
magnitude: (x * x + y * y).sqrt(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PointerButton {
Left,
Right,
Middle,
XButton1,
XButton2,
}
#[deprecated(since = "0.1.0", note = "Use PointerButton instead")]
pub type MouseButton = PointerButton;
#[derive(Component, Debug, Clone)]
#[component(storage = "SparseSet")]
pub struct PointerState {
pub client_point: PhysicalPoint,
pub local_point: PhysicalPoint,
pub left_down: bool,
pub right_down: bool,
pub middle_down: bool,
pub xbutton1_down: bool,
pub xbutton2_down: bool,
pub shift_down: bool,
pub ctrl_down: bool,
pub double_click: DoubleClick,
pub wheel: WheelDelta,
pub velocity: CursorVelocity,
pub timestamp: Instant,
}
impl Default for PointerState {
fn default() -> Self {
Self {
client_point: PhysicalPoint::default(),
local_point: PhysicalPoint::default(),
left_down: false,
right_down: false,
middle_down: false,
xbutton1_down: false,
xbutton2_down: false,
shift_down: false,
ctrl_down: false,
double_click: DoubleClick::None,
wheel: WheelDelta::default(),
velocity: CursorVelocity::default(),
timestamp: Instant::now(),
}
}
}
#[deprecated(since = "0.1.0", note = "Use PointerState instead")]
pub type MouseState = PointerState;
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
#[component(storage = "SparseSet")]
pub struct PointerLeave;
#[deprecated(since = "0.1.0", note = "Use PointerLeave instead")]
pub type MouseLeave = PointerLeave;
#[derive(Component, Debug, Clone, Copy, Default)]
pub struct WindowPointerTracking(pub bool);
#[deprecated(since = "0.1.0", note = "Use WindowPointerTracking instead")]
pub type WindowMouseTracking = WindowPointerTracking;
#[derive(Debug, Clone, Copy)]
pub struct PositionSample {
pub x: f32,
pub y: f32,
pub timestamp: Instant,
}
#[derive(Debug, Default)]
pub struct PointerBuffer {
samples: VecDeque<PositionSample>,
}
impl PointerBuffer {
const MAX_SAMPLES: usize = 5;
pub fn new() -> Self {
Self {
samples: VecDeque::with_capacity(Self::MAX_SAMPLES),
}
}
pub fn push(&mut self, sample: PositionSample) {
if self.samples.len() >= Self::MAX_SAMPLES {
self.samples.pop_front();
}
self.samples.push_back(sample);
}
pub fn latest(&self) -> Option<&PositionSample> {
self.samples.back()
}
pub fn len(&self) -> usize {
self.samples.len()
}
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
pub fn clear(&mut self) {
self.samples.clear()
}
pub fn calculate_velocity(&self) -> (f32, f32) {
if self.samples.len() < 2 {
return (0.0, 0.0);
}
let newest = self.samples.back().unwrap();
let prev = &self.samples[self.samples.len() - 2];
let dt = newest
.timestamp
.duration_since(prev.timestamp)
.as_secs_f32();
if dt < 0.0001 {
return (0.0, 0.0);
}
((newest.x - prev.x) / dt, (newest.y - prev.y) / dt)
}
}
#[deprecated(since = "0.1.0", note = "Use PointerBuffer instead")]
pub type MouseBuffer = PointerBuffer;
#[derive(Debug, Clone, Copy, Default)]
pub struct ButtonBuffer {
pub down_received: bool,
pub up_received: bool,
}
impl ButtonBuffer {
pub fn record_down(&mut self) {
self.down_received = true;
}
pub fn record_up(&mut self) {
self.up_received = true;
}
pub fn reset(&mut self) {
self.down_received = false;
self.up_received = false;
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct WheelBuffer {
pub vertical: i16,
pub horizontal: i16,
}
impl WheelBuffer {
pub fn add_vertical(&mut self, delta: i16) {
self.vertical = self.vertical.saturating_add(delta);
}
pub fn add_horizontal(&mut self, delta: i16) {
self.horizontal = self.horizontal.saturating_add(delta);
}
pub fn reset(&mut self) {
self.vertical = 0;
self.horizontal = 0;
}
}
thread_local! {
pub(crate) static POINTER_BUFFERS: RefCell<HashMap<Entity, PointerBuffer>> = RefCell::new(HashMap::new());
pub(crate) static BUTTON_BUFFERS: RefCell<HashMap<(Entity, PointerButton), ButtonBuffer>> = RefCell::new(HashMap::new());
pub(crate) static WHEEL_BUFFERS: RefCell<HashMap<Entity, WheelBuffer>> = RefCell::new(HashMap::new());
pub(crate) static DOUBLE_CLICK_BUFFERS: RefCell<HashMap<Entity, DoubleClick>> = RefCell::new(HashMap::new());
pub(crate) static MODIFIER_STATE: RefCell<HashMap<Entity, (bool, bool)>> = RefCell::new(HashMap::new());
}
#[inline]
pub fn hit_test_placeholder(
_world: &bevy_ecs::world::World,
window_entity: Entity,
_position: (f32, f32),
) -> Option<Entity> {
Some(window_entity)
}
#[inline]
pub fn hit_test_with_local_coords(
_world: &bevy_ecs::world::World,
window_entity: Entity,
screen_x: i32,
screen_y: i32,
) -> Option<(Entity, PhysicalPoint)> {
Some((window_entity, PhysicalPoint::new(screen_x, screen_y)))
}
pub fn process_pointer_buffers(mut query: Query<(Entity, &mut PointerState)>) {
tracing::trace!("[process_pointer_buffers] Called");
BUTTON_BUFFERS.with(|buffers| {
let buffers = buffers.borrow();
for (entity, mut pointer) in query.iter_mut() {
for button in [
PointerButton::Left,
PointerButton::Right,
PointerButton::Middle,
PointerButton::XButton1,
PointerButton::XButton2,
] {
if let Some(buf) = buffers.get(&(entity, button)) {
let is_down = if buf.down_received {
true
} else if buf.up_received {
false
} else {
match button {
PointerButton::Left => pointer.left_down,
PointerButton::Right => pointer.right_down,
PointerButton::Middle => pointer.middle_down,
PointerButton::XButton1 => pointer.xbutton1_down,
PointerButton::XButton2 => pointer.xbutton2_down,
}
};
match button {
PointerButton::Left => pointer.left_down = is_down,
PointerButton::Right => pointer.right_down = is_down,
PointerButton::Middle => pointer.middle_down = is_down,
PointerButton::XButton1 => pointer.xbutton1_down = is_down,
PointerButton::XButton2 => pointer.xbutton2_down = is_down,
}
if buf.down_received || buf.up_received {
tracing::trace!(
entity = ?entity,
button = ?button,
is_down,
"[process_pointer_buffers] Button state updated"
);
}
}
}
}
});
for (entity, mut pointer) in query.iter_mut() {
tracing::trace!(
entity = ?entity,
thread_id = ?std::thread::current().id(),
"[process_pointer_buffers] Checking POINTER_BUFFERS"
);
POINTER_BUFFERS.with(|buffers| {
let mut buffers = buffers.borrow_mut();
if let Some(buffer) = buffers.get_mut(&entity) {
tracing::trace!(
entity = ?entity,
"[process_pointer_buffers] Buffer found"
);
let (vx, vy) = buffer.calculate_velocity();
pointer.velocity = CursorVelocity::new(vx, vy);
if let Some(sample) = buffer.latest() {
let old_x = pointer.client_point.x;
let old_y = pointer.client_point.y;
pointer.client_point = PhysicalPoint::new(sample.x as i32, sample.y as i32);
pointer.local_point = pointer.client_point;
tracing::trace!(
entity = ?entity,
old_x, old_y,
new_x = pointer.client_point.x,
new_y = pointer.client_point.y,
"[process_pointer_buffers] Position updated"
);
}
buffer.clear();
} else {
tracing::trace!(
entity = ?entity,
"[process_pointer_buffers] No buffer found"
);
}
});
WHEEL_BUFFERS.with(|buffers| {
let mut buffers = buffers.borrow_mut();
if let Some(buf) = buffers.get_mut(&entity) {
pointer.wheel = WheelDelta {
vertical: buf.vertical,
horizontal: buf.horizontal,
};
buf.reset();
}
});
DOUBLE_CLICK_BUFFERS.with(|buffers| {
let mut buffers = buffers.borrow_mut();
if let Some(dc) = buffers.remove(&entity) {
pointer.double_click = dc;
}
});
MODIFIER_STATE.with(|state| {
let state = state.borrow();
if let Some(&(shift, ctrl)) = state.get(&entity) {
pointer.shift_down = shift;
pointer.ctrl_down = ctrl;
}
});
pointer.timestamp = Instant::now();
}
}
#[deprecated(since = "0.1.0", note = "Use process_pointer_buffers instead")]
pub fn process_mouse_buffers(query: Query<(Entity, &mut PointerState)>) {
process_pointer_buffers(query);
}
pub fn clear_transient_pointer_state(
mut query: Query<&mut PointerState>,
mut commands: Commands,
leave_query: Query<Entity, With<PointerLeave>>,
) {
for mut pointer in query.iter_mut() {
pointer.double_click = DoubleClick::None;
pointer.wheel = WheelDelta::default();
}
for entity in leave_query.iter() {
commands.entity(entity).remove::<PointerLeave>();
}
}
#[deprecated(since = "0.1.0", note = "Use clear_transient_pointer_state instead")]
pub fn clear_transient_mouse_state(
query: Query<&mut PointerState>,
commands: Commands,
leave_query: Query<Entity, With<PointerLeave>>,
) {
clear_transient_pointer_state(query, commands, leave_query);
}
pub fn debug_pointer_state_changes(
added_query: Query<(Entity, &PointerState), Added<PointerState>>,
changed_query: Query<(Entity, &PointerState), Changed<PointerState>>,
) {
use tracing::debug;
for (entity, pointer) in added_query.iter() {
debug!(
entity = ?entity,
client_x = pointer.client_point.x,
client_y = pointer.client_point.y,
left = pointer.left_down,
right = pointer.right_down,
middle = pointer.middle_down,
shift = pointer.shift_down,
ctrl = pointer.ctrl_down,
"[PointerState Added] Enter detected"
);
}
for (entity, pointer) in changed_query.iter() {
if pointer.double_click != DoubleClick::None {
debug!(
entity = ?entity,
double_click = ?pointer.double_click,
"[PointerState Changed] DoubleClick detected"
);
}
if pointer.wheel.vertical != 0 || pointer.wheel.horizontal != 0 {
debug!(
entity = ?entity,
vertical = pointer.wheel.vertical,
horizontal = pointer.wheel.horizontal,
"[PointerState Changed] Wheel detected"
);
}
}
}
#[deprecated(since = "0.1.0", note = "Use debug_pointer_state_changes instead")]
pub fn debug_mouse_state_changes(
added_query: Query<(Entity, &PointerState), Added<PointerState>>,
changed_query: Query<(Entity, &PointerState), Changed<PointerState>>,
) {
debug_pointer_state_changes(added_query, changed_query);
}
pub fn debug_pointer_leave(leave_query: Query<Entity, Added<PointerLeave>>) {
use tracing::debug;
for entity in leave_query.iter() {
debug!(
entity = ?entity,
"[PointerLeave Added] Leave detected"
);
}
}
#[deprecated(since = "0.1.0", note = "Use debug_pointer_leave instead")]
pub fn debug_mouse_leave(leave_query: Query<Entity, Added<PointerLeave>>) {
debug_pointer_leave(leave_query);
}
#[inline]
pub(crate) fn push_pointer_sample(entity: Entity, x: f32, y: f32, timestamp: Instant) {
tracing::trace!(
entity = ?entity,
x, y,
thread_id = ?std::thread::current().id(),
"[push_pointer_sample] Sample added"
);
POINTER_BUFFERS.with(|buffers| {
let mut buffers = buffers.borrow_mut();
let buffer = buffers.entry(entity).or_insert_with(PointerBuffer::new);
buffer.push(PositionSample { x, y, timestamp });
});
}
#[allow(dead_code)]
#[inline]
pub(crate) fn push_mouse_sample(entity: Entity, x: f32, y: f32, timestamp: Instant) {
push_pointer_sample(entity, x, y, timestamp);
}
#[inline]
pub(crate) fn record_button_down(entity: Entity, button: PointerButton) {
BUTTON_BUFFERS.with(|buffers| {
let mut buffers = buffers.borrow_mut();
let buffer = buffers.entry((entity, button)).or_default();
buffer.record_down();
tracing::info!(
entity = ?entity,
button = ?button,
"[ButtonBuffer] record_button_down"
);
});
}
#[inline]
pub(crate) fn record_button_up(entity: Entity, button: PointerButton) {
BUTTON_BUFFERS.with(|buffers| {
let mut buffers = buffers.borrow_mut();
let buffer = buffers.entry((entity, button)).or_default();
buffer.record_up();
tracing::info!(
entity = ?entity,
button = ?button,
"[ButtonBuffer] record_button_up"
);
});
}
#[inline]
pub(crate) fn add_wheel_vertical(entity: Entity, delta: i16) {
WHEEL_BUFFERS.with(|buffers| {
let mut buffers = buffers.borrow_mut();
let buffer = buffers.entry(entity).or_default();
buffer.add_vertical(delta);
});
}
#[inline]
pub(crate) fn add_wheel_horizontal(entity: Entity, delta: i16) {
WHEEL_BUFFERS.with(|buffers| {
let mut buffers = buffers.borrow_mut();
let buffer = buffers.entry(entity).or_default();
buffer.add_horizontal(delta);
});
}
#[inline]
pub(crate) fn set_modifier_state(entity: Entity, shift: bool, ctrl: bool) {
MODIFIER_STATE.with(|state| {
let mut state = state.borrow_mut();
state.insert(entity, (shift, ctrl));
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pointer_buffer_push() {
let mut buffer = PointerBuffer::new();
assert!(buffer.is_empty());
buffer.push(PositionSample {
x: 10.0,
y: 20.0,
timestamp: Instant::now(),
});
assert_eq!(buffer.len(), 1);
assert!(!buffer.is_empty());
}
#[test]
fn test_pointer_buffer_max_samples() {
let mut buffer = PointerBuffer::new();
for i in 0..10 {
buffer.push(PositionSample {
x: i as f32,
y: i as f32,
timestamp: Instant::now(),
});
}
assert_eq!(buffer.len(), PointerBuffer::MAX_SAMPLES);
let latest = buffer.latest().unwrap();
assert_eq!(latest.x, 9.0);
}
#[test]
fn test_velocity_calculation() {
let mut buffer = PointerBuffer::new();
let t1 = Instant::now();
buffer.push(PositionSample {
x: 0.0,
y: 0.0,
timestamp: t1,
});
let (vx, vy) = buffer.calculate_velocity();
assert_eq!(vx, 0.0);
assert_eq!(vy, 0.0);
}
#[test]
fn test_button_buffer_state() {
let mut buffer = ButtonBuffer::default();
assert!(!buffer.down_received);
assert!(!buffer.up_received);
buffer.record_down();
assert!(buffer.down_received);
assert!(!buffer.up_received);
buffer.record_up();
assert!(buffer.down_received);
assert!(buffer.up_received);
buffer.reset();
assert!(!buffer.down_received);
assert!(!buffer.up_received);
}
#[test]
fn test_wheel_buffer() {
let mut buffer = WheelBuffer::default();
assert_eq!(buffer.vertical, 0);
assert_eq!(buffer.horizontal, 0);
buffer.add_vertical(120);
buffer.add_vertical(120);
assert_eq!(buffer.vertical, 240);
buffer.add_horizontal(-60);
assert_eq!(buffer.horizontal, -60);
buffer.reset();
assert_eq!(buffer.vertical, 0);
assert_eq!(buffer.horizontal, 0);
}
#[test]
fn test_cursor_velocity_new() {
let v = CursorVelocity::new(3.0, 4.0);
assert_eq!(v.x, 3.0);
assert_eq!(v.y, 4.0);
assert_eq!(v.magnitude, 5.0); }
#[test]
fn test_pointer_state_default() {
let state = PointerState::default();
assert_eq!(state.client_point, PhysicalPoint::default());
assert_eq!(state.local_point, PhysicalPoint::default());
assert!(!state.left_down);
assert!(!state.right_down);
assert!(!state.middle_down);
assert!(!state.xbutton1_down);
assert!(!state.xbutton2_down);
assert!(!state.shift_down);
assert!(!state.ctrl_down);
assert_eq!(state.double_click, DoubleClick::None);
assert_eq!(state.wheel, WheelDelta::default());
}
#[test]
fn test_pointer_leave_marker() {
let leave1 = PointerLeave;
let leave2 = PointerLeave;
assert_eq!(leave1, leave2);
}
#[test]
fn test_window_pointer_tracking_default() {
let tracking = WindowPointerTracking::default();
assert!(!tracking.0);
let tracking_enabled = WindowPointerTracking(true);
assert!(tracking_enabled.0);
}
#[test]
fn test_physical_point_new() {
let pt = PhysicalPoint::new(100, 200);
assert_eq!(pt.x, 100);
assert_eq!(pt.y, 200);
}
#[test]
fn test_double_click_variants() {
assert_eq!(DoubleClick::default(), DoubleClick::None);
assert_ne!(DoubleClick::Left, DoubleClick::Right);
assert_ne!(DoubleClick::Middle, DoubleClick::XButton1);
assert_ne!(DoubleClick::XButton1, DoubleClick::XButton2);
}
#[test]
fn test_wheel_delta_default() {
let delta = WheelDelta::default();
assert_eq!(delta.vertical, 0);
assert_eq!(delta.horizontal, 0);
}
#[test]
fn test_pointer_button_enum() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(PointerButton::Left);
set.insert(PointerButton::Right);
set.insert(PointerButton::Middle);
set.insert(PointerButton::XButton1);
set.insert(PointerButton::XButton2);
assert_eq!(set.len(), 5);
}
}
pub(crate) fn transfer_buffers_to_world(world: &mut World) {
POINTER_BUFFERS.with(|buffers| {
let mut buffers = buffers.borrow_mut();
for (entity, buffer) in buffers.iter_mut() {
if let Some(sample) = buffer.latest() {
let (vx, vy) = buffer.calculate_velocity();
if let Some(mut pointer_state) = world.get_mut::<PointerState>(*entity) {
pointer_state.client_point =
PhysicalPoint::new(sample.x as i32, sample.y as i32);
pointer_state.local_point = pointer_state.client_point;
pointer_state.velocity = CursorVelocity::new(vx, vy);
tracing::trace!(
entity = ?entity,
x = sample.x,
y = sample.y,
"[transfer_buffers_to_world] PointerState updated"
);
}
}
buffer.clear();
}
});
BUTTON_BUFFERS.with(|buffers| {
let buffers = buffers.borrow();
for ((entity, button), buf) in buffers.iter() {
if buf.down_received {
if let Some(mut pointer_state) = world.get_mut::<PointerState>(*entity) {
match button {
PointerButton::Left => pointer_state.left_down = true,
PointerButton::Right => pointer_state.right_down = true,
PointerButton::Middle => pointer_state.middle_down = true,
PointerButton::XButton1 => pointer_state.xbutton1_down = true,
PointerButton::XButton2 => pointer_state.xbutton2_down = true,
}
tracing::trace!(
entity = ?entity,
button = ?button,
"[transfer_buffers_to_world] Button pressed"
);
}
} else if buf.up_received {
if let Some(mut pointer_state) = world.get_mut::<PointerState>(*entity) {
match button {
PointerButton::Left => pointer_state.left_down = false,
PointerButton::Right => pointer_state.right_down = false,
PointerButton::Middle => pointer_state.middle_down = false,
PointerButton::XButton1 => pointer_state.xbutton1_down = false,
PointerButton::XButton2 => pointer_state.xbutton2_down = false,
}
tracing::trace!(
entity = ?entity,
button = ?button,
"[transfer_buffers_to_world] Button released"
);
}
}
}
});
BUTTON_BUFFERS.with(|buffers| {
let mut buffers = buffers.borrow_mut();
for buf in buffers.values_mut() {
buf.reset();
}
});
MODIFIER_STATE.with(|state| {
let state = state.borrow();
for (entity, (shift_down, ctrl_down)) in state.iter() {
if let Some(mut pointer_state) = world.get_mut::<PointerState>(*entity) {
pointer_state.shift_down = *shift_down;
pointer_state.ctrl_down = *ctrl_down;
}
}
});
}