diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index ca26a0abee..dad87382b4 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -38,7 +38,7 @@ pub trait Event: Component { /// The component that describes which Entity to propagate this event to next, when [propagation] is enabled. /// /// [propagation]: crate::observer::Trigger::propagate - type Traversal: Traversal; + type Traversal: Traversal; /// When true, this event will always attempt to propagate when [triggered], without requiring a call /// to [`Trigger::propagate`]. diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 53e8b1dec2..7ae876307c 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -615,8 +615,8 @@ mod tests { #[derive(Component)] struct Parent(Entity); - impl Traversal for &'_ Parent { - fn traverse(item: Self::Item<'_>) -> Option { + impl Traversal for &'_ Parent { + fn traverse(item: Self::Item<'_>, _: &D) -> Option { Some(item.0) } } diff --git a/crates/bevy_ecs/src/traversal.rs b/crates/bevy_ecs/src/traversal.rs index 3795883dfd..a8605e94ec 100644 --- a/crates/bevy_ecs/src/traversal.rs +++ b/crates/bevy_ecs/src/traversal.rs @@ -13,16 +13,20 @@ use crate::{entity::Entity, query::ReadOnlyQueryData}; /// for documenting possible looping behavior, and consumers of those implementations are responsible for /// avoiding infinite loops in their code. /// +/// Traversals may be parameterized with additional data. For example, in observer event propagation, the +/// parameter `D` is the event type given in `Trigger`. This allows traversal to differ depending on event +/// data. +/// /// [specify the direction]: crate::event::Event::Traversal /// [event propagation]: crate::observer::Trigger::propagate /// [observers]: crate::observer::Observer -pub trait Traversal: ReadOnlyQueryData { +pub trait Traversal: ReadOnlyQueryData { /// Returns the next entity to visit. - fn traverse(item: Self::Item<'_>) -> Option; + fn traverse(item: Self::Item<'_>, data: &D) -> Option; } -impl Traversal for () { - fn traverse(_: Self::Item<'_>) -> Option { +impl Traversal for () { + fn traverse(_: Self::Item<'_>, _data: &D) -> Option { None } } diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index fd31c3edf1..54bacb13fd 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -534,7 +534,7 @@ impl<'w> DeferredWorld<'w> { data: &mut E, mut propagate: bool, ) where - T: Traversal, + T: Traversal, { loop { Observers::invoke::<_>( @@ -552,7 +552,7 @@ impl<'w> DeferredWorld<'w> { .get_entity(entity) .ok() .and_then(|entity| entity.get_components::()) - .and_then(T::traverse) + .and_then(|item| T::traverse(item, data)) { entity = traverse_to; } else { diff --git a/crates/bevy_hierarchy/src/components/parent.rs b/crates/bevy_hierarchy/src/components/parent.rs index 9217cb1521..b808a6dcff 100644 --- a/crates/bevy_hierarchy/src/components/parent.rs +++ b/crates/bevy_hierarchy/src/components/parent.rs @@ -93,8 +93,8 @@ impl Deref for Parent { /// `Parent::traverse` will never form loops in properly-constructed hierarchies. /// /// [event propagation]: bevy_ecs::observer::Trigger::propagate -impl Traversal for &Parent { - fn traverse(item: Self::Item<'_>) -> Option { +impl Traversal for &Parent { + fn traverse(item: Self::Item<'_>, _data: &D) -> Option { Some(item.0) } } diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index e75d6a6819..0be935985e 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -39,11 +39,13 @@ use core::fmt::Debug; -use bevy_ecs::{prelude::*, system::SystemParam}; +use bevy_ecs::{prelude::*, query::QueryData, system::SystemParam, traversal::Traversal}; use bevy_hierarchy::Parent; use bevy_math::Vec2; use bevy_reflect::prelude::*; +use bevy_render::camera::NormalizedRenderTarget; use bevy_utils::{tracing::debug, Duration, HashMap, Instant}; +use bevy_window::Window; use crate::{ backend::{prelude::PointerLocation, HitData}, @@ -71,11 +73,44 @@ pub struct Pointer { pub event: E, } +/// A traversal query (eg it implements [`Traversal`]) intended for use with [`Pointer`] events. +/// +/// This will always traverse to the parent, if the entity being visited has one. Otherwise, it +/// propagates to the pointer's window and stops there. +#[derive(QueryData)] +pub struct PointerTraversal { + parent: Option<&'static Parent>, + window: Option<&'static Window>, +} + +impl Traversal> for PointerTraversal +where + E: Debug + Clone + Reflect, +{ + fn traverse(item: Self::Item<'_>, pointer: &Pointer) -> Option { + let PointerTraversalItem { parent, window } = item; + + // Send event to parent, if it has one. + if let Some(parent) = parent { + return Some(parent.get()); + }; + + // Otherwise, send it to the window entity (unless this is a window entity). + if window.is_none() { + if let NormalizedRenderTarget::Window(window_ref) = pointer.pointer_location.target { + return Some(window_ref.entity()); + } + } + + None + } +} + impl Event for Pointer where E: Debug + Clone + Reflect, { - type Traversal = &'static Parent; + type Traversal = PointerTraversal; const AUTO_PROPAGATE: bool = true; } diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 6b0b95f11b..b77fab9b37 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -159,6 +159,7 @@ pub mod input; #[cfg(feature = "bevy_mesh_picking_backend")] pub mod mesh_picking; pub mod pointer; +pub mod window; use bevy_app::{prelude::*, PluginGroupBuilder}; use bevy_ecs::prelude::*; @@ -298,6 +299,8 @@ pub struct PickingPlugin { pub is_input_enabled: bool, /// Enables and disables updating interaction states of entities. pub is_focus_enabled: bool, + /// Enables or disables picking for window entities. + pub is_window_picking_enabled: bool, } impl PickingPlugin { @@ -305,11 +308,17 @@ impl PickingPlugin { pub fn input_should_run(state: Res) -> bool { state.is_input_enabled && state.is_enabled } + /// Whether or not systems updating entities' [`PickingInteraction`](focus::PickingInteraction) /// component should be running. pub fn focus_should_run(state: Res) -> bool { state.is_focus_enabled && state.is_enabled } + + /// Whether or not window entities should receive pick events. + pub fn window_picking_should_run(state: Res) -> bool { + state.is_window_picking_enabled && state.is_enabled + } } impl Default for PickingPlugin { @@ -318,6 +327,7 @@ impl Default for PickingPlugin { is_enabled: true, is_input_enabled: true, is_focus_enabled: true, + is_window_picking_enabled: true, } } } @@ -342,6 +352,12 @@ impl Plugin for PickingPlugin { ) .in_set(PickSet::ProcessInput), ) + .add_systems( + PreUpdate, + window::update_window_hits + .run_if(Self::window_picking_should_run) + .in_set(PickSet::Backend), + ) .configure_sets( First, (PickSet::Input, PickSet::PostInput) diff --git a/crates/bevy_picking/src/window.rs b/crates/bevy_picking/src/window.rs new file mode 100644 index 0000000000..f55edca2dd --- /dev/null +++ b/crates/bevy_picking/src/window.rs @@ -0,0 +1,45 @@ +//! This module contains a basic backend that implements picking for window +//! entities. +//! +//! Pointers can exist on windows, images, and gpu texture views. With +//! [`update_window_hits`] enabled, when a pointer hovers over a window that +//! window will be inserted as a pointer hit, listed behind all other pointer +//! hits. This means that when the pointer isn't hovering any other entities, +//! the picking events will be routed to the window. + +use core::f32; + +use bevy_ecs::prelude::*; +use bevy_render::camera::NormalizedRenderTarget; + +use crate::{ + backend::{HitData, PointerHits}, + pointer::{Location, PointerId, PointerLocation}, +}; + +/// Generates pointer hit events for window entities. +/// +/// A pointer is treated as hitting a window when it is located on that window. The order +/// of the hit event is negative infinity, meaning it should appear behind all other entities. +/// +/// The depth of the hit will be listed as zero. +pub fn update_window_hits( + pointers: Query<(&PointerId, &PointerLocation)>, + mut output_events: EventWriter, +) { + for (pointer_id, pointer_location) in pointers.iter() { + if let Some(Location { + target: NormalizedRenderTarget::Window(window_ref), + .. + }) = pointer_location.location + { + let entity = window_ref.entity(); + let hit_data = HitData::new(entity, 0.0, None, None); + output_events.send(PointerHits::new( + *pointer_id, + vec![(entity, hit_data)], + f32::NEG_INFINITY, + )); + } + } +}