use core::{fmt::Debug, time::Duration};
use std::collections::HashSet;
use bevy_camera::NormalizedRenderTarget;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
entity::{EntityHashMap, EntityHashSet},
prelude::*,
query::QueryData,
system::SystemParam,
traversal::Traversal,
};
use bevy_input::{mouse::MouseScrollUnit, touch::TouchPhase};
use bevy_math::Vec2;
use bevy_platform::collections::HashMap;
use bevy_platform::time::Instant;
use bevy_reflect::prelude::*;
use bevy_window::Window;
use tracing::debug;
use crate::{
backend::{prelude::PointerLocation, HitData},
hover::{get_hovered_entities, is_directly_hovered, HoverMap, PreviousHoverMap},
pointer::{Location, PointerAction, PointerButton, PointerId, PointerInput, PointerMap},
};
pub const MULTI_CLICK_DURATION: Duration = Duration::from_millis(500);
#[derive(Message, EntityEvent, Clone, PartialEq, Debug, Reflect, Component)]
#[entity_event(propagate = PointerTraversal, auto_propagate)]
#[reflect(Component, Debug, Clone)]
pub struct Pointer<E: Debug + Clone + Reflect> {
pub entity: Entity,
pub pointer_id: PointerId,
pub pointer_location: Location,
pub event: E,
pub(crate) propagate: bool,
}
#[derive(QueryData)]
pub struct PointerTraversal {
child_of: Option<&'static ChildOf>,
window: Option<&'static Window>,
}
impl<E> Traversal<Pointer<E>> for PointerTraversal
where
E: Debug + Clone + Reflect,
{
fn traverse(item: Self::Item<'_, '_>, pointer: &Pointer<E>) -> Option<Entity> {
if !pointer.propagate {
return None;
}
let PointerTraversalItem { child_of, window } = item;
if let Some(child_of) = child_of {
return Some(child_of.parent());
};
if window.is_none()
&& let NormalizedRenderTarget::Window(window_ref) = pointer.pointer_location.target
{
return Some(window_ref.entity());
}
None
}
}
impl<E: Debug + Clone + Reflect> core::fmt::Display for Pointer<E> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_fmt(format_args!(
"{:?}, {:.1?}, {:.1?}",
self.pointer_id, self.pointer_location.position, self.event
))
}
}
impl<E: Debug + Clone + Reflect> core::ops::Deref for Pointer<E> {
type Target = E;
fn deref(&self) -> &Self::Target {
&self.event
}
}
impl<E: Debug + Clone + Reflect> Pointer<E> {
pub fn new(id: PointerId, location: Location, event: E, entity: Entity) -> Self {
Self::new_inner(id, location, event, entity, true)
}
pub fn new_without_propagate(
id: PointerId,
location: Location,
event: E,
entity: Entity,
) -> Self {
Self::new_inner(id, location, event, entity, false)
}
fn new_inner(
id: PointerId,
location: Location,
event: E,
entity: Entity,
propagate: bool,
) -> Self {
Self {
pointer_id: id,
pointer_location: location,
event,
entity,
propagate,
}
}
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct Cancel {
pub hit: HitData,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct Over {
pub hit: HitData,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct Enter {
pub hit: HitData,
pub is_in_bounds: bool,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct Out {
pub hit: HitData,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct Leave {
pub hit: HitData,
pub was_in_bounds: bool,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct Press {
pub button: PointerButton,
pub hit: HitData,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct Release {
pub button: PointerButton,
pub hit: HitData,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct Click {
pub button: PointerButton,
pub hit: HitData,
pub duration: Duration,
pub count: u8,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct Move {
pub hit: HitData,
pub delta: Vec2,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct DragStart {
pub button: PointerButton,
pub hit: HitData,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct Drag {
pub button: PointerButton,
pub distance: Vec2,
pub delta: Vec2,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct DragEnd {
pub button: PointerButton,
pub distance: Vec2,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct DragEnter {
pub button: PointerButton,
pub dragged: Entity,
pub hit: HitData,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct DragOver {
pub button: PointerButton,
pub dragged: Entity,
pub hit: HitData,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct DragLeave {
pub button: PointerButton,
pub dragged: Entity,
pub hit: HitData,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct DragDrop {
pub button: PointerButton,
pub dropped: Entity,
pub hit: HitData,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct DragEntry {
pub start_pos: Vec2,
pub latest_pos: Vec2,
}
#[derive(Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq)]
pub struct Scroll {
pub unit: MouseScrollUnit,
pub x: f32,
pub y: f32,
pub hit: HitData,
pub phase: TouchPhase,
}
#[derive(Debug, Clone, Default)]
pub struct PointerButtonState {
pub pressing: EntityHashMap<(Location, Instant, HitData)>,
pub clicking: EntityHashMap<(Instant, u8)>,
pub dragging: EntityHashMap<DragEntry>,
pub dragging_over: EntityHashMap<HitData>,
}
impl PointerButtonState {
pub fn clear(&mut self) {
self.pressing.clear();
self.dragging.clear();
self.dragging_over.clear();
}
}
#[derive(Debug, Clone, Default, Deref, DerefMut)]
pub struct HoveredEntityAncestors(EntityHashMap<EntityHashSet>);
impl HoveredEntityAncestors {
pub fn rebuild(
&mut self,
hover_map: &HoverMap,
pointer_state: &PointerState,
ancestors_query: &Query<&ChildOf>,
) {
self.clear();
for hovered_entity in hover_map
.iter()
.flat_map(|(_, hashmap)| hashmap.iter().map(|data| *data.0))
{
if self.contains_key(&hovered_entity) {
continue;
}
if let Some(previous_entry) =
pointer_state.hovered_entity_ancestors.get(&hovered_entity)
{
self.insert(hovered_entity, previous_entry.clone());
} else {
let mut ancestors = EntityHashSet::new();
for member in ancestors_query.iter_ancestors(hovered_entity) {
ancestors.insert(member);
}
self.insert(hovered_entity, ancestors);
}
}
}
pub fn get_ancestors_union(&self, hover_entities: &EntityHashSet) -> EntityHashSet {
hover_entities
.iter()
.flat_map(|entity| self.get(entity))
.flat_map(|set| set.iter().copied())
.collect::<EntityHashSet>()
}
pub fn get_ancestors(&self, hover_entity: &Entity) -> Option<&EntityHashSet> {
self.get(hover_entity)
}
}
#[derive(Debug, Clone, Default, Resource)]
pub struct PointerState {
pub pointer_buttons: HashMap<(PointerId, PointerButton), PointerButtonState>,
pub hovered_entity_ancestors: HoveredEntityAncestors,
}
impl PointerState {
pub fn get(&self, pointer_id: PointerId, button: PointerButton) -> Option<&PointerButtonState> {
self.pointer_buttons.get(&(pointer_id, button))
}
pub fn get_mut(
&mut self,
pointer_id: PointerId,
button: PointerButton,
) -> &mut PointerButtonState {
self.pointer_buttons
.entry((pointer_id, button))
.or_default()
}
pub fn get_ancestors(&self, hovered_entity: &Entity) -> Option<&EntityHashSet> {
self.hovered_entity_ancestors.get_ancestors(hovered_entity)
}
pub fn get_ancestors_union(&self, hovered_entities: &EntityHashSet) -> EntityHashSet {
self.hovered_entity_ancestors
.get_ancestors_union(hovered_entities)
}
pub fn clear(&mut self, pointer_id: PointerId) {
for button in PointerButton::iter() {
if let Some(state) = self.pointer_buttons.get_mut(&(pointer_id, button)) {
state.clear();
}
}
}
}
#[derive(SystemParam)]
pub struct PickingMessageWriters<'w> {
cancel_events: MessageWriter<'w, Pointer<Cancel>>,
click_events: MessageWriter<'w, Pointer<Click>>,
pressed_events: MessageWriter<'w, Pointer<Press>>,
drag_drop_events: MessageWriter<'w, Pointer<DragDrop>>,
drag_end_events: MessageWriter<'w, Pointer<DragEnd>>,
drag_enter_events: MessageWriter<'w, Pointer<DragEnter>>,
drag_events: MessageWriter<'w, Pointer<Drag>>,
drag_leave_events: MessageWriter<'w, Pointer<DragLeave>>,
drag_over_events: MessageWriter<'w, Pointer<DragOver>>,
drag_start_events: MessageWriter<'w, Pointer<DragStart>>,
scroll_events: MessageWriter<'w, Pointer<Scroll>>,
move_events: MessageWriter<'w, Pointer<Move>>,
out_events: MessageWriter<'w, Pointer<Out>>,
over_events: MessageWriter<'w, Pointer<Over>>,
leave_events: MessageWriter<'w, Pointer<Leave>>,
enter_events: MessageWriter<'w, Pointer<Enter>>,
released_events: MessageWriter<'w, Pointer<Release>>,
}
pub fn pointer_events(
mut input_events: MessageReader<PointerInput>,
pointers: Query<&PointerLocation>,
ancestors_query: Query<&ChildOf>,
pointer_map: Res<PointerMap>,
hover_map: Res<HoverMap>,
previous_hover_map: Res<PreviousHoverMap>,
mut pointer_state: ResMut<PointerState>,
mut hovered_entity_ancestors: Local<HoveredEntityAncestors>,
mut sent_leave: Local<HashSet<(PointerId, Entity)>>,
mut sent_enter: Local<HashSet<(PointerId, Entity)>>,
mut commands: Commands,
mut message_writers: PickingMessageWriters,
) {
let now = Instant::now();
let pointer_location = |pointer_id: PointerId| {
pointer_map
.get_entity(pointer_id)
.and_then(|entity| pointers.get(entity).ok())
.and_then(|pointer| pointer.location.clone())
};
hovered_entity_ancestors.rebuild(&hover_map, &pointer_state, &ancestors_query);
sent_leave.clear();
sent_enter.clear();
for (pointer_id, hovered_entity, hit) in previous_hover_map
.iter()
.flat_map(|(id, hashmap)| hashmap.iter().map(|data| (*id, *data.0, data.1.clone())))
{
if !hover_map
.get(&pointer_id)
.iter()
.any(|e| e.contains_key(&hovered_entity))
{
let Some(location) = pointer_location(pointer_id) else {
debug!(
"Unable to get location for pointer {:?} during pointer out",
pointer_id
);
continue;
};
let out_event = Pointer::new(
pointer_id,
location.clone(),
Out { hit: hit.clone() },
hovered_entity,
);
commands.trigger(out_event.clone());
message_writers.out_events.write(out_event);
let mut entities_to_send_leave =
pointer_state.get_ancestors(&hovered_entity).map_or_else(
|| {
ancestors_query
.iter_ancestors(hovered_entity)
.collect::<EntityHashSet>()
},
Clone::clone,
);
entities_to_send_leave.insert(hovered_entity);
entities_to_send_leave.retain(|entity| !sent_leave.contains(&(pointer_id, *entity)));
if !entities_to_send_leave.is_empty() {
let new_hovered_entities = get_hovered_entities(&hover_map, &pointer_id);
let new_hovered_ancestors =
hovered_entity_ancestors.get_ancestors_union(&new_hovered_entities);
let union = new_hovered_entities
.union(&new_hovered_ancestors)
.copied()
.collect::<EntityHashSet>();
entities_to_send_leave.retain(|entity| !union.contains(entity));
for leave_event in entities_to_send_leave.iter().map(|entity| {
Pointer::new_without_propagate(
pointer_id,
location.clone(),
Leave {
hit: hit.clone(),
was_in_bounds: is_directly_hovered(
&previous_hover_map.0,
&pointer_id,
entity,
),
},
*entity,
)
}) {
let entity = leave_event.entity;
commands.trigger(leave_event.clone());
message_writers.leave_events.write(leave_event);
sent_leave.insert((pointer_id, entity));
}
}
for button in PointerButton::iter() {
let state = pointer_state.get_mut(pointer_id, button);
state.dragging_over.remove(&hovered_entity);
for drag_target in state.dragging.keys() {
let drag_leave_event = Pointer::new(
pointer_id,
location.clone(),
DragLeave {
button,
dragged: *drag_target,
hit: hit.clone(),
},
hovered_entity,
);
commands.trigger(drag_leave_event.clone());
message_writers.drag_leave_events.write(drag_leave_event);
}
}
}
}
for (pointer_id, hovered_entity, hit) in hover_map
.iter()
.flat_map(|(id, hashmap)| hashmap.iter().map(|data| (*id, *data.0, data.1.clone())))
{
let Some(location) = pointer_location(pointer_id) else {
debug!(
"Unable to get location for pointer {:?} during pointer over",
pointer_id
);
continue;
};
for button in PointerButton::iter() {
let state = pointer_state.get_mut(pointer_id, button);
if !state.dragging.is_empty()
&& state
.dragging_over
.insert(hovered_entity, hit.clone())
.is_none()
{
for drag_target in state.dragging.keys() {
let drag_enter_event = Pointer::new(
pointer_id,
location.clone(),
DragEnter {
button,
dragged: *drag_target,
hit: hit.clone(),
},
hovered_entity,
);
commands.trigger(drag_enter_event.clone());
message_writers.drag_enter_events.write(drag_enter_event);
}
}
}
if !previous_hover_map
.get(&pointer_id)
.iter()
.any(|e| e.contains_key(&hovered_entity))
{
let mut entities_to_send_enter = hovered_entity_ancestors
.get_ancestors(&hovered_entity)
.map_or_else(
|| {
ancestors_query
.iter_ancestors(hovered_entity)
.collect::<EntityHashSet>()
},
Clone::clone,
);
entities_to_send_enter.insert(hovered_entity);
entities_to_send_enter
.retain(|entity: &Entity| !sent_enter.contains(&(pointer_id, *entity)));
if !entities_to_send_enter.is_empty() {
let prev_hovered_entities = get_hovered_entities(&previous_hover_map, &pointer_id);
let prev_hovered_ancestors =
pointer_state.get_ancestors_union(&prev_hovered_entities);
let union = prev_hovered_entities
.union(&prev_hovered_ancestors)
.copied()
.collect::<EntityHashSet>();
entities_to_send_enter.retain(|entity| !union.contains(entity));
for enter_event in entities_to_send_enter.iter().map(|entity| {
Pointer::new_without_propagate(
pointer_id,
location.clone(),
Enter {
hit: hit.clone(),
is_in_bounds: is_directly_hovered(&hover_map.0, &pointer_id, entity),
},
*entity,
)
}) {
let entity = enter_event.entity;
commands.trigger(enter_event.clone());
message_writers.enter_events.write(enter_event);
sent_enter.insert((pointer_id, entity));
}
}
let over_event = Pointer::new(
pointer_id,
location.clone(),
Over { hit: hit.clone() },
hovered_entity,
);
commands.trigger(over_event.clone());
message_writers.over_events.write(over_event);
}
}
core::mem::swap(
&mut hovered_entity_ancestors.0,
&mut pointer_state.hovered_entity_ancestors,
);
for PointerInput {
pointer_id,
location,
action,
} in input_events.read().cloned()
{
match action {
PointerAction::Press(button) => {
let state = pointer_state.get_mut(pointer_id, button);
for (hovered_entity, hit) in hover_map
.get(&pointer_id)
.iter()
.flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.clone())))
{
let pressed_event = Pointer::new(
pointer_id,
location.clone(),
Press {
button,
hit: hit.clone(),
},
hovered_entity,
);
commands.trigger(pressed_event.clone());
message_writers.pressed_events.write(pressed_event);
state
.pressing
.insert(hovered_entity, (location.clone(), now, hit));
}
}
PointerAction::Release(button) => {
let state = pointer_state.get_mut(pointer_id, button);
state
.clicking
.retain(|_, (last_click, _)| now - *last_click <= MULTI_CLICK_DURATION);
for (hovered_entity, hit) in previous_hover_map
.get(&pointer_id)
.iter()
.flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.clone())))
{
if let Some((_, press_instant, _)) = state.pressing.get(&hovered_entity) {
let count = state
.clicking
.get(&hovered_entity)
.map_or(1, |(_, count)| count.saturating_add(1));
state.clicking.insert(hovered_entity, (now, count));
let click_event = Pointer::new(
pointer_id,
location.clone(),
Click {
button,
hit: hit.clone(),
duration: now - *press_instant,
count,
},
hovered_entity,
);
commands.trigger(click_event.clone());
message_writers.click_events.write(click_event);
}
let released_event = Pointer::new(
pointer_id,
location.clone(),
Release {
button,
hit: hit.clone(),
},
hovered_entity,
);
commands.trigger(released_event.clone());
message_writers.released_events.write(released_event);
}
for (drag_target, drag) in state.dragging.drain() {
for (dragged_over, hit) in state.dragging_over.iter() {
let drag_drop_event = Pointer::new(
pointer_id,
location.clone(),
DragDrop {
button,
dropped: drag_target,
hit: hit.clone(),
},
*dragged_over,
);
commands.trigger(drag_drop_event.clone());
message_writers.drag_drop_events.write(drag_drop_event);
}
let drag_end_event = Pointer::new(
pointer_id,
location.clone(),
DragEnd {
button,
distance: drag.latest_pos - drag.start_pos,
},
drag_target,
);
commands.trigger(drag_end_event.clone());
message_writers.drag_end_events.write(drag_end_event);
for (dragged_over, hit) in state.dragging_over.iter() {
let drag_leave_event = Pointer::new(
pointer_id,
location.clone(),
DragLeave {
button,
dragged: drag_target,
hit: hit.clone(),
},
*dragged_over,
);
commands.trigger(drag_leave_event.clone());
message_writers.drag_leave_events.write(drag_leave_event);
}
}
state.clear();
}
PointerAction::Move { delta } => {
if delta == Vec2::ZERO {
continue; }
for button in PointerButton::iter() {
let state = pointer_state.get_mut(pointer_id, button);
for (press_target, (location, _, hit)) in state.pressing.iter() {
if state.dragging.contains_key(press_target) {
continue; }
state.dragging.insert(
*press_target,
DragEntry {
start_pos: location.position,
latest_pos: location.position,
},
);
let drag_start_event = Pointer::new(
pointer_id,
location.clone(),
DragStart {
button,
hit: hit.clone(),
},
*press_target,
);
commands.trigger(drag_start_event.clone());
message_writers.drag_start_events.write(drag_start_event);
for (hovered_entity, hit) in hover_map
.get(&pointer_id)
.iter()
.flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned())))
.filter(|(hovered_entity, _)| *hovered_entity != *press_target)
{
state.dragging_over.insert(hovered_entity, hit.clone());
let drag_enter_event = Pointer::new(
pointer_id,
location.clone(),
DragEnter {
button,
dragged: *press_target,
hit: hit.clone(),
},
hovered_entity,
);
commands.trigger(drag_enter_event.clone());
message_writers.drag_enter_events.write(drag_enter_event);
}
}
for (drag_target, drag) in state.dragging.iter_mut() {
let delta = location.position - drag.latest_pos;
if delta == Vec2::ZERO {
continue; }
let drag_event = Pointer::new(
pointer_id,
location.clone(),
Drag {
button,
distance: location.position - drag.start_pos,
delta,
},
*drag_target,
);
commands.trigger(drag_event.clone());
message_writers.drag_events.write(drag_event);
drag.latest_pos = location.position;
for (hovered_entity, hit) in hover_map
.get(&pointer_id)
.iter()
.flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned())))
.filter(|(hovered_entity, _)| *hovered_entity != *drag_target)
{
let drag_over_event = Pointer::new(
pointer_id,
location.clone(),
DragOver {
button,
dragged: *drag_target,
hit: hit.clone(),
},
hovered_entity,
);
commands.trigger(drag_over_event.clone());
message_writers.drag_over_events.write(drag_over_event);
}
}
}
for (hovered_entity, hit) in hover_map
.get(&pointer_id)
.iter()
.flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned())))
{
let move_event = Pointer::new(
pointer_id,
location.clone(),
Move {
hit: hit.clone(),
delta,
},
hovered_entity,
);
commands.trigger(move_event.clone());
message_writers.move_events.write(move_event);
}
}
PointerAction::Scroll { x, y, unit, phase } => {
for (hovered_entity, hit) in hover_map
.get(&pointer_id)
.iter()
.flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.clone())))
{
let scroll_event = Pointer::new(
pointer_id,
location.clone(),
Scroll {
unit,
x,
y,
hit: hit.clone(),
phase,
},
hovered_entity,
);
commands.trigger(scroll_event.clone());
message_writers.scroll_events.write(scroll_event);
}
}
PointerAction::Cancel => {
for (hovered_entity, hit) in hover_map
.get(&pointer_id)
.iter()
.flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned())))
{
let cancel_event =
Pointer::new(pointer_id, location.clone(), Cancel { hit }, hovered_entity);
commands.trigger(cancel_event.clone());
message_writers.cancel_events.write(cancel_event);
}
pointer_state.clear(pointer_id);
}
}
}
}
#[cfg(test)]
mod tests {
use bevy_app::App;
use bevy_camera::{Camera, ManualTextureViewHandle};
use crate::pointer::update_pointer_map;
use super::*;
const POINTER_ID: PointerId = PointerId::Mouse;
const STUB_LOCATION: Location = Location {
target: NormalizedRenderTarget::TextureView(ManualTextureViewHandle(5)),
position: Vec2::new(3., 4.),
};
fn initialize_app_for_test(app: &mut App) {
app.init_resource::<HoverMap>()
.init_resource::<PreviousHoverMap>()
.init_resource::<PointerState>()
.add_message::<PointerInput>()
.add_message::<Pointer<Cancel>>()
.add_message::<Pointer<Click>>()
.add_message::<Pointer<Press>>()
.add_message::<Pointer<DragDrop>>()
.add_message::<Pointer<DragEnd>>()
.add_message::<Pointer<DragEnter>>()
.add_message::<Pointer<Drag>>()
.add_message::<Pointer<DragLeave>>()
.add_message::<Pointer<DragOver>>()
.add_message::<Pointer<DragStart>>()
.add_message::<Pointer<Scroll>>()
.add_message::<Pointer<Move>>()
.add_message::<Pointer<Out>>()
.add_message::<Pointer<Over>>()
.add_message::<Pointer<Leave>>()
.add_message::<Pointer<Enter>>()
.add_message::<Pointer<Release>>();
app.world_mut()
.spawn((POINTER_ID, PointerLocation::new(STUB_LOCATION)));
app.world_mut().insert_resource(PointerMap::default());
assert!(app
.world_mut()
.run_system_cached(update_pointer_map)
.is_ok());
}
fn update_hover_map_with_hovered_entities(app: &mut App, camera: Entity, entities: &[Entity]) {
let mut hover_map = HoverMap::default();
let mut entity_map = EntityHashMap::with_capacity(entities.len());
for entity in entities {
entity_map.insert(
*entity,
HitData {
depth: 0.0,
camera,
position: None,
normal: None,
extra: None,
},
);
}
hover_map.insert(PointerId::Mouse, entity_map);
let previous_hover_map = app.world().resource::<HoverMap>().0.clone();
app.world_mut()
.insert_resource(PreviousHoverMap(previous_hover_map));
app.world_mut().insert_resource(hover_map);
}
#[test]
fn enter_leave_events() {
#[derive(Resource, Default)]
struct EnterEventCounts(HashMap<(Entity, bool), usize>);
#[derive(Resource, Default)]
struct LeaveEventCounts(HashMap<(Entity, bool), usize>);
fn observe_enter(event: On<Pointer<Enter>>, mut counts: ResMut<EnterEventCounts>) {
*counts
.0
.entry((event.entity, event.event().is_in_bounds))
.or_insert(0_usize) += 1;
}
fn observe_leave(event: On<Pointer<Leave>>, mut counts: ResMut<LeaveEventCounts>) {
*counts
.0
.entry((event.entity, event.event().was_in_bounds))
.or_insert(0_usize) += 1;
}
fn assert_msg_event_counts(app: &App, enter_count: usize, leave_count: usize) {
let enter_messages = app.world().resource::<Messages<Pointer<Enter>>>();
let leave_messages = app.world().resource::<Messages<Pointer<Leave>>>();
assert_eq!(enter_messages.len(), enter_count);
assert_eq!(leave_messages.len(), leave_count);
}
fn assert_observer_event_counts(
app: &App,
entity: Entity,
enter_in_bounds_counts: usize,
enter_out_of_bounds_counts: usize,
leave_in_bounds_counts: usize,
leave_out_of_bounds_counts: usize,
) {
assert_eq!(
*app.world()
.resource::<EnterEventCounts>()
.0
.get(&(entity, true))
.unwrap_or(&0),
enter_in_bounds_counts
);
assert_eq!(
*app.world()
.resource::<EnterEventCounts>()
.0
.get(&(entity, false))
.unwrap_or(&0),
enter_out_of_bounds_counts
);
assert_eq!(
*app.world()
.resource::<LeaveEventCounts>()
.0
.get(&(entity, true))
.unwrap_or(&0),
leave_in_bounds_counts
);
assert_eq!(
*app.world()
.resource::<LeaveEventCounts>()
.0
.get(&(entity, false))
.unwrap_or(&0),
leave_out_of_bounds_counts
);
}
let mut app = App::new();
initialize_app_for_test(&mut app);
app.init_resource::<EnterEventCounts>()
.init_resource::<LeaveEventCounts>();
let enter_messages = app.world().resource::<Messages<Pointer<Enter>>>();
let leave_messages = app.world().resource::<Messages<Pointer<Leave>>>();
assert_eq!(enter_messages.len(), 0);
assert_eq!(leave_messages.len(), 0);
let camera = app.world_mut().spawn(Camera::default()).id();
let child_one = app
.world_mut()
.spawn_empty()
.observe(observe_enter)
.observe(observe_leave)
.id();
let child_two = app
.world_mut()
.spawn_empty()
.observe(observe_enter)
.observe(observe_leave)
.id();
let parent = app
.world_mut()
.spawn_empty()
.add_children(&[child_one, child_two])
.observe(observe_enter)
.observe(observe_leave)
.id();
update_hover_map_with_hovered_entities(&mut app, camera, &[child_one]);
assert!(app.world_mut().run_system_cached(pointer_events).is_ok());
assert_msg_event_counts(&app, 2, 0);
assert_observer_event_counts(&app, parent, 0, 1, 0, 0);
assert_observer_event_counts(&app, child_one, 1, 0, 0, 0);
assert_observer_event_counts(&app, child_two, 0, 0, 0, 0);
app.world_mut().increment_change_tick();
update_hover_map_with_hovered_entities(&mut app, camera, &[child_two, parent]);
assert!(app.world_mut().run_system_cached(pointer_events).is_ok());
assert_msg_event_counts(&app, 3, 1);
assert_observer_event_counts(&app, parent, 0, 1, 0, 0);
assert_observer_event_counts(&app, child_one, 1, 0, 1, 0);
assert_observer_event_counts(&app, child_two, 1, 0, 0, 0);
app.world_mut().increment_change_tick();
update_hover_map_with_hovered_entities(&mut app, camera, &[parent]);
assert!(app.world_mut().run_system_cached(pointer_events).is_ok());
assert_msg_event_counts(&app, 3, 2);
assert_observer_event_counts(&app, parent, 0, 1, 0, 0);
assert_observer_event_counts(&app, child_one, 1, 0, 1, 0);
assert_observer_event_counts(&app, child_two, 1, 0, 1, 0);
app.world_mut().increment_change_tick();
update_hover_map_with_hovered_entities(&mut app, camera, &[child_two]);
assert!(app.world_mut().run_system_cached(pointer_events).is_ok());
assert_msg_event_counts(&app, 4, 2);
assert_observer_event_counts(&app, parent, 0, 1, 0, 0);
assert_observer_event_counts(&app, child_one, 1, 0, 1, 0);
assert_observer_event_counts(&app, child_two, 2, 0, 1, 0);
app.world_mut().increment_change_tick();
update_hover_map_with_hovered_entities(&mut app, camera, &[]);
assert!(app.world_mut().run_system_cached(pointer_events).is_ok());
assert_msg_event_counts(&app, 4, 4);
assert_observer_event_counts(&app, parent, 0, 1, 0, 1);
assert_observer_event_counts(&app, child_one, 1, 0, 1, 0);
assert_observer_event_counts(&app, child_two, 2, 0, 2, 0);
app.world_mut().increment_change_tick();
update_hover_map_with_hovered_entities(&mut app, camera, &[parent, child_one]);
assert!(app.world_mut().run_system_cached(pointer_events).is_ok());
assert_msg_event_counts(&app, 6, 4);
assert_observer_event_counts(&app, parent, 1, 1, 0, 1);
assert_observer_event_counts(&app, child_one, 2, 0, 1, 0);
assert_observer_event_counts(&app, child_two, 2, 0, 2, 0);
app.world_mut().increment_change_tick();
}
}