bevy_picking/
hover.rs

1//! Determines which entities are being hovered by which pointers.
2//!
3//! The most important type in this module is the [`HoverMap`], which maps pointers to the entities
4//! they are hovering over.
5
6use alloc::collections::BTreeMap;
7use core::fmt::Debug;
8use std::collections::HashSet;
9
10use crate::{
11    backend::{self, HitData},
12    pointer::{PointerAction, PointerId, PointerInput, PointerInteraction, PointerPress},
13    Pickable,
14};
15
16use bevy_derive::{Deref, DerefMut};
17use bevy_ecs::{entity::EntityHashSet, prelude::*};
18use bevy_math::FloatOrd;
19use bevy_platform::collections::HashMap;
20use bevy_reflect::prelude::*;
21
22type DepthSortedHits = Vec<(Entity, HitData)>;
23
24/// Events returned from backends can be grouped with an order field. This allows picking to work
25/// with multiple layers of rendered output to the same render target.
26type PickLayer = FloatOrd;
27
28/// Maps [`PickLayer`]s to the map of entities within that pick layer, sorted by depth.
29type LayerMap = BTreeMap<PickLayer, DepthSortedHits>;
30
31/// Maps Pointers to a [`LayerMap`]. Note this is much more complex than the [`HoverMap`] because
32/// this data structure is used to sort entities by layer then depth for every pointer.
33type OverMap = HashMap<PointerId, LayerMap>;
34
35/// The source of truth for all hover state. This is used to determine what events to send, and what
36/// state components should be in.
37///
38/// Maps pointers to the entities they are hovering over.
39///
40/// "Hovering" refers to the *hover* state, which is not the same as whether or not a picking
41/// backend is reporting hits between a pointer and an entity. A pointer is "hovering" an entity
42/// only if the pointer is hitting the entity (as reported by a picking backend) *and* no entities
43/// between it and the pointer block interactions.
44///
45/// For example, if a pointer is hitting a UI button and a 3d mesh, but the button is in front of
46/// the mesh, the UI button will be hovered, but the mesh will not. Unless, the [`Pickable`]
47/// component is present with [`should_block_lower`](Pickable::should_block_lower) set to `false`.
48///
49/// # Advanced Users
50///
51/// If you want to completely replace the provided picking events or state produced by this plugin,
52/// you can use this resource to do that. All of the event systems for picking are built *on top of*
53/// this authoritative hover state, and you can do the same. You can also use the
54/// [`PreviousHoverMap`] as a robust way of determining changes in hover state from the previous
55/// update.
56#[derive(Debug, Deref, DerefMut, Default, Resource)]
57pub struct HoverMap(pub HashMap<PointerId, HashMap<Entity, HitData>>);
58
59/// The previous state of the hover map, used to track changes to hover state.
60#[derive(Debug, Deref, DerefMut, Default, Resource)]
61pub struct PreviousHoverMap(pub HashMap<PointerId, HashMap<Entity, HitData>>);
62
63/// Coalesces all data from inputs and backends to generate a map of the currently hovered entities.
64/// This is the final focusing step to determine which entity the pointer is hovering over.
65pub fn generate_hovermap(
66    // Inputs
67    pickable: Query<&Pickable>,
68    pointers: Query<&PointerId>,
69    mut pointer_hits_reader: MessageReader<backend::PointerHits>,
70    mut pointer_input_reader: MessageReader<PointerInput>,
71    // Local
72    mut over_map: Local<OverMap>,
73    // Output
74    mut hover_map: ResMut<HoverMap>,
75    mut previous_hover_map: ResMut<PreviousHoverMap>,
76) {
77    reset_maps(
78        &mut hover_map,
79        &mut previous_hover_map,
80        &mut over_map,
81        &pointers,
82    );
83    build_over_map(
84        &mut pointer_hits_reader,
85        &mut over_map,
86        &mut pointer_input_reader,
87    );
88    build_hover_map(&pointers, pickable, &over_map, &mut hover_map);
89}
90
91/// Clear non-empty local maps, reusing allocated memory.
92fn reset_maps(
93    hover_map: &mut HoverMap,
94    previous_hover_map: &mut PreviousHoverMap,
95    over_map: &mut OverMap,
96    pointers: &Query<&PointerId>,
97) {
98    // Swap the previous and current hover maps. This results in the previous values being stored in
99    // `PreviousHoverMap`. Swapping is okay because we clear the `HoverMap` which now holds stale
100    // data. This process is done without any allocations.
101    core::mem::swap(&mut previous_hover_map.0, &mut hover_map.0);
102
103    for entity_set in hover_map.values_mut() {
104        entity_set.clear();
105    }
106    for layer_map in over_map.values_mut() {
107        layer_map.clear();
108    }
109
110    // Clear pointers from the maps if they have been removed.
111    let active_pointers: Vec<PointerId> = pointers.iter().copied().collect();
112    hover_map.retain(|pointer, _| active_pointers.contains(pointer));
113    over_map.retain(|pointer, _| active_pointers.contains(pointer));
114}
115
116/// Build an ordered map of entities that are under each pointer
117fn build_over_map(
118    pointer_hit_reader: &mut MessageReader<backend::PointerHits>,
119    pointer_over_map: &mut Local<OverMap>,
120    pointer_input_reader: &mut MessageReader<PointerInput>,
121) {
122    let cancelled_pointers: HashSet<PointerId> = pointer_input_reader
123        .read()
124        .filter_map(|p| {
125            if let PointerAction::Cancel = p.action {
126                Some(p.pointer_id)
127            } else {
128                None
129            }
130        })
131        .collect();
132
133    for entities_under_pointer in pointer_hit_reader
134        .read()
135        .filter(|e| !cancelled_pointers.contains(&e.pointer))
136    {
137        let pointer = entities_under_pointer.pointer;
138        let layer_map = pointer_over_map.entry(pointer).or_default();
139        for (entity, pick_data) in entities_under_pointer.picks.iter() {
140            let layer = entities_under_pointer.order;
141            let hits = layer_map.entry(FloatOrd(layer)).or_default();
142            hits.push((*entity, pick_data.clone()));
143        }
144    }
145
146    for layers in pointer_over_map.values_mut() {
147        for hits in layers.values_mut() {
148            hits.sort_by_key(|(_, hit)| FloatOrd(hit.depth));
149        }
150    }
151}
152
153/// Build an unsorted set of hovered entities, accounting for depth, layer, and [`Pickable`]. Note
154/// that unlike the pointer map, this uses [`Pickable`] to determine if lower entities receive hover
155/// focus. Often, only a single entity per pointer will be hovered.
156fn build_hover_map(
157    pointers: &Query<&PointerId>,
158    pickable: Query<&Pickable>,
159    over_map: &Local<OverMap>,
160    // Output
161    hover_map: &mut HoverMap,
162) {
163    for pointer_id in pointers.iter() {
164        let pointer_entity_set = hover_map.entry(*pointer_id).or_default();
165        if let Some(layer_map) = over_map.get(pointer_id) {
166            // Note we reverse here to start from the highest layer first.
167            for (entity, pick_data) in layer_map.values().rev().flatten() {
168                if let Ok(pickable) = pickable.get(*entity) {
169                    if pickable.is_hoverable {
170                        pointer_entity_set.insert(*entity, pick_data.clone());
171                    }
172                    if pickable.should_block_lower {
173                        break;
174                    }
175                } else {
176                    pointer_entity_set.insert(*entity, pick_data.clone()); // Emit events by default
177                    break; // Entities block by default so we break out of the loop
178                }
179            }
180        }
181    }
182}
183
184/// A component that aggregates picking interaction state of this entity across all pointers.
185///
186/// Unlike bevy's `Interaction` component, this is an aggregate of the state of all pointers
187/// interacting with this entity. Aggregation is done by taking the interaction with the highest
188/// precedence.
189///
190/// For example, if we have an entity that is being hovered by one pointer, and pressed by another,
191/// the entity will be considered pressed. If that entity is instead being hovered by both pointers,
192/// it will be considered hovered.
193#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)]
194#[reflect(Component, Default, PartialEq, Debug, Clone)]
195pub enum PickingInteraction {
196    /// The entity is being pressed down by a pointer.
197    Pressed = 2,
198    /// The entity is being hovered by a pointer.
199    Hovered = 1,
200    /// No pointers are interacting with this entity.
201    #[default]
202    None = 0,
203}
204
205/// Uses [`HoverMap`] changes to update [`PointerInteraction`] and [`PickingInteraction`] components.
206pub fn update_interactions(
207    // Input
208    hover_map: Res<HoverMap>,
209    previous_hover_map: Res<PreviousHoverMap>,
210    // Outputs
211    mut commands: Commands,
212    mut pointers: Query<(&PointerId, &PointerPress, &mut PointerInteraction)>,
213    mut interact: Query<&mut PickingInteraction>,
214) {
215    // Create a map to hold the aggregated interaction for each entity. This is needed because we
216    // need to be able to insert the interaction component on entities if they do not exist. To do
217    // so we need to know the final aggregated interaction state to avoid the scenario where we set
218    // an entity to `Pressed`, then overwrite that with a lower precedent like `Hovered`.
219    let mut new_interaction_state = HashMap::<Entity, PickingInteraction>::default();
220    for (pointer, pointer_press, mut pointer_interaction) in &mut pointers {
221        if let Some(pointers_hovered_entities) = hover_map.get(pointer) {
222            // Insert a sorted list of hit entities into the pointer's interaction component.
223            let mut sorted_entities: Vec<_> = pointers_hovered_entities.clone().drain().collect();
224            sorted_entities.sort_by_key(|(_, hit)| FloatOrd(hit.depth));
225            pointer_interaction.sorted_entities = sorted_entities;
226
227            for hovered_entity in pointers_hovered_entities.iter().map(|(entity, _)| entity) {
228                merge_interaction_states(pointer_press, hovered_entity, &mut new_interaction_state);
229            }
230        }
231    }
232
233    // Take the aggregated entity states and update or insert the component if missing.
234    for (&hovered_entity, &new_interaction) in new_interaction_state.iter() {
235        if let Ok(mut interaction) = interact.get_mut(hovered_entity) {
236            interaction.set_if_neq(new_interaction);
237        } else if let Ok(mut entity_commands) = commands.get_entity(hovered_entity) {
238            entity_commands.try_insert(new_interaction);
239        }
240    }
241
242    // Clear all previous hover data from pointers that are no longer hovering any entities.
243    // We do this last to preserve change detection for picking interactions.
244    for (pointer, _, _) in &mut pointers {
245        let Some(previously_hovered_entities) = previous_hover_map.get(pointer) else {
246            continue;
247        };
248
249        for entity in previously_hovered_entities.keys() {
250            if !new_interaction_state.contains_key(entity)
251                && let Ok(mut interaction) = interact.get_mut(*entity)
252            {
253                interaction.set_if_neq(PickingInteraction::None);
254            }
255        }
256    }
257}
258
259/// Merge the interaction state of this entity into the aggregated map.
260fn merge_interaction_states(
261    pointer_press: &PointerPress,
262    hovered_entity: &Entity,
263    new_interaction_state: &mut HashMap<Entity, PickingInteraction>,
264) {
265    let new_interaction = match pointer_press.is_any_pressed() {
266        true => PickingInteraction::Pressed,
267        false => PickingInteraction::Hovered,
268    };
269
270    if let Some(old_interaction) = new_interaction_state.get_mut(hovered_entity) {
271        // Only update if the new value has a higher precedence than the old value.
272        if *old_interaction != new_interaction
273            && matches!(
274                (*old_interaction, new_interaction),
275                (PickingInteraction::Hovered, PickingInteraction::Pressed)
276                    | (PickingInteraction::None, PickingInteraction::Pressed)
277                    | (PickingInteraction::None, PickingInteraction::Hovered)
278            )
279        {
280            *old_interaction = new_interaction;
281        }
282    } else {
283        new_interaction_state.insert(*hovered_entity, new_interaction);
284    }
285}
286
287/// A component that allows users to use regular Bevy change detection to determine when the pointer
288/// enters or leaves an entity. Users should insert this component on an entity to indicate interest
289/// in knowing about hover state changes.
290///
291/// The component's boolean value will be `true` whenever the pointer is currently directly hovering
292/// over the entity, or any of the entity's descendants (as defined by the [`ChildOf`]
293/// relationship). This is consistent with the behavior of the CSS `:hover` pseudo-class, which
294/// applies to the element and all of its descendants.
295///
296/// The contained boolean value is guaranteed to only be mutated when the pointer enters or leaves
297/// the entity, allowing Bevy change detection to be used efficiently. This is in contrast to the
298/// [`HoverMap`] resource, which is updated every frame.
299///
300/// Typically, a simple hoverable entity or widget will have this component added to it. More
301/// complex widgets can have this component added to each hoverable part.
302///
303/// The computational cost of keeping the `Hovered` components up to date is relatively cheap, and
304/// linear in the number of entities that have the [`Hovered`] component inserted.
305#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)]
306#[reflect(Component, Default, PartialEq, Debug, Clone)]
307#[component(immutable)]
308pub struct Hovered(pub bool);
309
310impl Hovered {
311    /// Get whether the entity is currently hovered.
312    pub fn get(&self) -> bool {
313        self.0
314    }
315}
316
317/// A component that allows users to use regular Bevy change detection to determine when the pointer
318/// is directly hovering over an entity. Users should insert this component on an entity to indicate
319/// interest in knowing about hover state changes.
320///
321/// This is similar to [`Hovered`] component, except that it does not include descendants in the
322/// hover state.
323#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)]
324#[reflect(Component, Default, PartialEq, Debug, Clone)]
325#[component(immutable)]
326pub struct DirectlyHovered(pub bool);
327
328impl DirectlyHovered {
329    /// Get whether the entity is currently hovered.
330    pub fn get(&self) -> bool {
331        self.0
332    }
333}
334
335/// Uses [`HoverMap`] changes to update [`Hovered`] components.
336pub fn update_is_hovered(
337    hover_map: Option<Res<HoverMap>>,
338    mut hovers: Query<(Entity, &Hovered)>,
339    parent_query: Query<&ChildOf>,
340    mut commands: Commands,
341) {
342    // Don't do any work if there's no hover map.
343    let Some(hover_map) = hover_map else { return };
344
345    // Don't bother collecting ancestors if there are no hovers.
346    if hovers.is_empty() {
347        return;
348    }
349
350    // Algorithm: for each entity having a `Hovered` component, we want to know if the current
351    // entry in the hover map is "within" (that is, in the set of descendants of) that entity. Rather
352    // than doing an expensive breadth-first traversal of children, instead start with the hovermap
353    // entry and search upwards. We can make this even cheaper by building a set of ancestors for
354    // the hovermap entry, and then testing each `Hovered` entity against that set.
355
356    // A set which contains the hovered for the current pointer entity and its ancestors. The
357    // capacity is based on the likely tree depth of the hierarchy, which is typically greater for
358    // UI (because of layout issues) than for 3D scenes. A depth of 32 is a reasonable upper bound
359    // for most use cases.
360    let mut hover_ancestors = EntityHashSet::with_capacity(32);
361    if let Some(map) = hover_map.get(&PointerId::Mouse) {
362        for hovered_entity in map.keys() {
363            hover_ancestors.insert(*hovered_entity);
364            hover_ancestors.extend(parent_query.iter_ancestors(*hovered_entity));
365        }
366    }
367
368    // For each hovered entity, it is considered "hovering" if it's in the set of hovered ancestors.
369    for (entity, hoverable) in hovers.iter_mut() {
370        let is_hovering = hover_ancestors.contains(&entity);
371        if hoverable.0 != is_hovering {
372            commands.entity(entity).insert(Hovered(is_hovering));
373        }
374    }
375}
376
377/// Uses [`HoverMap`] changes to update [`DirectlyHovered`] components.
378pub fn update_is_directly_hovered(
379    hover_map: Option<Res<HoverMap>>,
380    hovers: Query<(Entity, &DirectlyHovered)>,
381    mut commands: Commands,
382) {
383    // Don't do any work if there's no hover map.
384    let Some(hover_map) = hover_map else { return };
385
386    // Don't bother collecting ancestors if there are no hovers.
387    if hovers.is_empty() {
388        return;
389    }
390
391    if let Some(map) = hover_map.get(&PointerId::Mouse) {
392        // It's hovering if it's in the HoverMap.
393        for (entity, hoverable) in hovers.iter() {
394            let is_hovering = map.contains_key(&entity);
395            if hoverable.0 != is_hovering {
396                commands.entity(entity).insert(DirectlyHovered(is_hovering));
397            }
398        }
399    } else {
400        // No hovered entity, reset all hovers.
401        for (entity, hoverable) in hovers.iter() {
402            if hoverable.0 {
403                commands.entity(entity).insert(DirectlyHovered(false));
404            }
405        }
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use bevy_camera::Camera;
412
413    use super::*;
414
415    #[test]
416    fn update_is_hovered_memoized() {
417        let mut world = World::default();
418        let camera = world.spawn(Camera::default()).id();
419
420        // Setup entities
421        let hovered_child = world.spawn_empty().id();
422        let hovered_entity = world.spawn(Hovered(false)).add_child(hovered_child).id();
423
424        // Setup hover map with hovered_entity hovered by mouse
425        let mut hover_map = HoverMap::default();
426        let mut entity_map = HashMap::new();
427        entity_map.insert(
428            hovered_child,
429            HitData {
430                depth: 0.0,
431                camera,
432                position: None,
433                normal: None,
434            },
435        );
436        hover_map.insert(PointerId::Mouse, entity_map);
437        world.insert_resource(hover_map);
438
439        // Run the system
440        assert!(world.run_system_cached(update_is_hovered).is_ok());
441
442        // Check to insure that the hovered entity has the Hovered component set to true
443        let hover = world.entity(hovered_entity).get_ref::<Hovered>().unwrap();
444        assert!(hover.get());
445        assert!(hover.is_changed());
446
447        // Now do it again, but don't change the hover map.
448        world.increment_change_tick();
449
450        assert!(world.run_system_cached(update_is_hovered).is_ok());
451        let hover = world.entity(hovered_entity).get_ref::<Hovered>().unwrap();
452        assert!(hover.get());
453
454        // Should not be changed
455        // NOTE: Test doesn't work - thinks it is always changed
456        // assert!(!hover.is_changed());
457
458        // Clear the hover map and run again.
459        world.insert_resource(HoverMap::default());
460        world.increment_change_tick();
461
462        assert!(world.run_system_cached(update_is_hovered).is_ok());
463        let hover = world.entity(hovered_entity).get_ref::<Hovered>().unwrap();
464        assert!(!hover.get());
465        assert!(hover.is_changed());
466    }
467
468    #[test]
469    fn update_is_hovered_direct_self() {
470        let mut world = World::default();
471        let camera = world.spawn(Camera::default()).id();
472
473        // Setup entities
474        let hovered_entity = world.spawn(DirectlyHovered(false)).id();
475
476        // Setup hover map with hovered_entity hovered by mouse
477        let mut hover_map = HoverMap::default();
478        let mut entity_map = HashMap::new();
479        entity_map.insert(
480            hovered_entity,
481            HitData {
482                depth: 0.0,
483                camera,
484                position: None,
485                normal: None,
486            },
487        );
488        hover_map.insert(PointerId::Mouse, entity_map);
489        world.insert_resource(hover_map);
490
491        // Run the system
492        assert!(world.run_system_cached(update_is_directly_hovered).is_ok());
493
494        // Check to insure that the hovered entity has the DirectlyHovered component set to true
495        let hover = world
496            .entity(hovered_entity)
497            .get_ref::<DirectlyHovered>()
498            .unwrap();
499        assert!(hover.get());
500        assert!(hover.is_changed());
501
502        // Now do it again, but don't change the hover map.
503        world.increment_change_tick();
504
505        assert!(world.run_system_cached(update_is_directly_hovered).is_ok());
506        let hover = world
507            .entity(hovered_entity)
508            .get_ref::<DirectlyHovered>()
509            .unwrap();
510        assert!(hover.get());
511
512        // Should not be changed
513        // NOTE: Test doesn't work - thinks it is always changed
514        // assert!(!hover.is_changed());
515
516        // Clear the hover map and run again.
517        world.insert_resource(HoverMap::default());
518        world.increment_change_tick();
519
520        assert!(world.run_system_cached(update_is_directly_hovered).is_ok());
521        let hover = world
522            .entity(hovered_entity)
523            .get_ref::<DirectlyHovered>()
524            .unwrap();
525        assert!(!hover.get());
526        assert!(hover.is_changed());
527    }
528
529    #[test]
530    fn update_is_hovered_direct_child() {
531        let mut world = World::default();
532        let camera = world.spawn(Camera::default()).id();
533
534        // Setup entities
535        let hovered_child = world.spawn_empty().id();
536        let hovered_entity = world
537            .spawn(DirectlyHovered(false))
538            .add_child(hovered_child)
539            .id();
540
541        // Setup hover map with hovered_entity hovered by mouse
542        let mut hover_map = HoverMap::default();
543        let mut entity_map = HashMap::new();
544        entity_map.insert(
545            hovered_child,
546            HitData {
547                depth: 0.0,
548                camera,
549                position: None,
550                normal: None,
551            },
552        );
553        hover_map.insert(PointerId::Mouse, entity_map);
554        world.insert_resource(hover_map);
555
556        // Run the system
557        assert!(world.run_system_cached(update_is_directly_hovered).is_ok());
558
559        // Check to insure that the DirectlyHovered component is still false
560        let hover = world
561            .entity(hovered_entity)
562            .get_ref::<DirectlyHovered>()
563            .unwrap();
564        assert!(!hover.get());
565        assert!(hover.is_changed());
566    }
567}