Skip to main content

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