use alloc::collections::BTreeMap;
use core::fmt::Debug;
use std::collections::HashSet;
use crate::{
backend::{self, HitData},
pointer::{PointerAction, PointerId, PointerInput, PointerInteraction, PointerPressState},
Pickable,
};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
entity::{EntityHashMap, EntityHashSet},
prelude::*,
};
use bevy_math::FloatOrd;
use bevy_platform::collections::HashMap;
use bevy_reflect::prelude::*;
type DepthSortedHits = Vec<(Entity, HitData)>;
type PickLayer = FloatOrd;
type LayerMap = BTreeMap<PickLayer, DepthSortedHits>;
type OverMap = HashMap<PointerId, LayerMap>;
#[derive(Debug, Deref, DerefMut, Default, Resource, Reflect)]
#[reflect(Debug, Default, Resource)]
pub struct HoverMap(pub HashMap<PointerId, EntityHashMap<HitData>>);
#[derive(Debug, Deref, DerefMut, Default, Resource, Reflect)]
#[reflect(Debug, Default, Resource)]
pub struct PreviousHoverMap(pub HashMap<PointerId, EntityHashMap<HitData>>);
#[derive(Debug, Default, Resource)]
pub struct PointerCaptureMap(pub HashMap<PointerId, (Entity, HitData)>);
impl PointerCaptureMap {
pub fn capture(&mut self, pointer: PointerId, entity: Entity, hit: HitData) {
self.0.insert(pointer, (entity, hit));
}
pub fn release(&mut self, pointer: PointerId) -> Option<(Entity, HitData)> {
self.0.remove(&pointer)
}
pub fn get(&self, pointer: &PointerId) -> Option<(Entity, &HitData)> {
self.0.get(pointer).map(|(e, h)| (*e, h))
}
pub fn is_captured(&self, pointer: &PointerId) -> bool {
self.0.contains_key(pointer)
}
}
pub(crate) fn get_hovered_entities(
hover_map: &HashMap<PointerId, EntityHashMap<HitData>>,
pointer_id: &PointerId,
) -> EntityHashSet {
hover_map
.get(pointer_id)
.map_or(EntityHashSet::default(), |entity_hit| {
entity_hit
.iter()
.map(|(&entity, _)| entity)
.collect::<EntityHashSet>()
})
}
pub(crate) fn is_directly_hovered(
hover_map: &HashMap<PointerId, EntityHashMap<HitData>>,
pointer_id: &PointerId,
entity: &Entity,
) -> bool {
hover_map
.get(pointer_id)
.is_some_and(|hit_data_map| hit_data_map.contains_key(entity))
}
pub fn generate_hovermap(
pickable: Query<&Pickable>,
pointers: Query<&PointerId>,
mut pointer_hits_reader: MessageReader<backend::PointerHits>,
mut pointer_input_reader: MessageReader<PointerInput>,
mut capture_map: ResMut<PointerCaptureMap>,
mut over_map: Local<OverMap>,
mut hover_map: ResMut<HoverMap>,
mut previous_hover_map: ResMut<PreviousHoverMap>,
) {
reset_maps(
&mut hover_map,
&mut previous_hover_map,
&mut over_map,
&mut capture_map,
&pointers,
);
let pointer_inputs: Vec<PointerInput> = pointer_input_reader.read().cloned().collect();
release_captures_on_release_or_cancel(&pointer_inputs, &mut capture_map);
build_over_map(&mut pointer_hits_reader, &mut over_map, &pointer_inputs);
build_hover_map(&pointers, pickable, &over_map, &mut hover_map);
apply_pointer_captures(&capture_map, &mut hover_map);
}
fn release_captures_on_release_or_cancel(
pointer_inputs: &[PointerInput],
capture_map: &mut PointerCaptureMap,
) {
for input in pointer_inputs {
if matches!(
input.action,
PointerAction::Release(_) | PointerAction::Cancel
) {
capture_map.release(input.pointer_id);
}
}
}
fn apply_pointer_captures(capture_map: &PointerCaptureMap, hover_map: &mut HoverMap) {
for (pointer_id, (entity, hit_data)) in &capture_map.0 {
let entry = hover_map.entry(*pointer_id).or_default();
entry.clear();
entry.insert(*entity, hit_data.clone());
}
}
fn reset_maps(
hover_map: &mut HoverMap,
previous_hover_map: &mut PreviousHoverMap,
over_map: &mut OverMap,
capture_map: &mut PointerCaptureMap,
pointers: &Query<&PointerId>,
) {
core::mem::swap(&mut previous_hover_map.0, &mut hover_map.0);
for entity_set in hover_map.values_mut() {
entity_set.clear();
}
for layer_map in over_map.values_mut() {
layer_map.clear();
}
let active_pointers: Vec<PointerId> = pointers.iter().copied().collect();
hover_map.retain(|pointer, _| active_pointers.contains(pointer));
over_map.retain(|pointer, _| active_pointers.contains(pointer));
capture_map
.0
.retain(|pointer, _| active_pointers.contains(pointer));
}
fn build_over_map(
pointer_hit_reader: &mut MessageReader<backend::PointerHits>,
pointer_over_map: &mut Local<OverMap>,
pointer_inputs: &[PointerInput],
) {
let cancelled_pointers: HashSet<PointerId> = pointer_inputs
.iter()
.filter_map(|p| {
if let PointerAction::Cancel = p.action {
Some(p.pointer_id)
} else {
None
}
})
.collect();
for entities_under_pointer in pointer_hit_reader
.read()
.filter(|e| !cancelled_pointers.contains(&e.pointer))
{
let pointer = entities_under_pointer.pointer;
let layer_map = pointer_over_map.entry(pointer).or_default();
for (entity, pick_data) in entities_under_pointer.picks.iter() {
let layer = entities_under_pointer.order;
let hits = layer_map.entry(FloatOrd(layer)).or_default();
hits.push((*entity, pick_data.clone()));
}
}
for layers in pointer_over_map.values_mut() {
for hits in layers.values_mut() {
hits.sort_by_key(|(_, hit)| FloatOrd(hit.depth));
}
}
}
fn build_hover_map(
pointers: &Query<&PointerId>,
pickable: Query<&Pickable>,
over_map: &Local<OverMap>,
hover_map: &mut HoverMap,
) {
for pointer_id in pointers.iter() {
let pointer_entity_set = hover_map.entry(*pointer_id).or_default();
if let Some(layer_map) = over_map.get(pointer_id) {
for (entity, pick_data) in layer_map.values().rev().flatten() {
if let Ok(pickable) = pickable.get(*entity) {
if pickable.is_hoverable {
pointer_entity_set.insert(*entity, pick_data.clone());
}
if pickable.should_block_lower {
break;
}
} else {
pointer_entity_set.insert(*entity, pick_data.clone()); break; }
}
}
}
}
#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)]
#[reflect(Component, Default, PartialEq, Debug, Clone)]
pub enum PickingInteraction {
Pressed = 2,
Hovered = 1,
#[default]
None = 0,
}
pub fn update_interactions(
hover_map: Res<HoverMap>,
previous_hover_map: Res<PreviousHoverMap>,
mut commands: Commands,
mut pointers: Query<(&PointerId, &PointerPressState, &mut PointerInteraction)>,
mut interact: Query<&mut PickingInteraction>,
) {
let mut new_interaction_state = EntityHashMap::<PickingInteraction>::default();
for (pointer, pointer_press, mut pointer_interaction) in &mut pointers {
if let Some(pointers_hovered_entities) = hover_map.get(pointer) {
let mut sorted_entities: Vec<_> = pointers_hovered_entities.clone().drain().collect();
sorted_entities.sort_by_key(|(_, hit)| FloatOrd(hit.depth));
pointer_interaction.sorted_entities = sorted_entities;
for hovered_entity in pointers_hovered_entities.iter().map(|(entity, _)| entity) {
merge_interaction_states(pointer_press, hovered_entity, &mut new_interaction_state);
}
}
}
for (&hovered_entity, &new_interaction) in new_interaction_state.iter() {
if let Ok(mut interaction) = interact.get_mut(hovered_entity) {
interaction.set_if_neq(new_interaction);
} else if let Ok(mut entity_commands) = commands.get_entity(hovered_entity) {
entity_commands.try_insert(new_interaction);
}
}
for (pointer, _, _) in &mut pointers {
let Some(previously_hovered_entities) = previous_hover_map.get(pointer) else {
continue;
};
for entity in previously_hovered_entities.keys() {
if !new_interaction_state.contains_key(entity)
&& let Ok(mut interaction) = interact.get_mut(*entity)
{
interaction.set_if_neq(PickingInteraction::None);
}
}
}
}
fn merge_interaction_states(
pointer_press: &PointerPressState,
hovered_entity: &Entity,
new_interaction_state: &mut EntityHashMap<PickingInteraction>,
) {
let new_interaction = match pointer_press.is_any_pressed() {
true => PickingInteraction::Pressed,
false => PickingInteraction::Hovered,
};
if let Some(old_interaction) = new_interaction_state.get_mut(hovered_entity) {
if *old_interaction != new_interaction
&& matches!(
(*old_interaction, new_interaction),
(PickingInteraction::Hovered, PickingInteraction::Pressed)
| (PickingInteraction::None, PickingInteraction::Pressed)
| (PickingInteraction::None, PickingInteraction::Hovered)
)
{
*old_interaction = new_interaction;
}
} else {
new_interaction_state.insert(*hovered_entity, new_interaction);
}
}
#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)]
#[reflect(Component, Default, PartialEq, Debug, Clone)]
#[component(immutable)]
pub struct Hovered(pub bool);
impl Hovered {
pub fn get(&self) -> bool {
self.0
}
}
#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)]
#[reflect(Component, Default, PartialEq, Debug, Clone)]
#[component(immutable)]
pub struct DirectlyHovered(pub bool);
impl DirectlyHovered {
pub fn get(&self) -> bool {
self.0
}
}
pub fn update_is_hovered(
hover_map: Option<Res<HoverMap>>,
mut hovers: Query<(Entity, &Hovered)>,
parent_query: Query<&ChildOf>,
mut commands: Commands,
) {
let Some(hover_map) = hover_map else { return };
if hovers.is_empty() {
return;
}
let mut hover_ancestors = EntityHashSet::with_capacity(32);
if let Some(map) = hover_map.get(&PointerId::Mouse) {
for hovered_entity in map.keys() {
hover_ancestors.insert(*hovered_entity);
hover_ancestors.extend(parent_query.iter_ancestors(*hovered_entity));
}
}
for (entity, hoverable) in hovers.iter_mut() {
let is_hovering = hover_ancestors.contains(&entity);
if hoverable.0 != is_hovering {
commands.entity(entity).insert(Hovered(is_hovering));
}
}
}
pub fn update_is_directly_hovered(
hover_map: Option<Res<HoverMap>>,
hovers: Query<(Entity, &DirectlyHovered)>,
mut commands: Commands,
) {
let Some(hover_map) = hover_map else { return };
if hovers.is_empty() {
return;
}
if let Some(map) = hover_map.get(&PointerId::Mouse) {
for (entity, hoverable) in hovers.iter() {
let is_hovering = map.contains_key(&entity);
if hoverable.0 != is_hovering {
commands.entity(entity).insert(DirectlyHovered(is_hovering));
}
}
} else {
for (entity, hoverable) in hovers.iter() {
if hoverable.0 {
commands.entity(entity).insert(DirectlyHovered(false));
}
}
}
}
#[cfg(test)]
mod tests {
use bevy_camera::Camera;
use super::*;
#[test]
fn update_is_hovered_memoized() {
let mut world = World::default();
let camera = world.spawn(Camera::default()).id();
let hovered_child = world.spawn_empty().id();
let hovered_entity = world.spawn(Hovered(false)).add_child(hovered_child).id();
let mut hover_map = HoverMap::default();
let mut entity_map = EntityHashMap::new();
entity_map.insert(
hovered_child,
HitData {
depth: 0.0,
camera,
position: None,
normal: None,
extra: None,
},
);
hover_map.insert(PointerId::Mouse, entity_map);
world.insert_resource(hover_map);
assert!(world.run_system_cached(update_is_hovered).is_ok());
let hover = world.entity(hovered_entity).get_ref::<Hovered>().unwrap();
assert!(hover.get());
assert!(hover.is_changed());
world.increment_change_tick();
assert!(world.run_system_cached(update_is_hovered).is_ok());
let hover = world.entity(hovered_entity).get_ref::<Hovered>().unwrap();
assert!(hover.get());
world.insert_resource(HoverMap::default());
world.increment_change_tick();
assert!(world.run_system_cached(update_is_hovered).is_ok());
let hover = world.entity(hovered_entity).get_ref::<Hovered>().unwrap();
assert!(!hover.get());
assert!(hover.is_changed());
}
#[test]
fn update_is_hovered_direct_self() {
let mut world = World::default();
let camera = world.spawn(Camera::default()).id();
let hovered_entity = world.spawn(DirectlyHovered(false)).id();
let mut hover_map = HoverMap::default();
let mut entity_map = EntityHashMap::new();
entity_map.insert(
hovered_entity,
HitData {
depth: 0.0,
camera,
position: None,
normal: None,
extra: None,
},
);
hover_map.insert(PointerId::Mouse, entity_map);
world.insert_resource(hover_map);
assert!(world.run_system_cached(update_is_directly_hovered).is_ok());
let hover = world
.entity(hovered_entity)
.get_ref::<DirectlyHovered>()
.unwrap();
assert!(hover.get());
assert!(hover.is_changed());
world.increment_change_tick();
assert!(world.run_system_cached(update_is_directly_hovered).is_ok());
let hover = world
.entity(hovered_entity)
.get_ref::<DirectlyHovered>()
.unwrap();
assert!(hover.get());
world.insert_resource(HoverMap::default());
world.increment_change_tick();
assert!(world.run_system_cached(update_is_directly_hovered).is_ok());
let hover = world
.entity(hovered_entity)
.get_ref::<DirectlyHovered>()
.unwrap();
assert!(!hover.get());
assert!(hover.is_changed());
}
#[test]
fn update_is_hovered_direct_child() {
let mut world = World::default();
let camera = world.spawn(Camera::default()).id();
let hovered_child = world.spawn_empty().id();
let hovered_entity = world
.spawn(DirectlyHovered(false))
.add_child(hovered_child)
.id();
let mut hover_map = HoverMap::default();
let mut entity_map = EntityHashMap::new();
entity_map.insert(
hovered_child,
HitData {
depth: 0.0,
camera,
position: None,
normal: None,
extra: None,
},
);
hover_map.insert(PointerId::Mouse, entity_map);
world.insert_resource(hover_map);
assert!(world.run_system_cached(update_is_directly_hovered).is_ok());
let hover = world
.entity(hovered_entity)
.get_ref::<DirectlyHovered>()
.unwrap();
assert!(!hover.get());
assert!(hover.is_changed());
}
fn make_hit(camera: Entity) -> HitData {
HitData {
depth: 0.0,
camera,
position: None,
normal: None,
extra: None,
}
}
#[test]
fn capture_overrides_hover_map() {
let camera = Entity::from_bits(1);
let entity_a = Entity::from_bits(2);
let entity_b = Entity::from_bits(3);
let hit = make_hit(camera);
let mut hover_map = HoverMap::default();
let mut entity_map = EntityHashMap::new();
entity_map.insert(entity_a, hit.clone());
hover_map.insert(PointerId::Mouse, entity_map);
let mut capture_map = PointerCaptureMap::default();
capture_map.capture(PointerId::Mouse, entity_b, hit.clone());
apply_pointer_captures(&capture_map, &mut hover_map);
let mouse_hovered = hover_map.get(&PointerId::Mouse).unwrap();
assert!(
!mouse_hovered.contains_key(&entity_a),
"original hover should be evicted"
);
assert!(
mouse_hovered.contains_key(&entity_b),
"captured entity should be sole entry"
);
assert_eq!(mouse_hovered.len(), 1);
}
#[test]
fn capture_does_not_affect_uncaptured_pointers() {
let camera = Entity::from_bits(1);
let entity_a = Entity::from_bits(2);
let entity_b = Entity::from_bits(3);
let hit = make_hit(camera);
let touch_id = PointerId::Touch(0);
let mut hover_map = HoverMap::default();
let mut mouse_map = EntityHashMap::new();
mouse_map.insert(entity_a, hit.clone());
hover_map.insert(PointerId::Mouse, mouse_map);
let mut touch_map = EntityHashMap::new();
touch_map.insert(entity_b, hit.clone());
hover_map.insert(touch_id, touch_map);
let mut capture_map = PointerCaptureMap::default();
capture_map.capture(touch_id, entity_a, hit.clone());
apply_pointer_captures(&capture_map, &mut hover_map);
let mouse_hovered = hover_map.get(&PointerId::Mouse).unwrap();
assert!(mouse_hovered.contains_key(&entity_a));
assert_eq!(mouse_hovered.len(), 1);
let touch_hovered = hover_map.get(&touch_id).unwrap();
assert!(touch_hovered.contains_key(&entity_a));
assert!(!touch_hovered.contains_key(&entity_b));
assert_eq!(touch_hovered.len(), 1);
}
#[test]
fn capture_creates_entry_for_pointer_absent_from_hover_map() {
let camera = Entity::from_bits(1);
let entity = Entity::from_bits(2);
let hit = make_hit(camera);
let mut hover_map = HoverMap::default();
let mut capture_map = PointerCaptureMap::default();
capture_map.capture(PointerId::Mouse, entity, hit.clone());
apply_pointer_captures(&capture_map, &mut hover_map);
let mouse_hovered = hover_map.get(&PointerId::Mouse).unwrap();
assert!(mouse_hovered.contains_key(&entity));
assert_eq!(mouse_hovered.len(), 1);
}
#[test]
fn pointer_capture_map_api() {
let camera = Entity::from_bits(1);
let entity = Entity::from_bits(2);
let hit = make_hit(camera);
let mut map = PointerCaptureMap::default();
assert!(!map.is_captured(&PointerId::Mouse));
assert!(map.get(&PointerId::Mouse).is_none());
map.capture(PointerId::Mouse, entity, hit);
assert!(map.is_captured(&PointerId::Mouse));
let (captured_entity, _) = map.get(&PointerId::Mouse).unwrap();
assert_eq!(captured_entity, entity);
let released = map.release(PointerId::Mouse);
assert_eq!(released.unwrap().0, entity);
assert!(!map.is_captured(&PointerId::Mouse));
assert!(map.get(&PointerId::Mouse).is_none());
assert!(map.release(PointerId::Mouse).is_none());
}
fn make_pointer_input(pointer_id: PointerId, action: PointerAction) -> PointerInput {
use bevy_camera::{ManualTextureViewHandle, NormalizedRenderTarget};
use bevy_math::Vec2;
PointerInput::new(
pointer_id,
crate::pointer::Location {
target: NormalizedRenderTarget::TextureView(ManualTextureViewHandle(5)),
position: Vec2::ZERO,
},
action,
)
}
#[test]
fn release_action_releases_capture() {
let camera = Entity::from_bits(1);
let entity = Entity::from_bits(2);
let hit = make_hit(camera);
let mut capture_map = PointerCaptureMap::default();
capture_map.capture(PointerId::Mouse, entity, hit);
let inputs = [make_pointer_input(
PointerId::Mouse,
PointerAction::Release(crate::pointer::PointerButton::Primary),
)];
release_captures_on_release_or_cancel(&inputs, &mut capture_map);
assert!(!capture_map.is_captured(&PointerId::Mouse));
}
#[test]
fn cancel_action_releases_capture() {
let camera = Entity::from_bits(1);
let entity = Entity::from_bits(2);
let hit = make_hit(camera);
let mut capture_map = PointerCaptureMap::default();
capture_map.capture(PointerId::Mouse, entity, hit);
let inputs = [make_pointer_input(PointerId::Mouse, PointerAction::Cancel)];
release_captures_on_release_or_cancel(&inputs, &mut capture_map);
assert!(!capture_map.is_captured(&PointerId::Mouse));
}
#[test]
fn generate_hovermap_cleans_up_capture_for_removed_pointer() {
let mut app = bevy_app::App::new();
app.init_resource::<HoverMap>()
.init_resource::<PreviousHoverMap>()
.init_resource::<PointerCaptureMap>()
.add_message::<PointerInput>()
.add_message::<backend::PointerHits>();
let camera = Entity::from_bits(1);
let captured_entity = app.world_mut().spawn_empty().id();
app.world_mut().resource_mut::<PointerCaptureMap>().capture(
PointerId::Mouse,
captured_entity,
make_hit(camera),
);
assert!(app.world_mut().run_system_cached(generate_hovermap).is_ok());
assert!(!app
.world()
.resource::<PointerCaptureMap>()
.is_captured(&PointerId::Mouse));
}
}