mirror of
https://github.com/bevyengine/bevy
synced 2024-12-24 03:53:06 +00:00
Window picking (#16103)
# Objective On the web, it's common to attach observers to windows. As @viridia has discovered, this can be quite a nice paradigm in bevy as well when applied to observers. The changes here are intended to make this possible. + Adds a new default picking back-end as part to the core picking plugin (which can be disabled) that causes pointers on windows to treat the window entity as the final hit, behind everything else. This means clicking empty space now dispatches normal picking events to the window, and is especially nice for drag-and-drop functionality. + Adds a new traversal type, specific to picking events, that causes them to bubble up to the window entity after they reach the root of the hierarchy. ## Solution The window picking back-end is extremely simple, but the bubbling changes are much more complex, since they require doing a different traversal depending on the picking event. To achieve this, `Traversal` has been made generic over an associated sized data type `D`. Observer bounds have been changed such that `Event::Traversal<D>` is required for `Trigger<D>`. A blanket implementation has been added for `()` and `Parent` that preserves the existing functionality. A new `PointerTraversal` traversal has been implemented, with a blanket implementation for `Traversal<Pointer<E>>`. It is still possible to use `Parent` as the traversal for any event, because of the blanket implementation. It is now possible for users to add other custom traversals, which read event data during traversal. ## Testing I tested these changes locally on some picking UI prototypes I have been playing with. I also tested them on the picking examples. --------- Co-authored-by: Martín Maita <47983254+mnmaita@users.noreply.github.com>
This commit is contained in:
parent
ec39f6b904
commit
09b0b5df91
8 changed files with 113 additions and 13 deletions
|
@ -38,7 +38,7 @@ pub trait Event: Component {
|
||||||
/// The component that describes which Entity to propagate this event to next, when [propagation] is enabled.
|
/// The component that describes which Entity to propagate this event to next, when [propagation] is enabled.
|
||||||
///
|
///
|
||||||
/// [propagation]: crate::observer::Trigger::propagate
|
/// [propagation]: crate::observer::Trigger::propagate
|
||||||
type Traversal: Traversal;
|
type Traversal: Traversal<Self>;
|
||||||
|
|
||||||
/// When true, this event will always attempt to propagate when [triggered], without requiring a call
|
/// When true, this event will always attempt to propagate when [triggered], without requiring a call
|
||||||
/// to [`Trigger::propagate`].
|
/// to [`Trigger::propagate`].
|
||||||
|
|
|
@ -615,8 +615,8 @@ mod tests {
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct Parent(Entity);
|
struct Parent(Entity);
|
||||||
|
|
||||||
impl Traversal for &'_ Parent {
|
impl<D> Traversal<D> for &'_ Parent {
|
||||||
fn traverse(item: Self::Item<'_>) -> Option<Entity> {
|
fn traverse(item: Self::Item<'_>, _: &D) -> Option<Entity> {
|
||||||
Some(item.0)
|
Some(item.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,16 +13,20 @@ use crate::{entity::Entity, query::ReadOnlyQueryData};
|
||||||
/// for documenting possible looping behavior, and consumers of those implementations are responsible for
|
/// for documenting possible looping behavior, and consumers of those implementations are responsible for
|
||||||
/// avoiding infinite loops in their code.
|
/// 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<E>`. This allows traversal to differ depending on event
|
||||||
|
/// data.
|
||||||
|
///
|
||||||
/// [specify the direction]: crate::event::Event::Traversal
|
/// [specify the direction]: crate::event::Event::Traversal
|
||||||
/// [event propagation]: crate::observer::Trigger::propagate
|
/// [event propagation]: crate::observer::Trigger::propagate
|
||||||
/// [observers]: crate::observer::Observer
|
/// [observers]: crate::observer::Observer
|
||||||
pub trait Traversal: ReadOnlyQueryData {
|
pub trait Traversal<D: ?Sized>: ReadOnlyQueryData {
|
||||||
/// Returns the next entity to visit.
|
/// Returns the next entity to visit.
|
||||||
fn traverse(item: Self::Item<'_>) -> Option<Entity>;
|
fn traverse(item: Self::Item<'_>, data: &D) -> Option<Entity>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Traversal for () {
|
impl<D> Traversal<D> for () {
|
||||||
fn traverse(_: Self::Item<'_>) -> Option<Entity> {
|
fn traverse(_: Self::Item<'_>, _data: &D) -> Option<Entity> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -534,7 +534,7 @@ impl<'w> DeferredWorld<'w> {
|
||||||
data: &mut E,
|
data: &mut E,
|
||||||
mut propagate: bool,
|
mut propagate: bool,
|
||||||
) where
|
) where
|
||||||
T: Traversal,
|
T: Traversal<E>,
|
||||||
{
|
{
|
||||||
loop {
|
loop {
|
||||||
Observers::invoke::<_>(
|
Observers::invoke::<_>(
|
||||||
|
@ -552,7 +552,7 @@ impl<'w> DeferredWorld<'w> {
|
||||||
.get_entity(entity)
|
.get_entity(entity)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|entity| entity.get_components::<T>())
|
.and_then(|entity| entity.get_components::<T>())
|
||||||
.and_then(T::traverse)
|
.and_then(|item| T::traverse(item, data))
|
||||||
{
|
{
|
||||||
entity = traverse_to;
|
entity = traverse_to;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -93,8 +93,8 @@ impl Deref for Parent {
|
||||||
/// `Parent::traverse` will never form loops in properly-constructed hierarchies.
|
/// `Parent::traverse` will never form loops in properly-constructed hierarchies.
|
||||||
///
|
///
|
||||||
/// [event propagation]: bevy_ecs::observer::Trigger::propagate
|
/// [event propagation]: bevy_ecs::observer::Trigger::propagate
|
||||||
impl Traversal for &Parent {
|
impl<D> Traversal<D> for &Parent {
|
||||||
fn traverse(item: Self::Item<'_>) -> Option<Entity> {
|
fn traverse(item: Self::Item<'_>, _data: &D) -> Option<Entity> {
|
||||||
Some(item.0)
|
Some(item.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,11 +39,13 @@
|
||||||
|
|
||||||
use core::fmt::Debug;
|
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_hierarchy::Parent;
|
||||||
use bevy_math::Vec2;
|
use bevy_math::Vec2;
|
||||||
use bevy_reflect::prelude::*;
|
use bevy_reflect::prelude::*;
|
||||||
|
use bevy_render::camera::NormalizedRenderTarget;
|
||||||
use bevy_utils::{tracing::debug, Duration, HashMap, Instant};
|
use bevy_utils::{tracing::debug, Duration, HashMap, Instant};
|
||||||
|
use bevy_window::Window;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::{prelude::PointerLocation, HitData},
|
backend::{prelude::PointerLocation, HitData},
|
||||||
|
@ -71,11 +73,44 @@ pub struct Pointer<E: Debug + Clone + Reflect> {
|
||||||
pub event: E,
|
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<E> Traversal<Pointer<E>> for PointerTraversal
|
||||||
|
where
|
||||||
|
E: Debug + Clone + Reflect,
|
||||||
|
{
|
||||||
|
fn traverse(item: Self::Item<'_>, pointer: &Pointer<E>) -> Option<Entity> {
|
||||||
|
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<E> Event for Pointer<E>
|
impl<E> Event for Pointer<E>
|
||||||
where
|
where
|
||||||
E: Debug + Clone + Reflect,
|
E: Debug + Clone + Reflect,
|
||||||
{
|
{
|
||||||
type Traversal = &'static Parent;
|
type Traversal = PointerTraversal;
|
||||||
|
|
||||||
const AUTO_PROPAGATE: bool = true;
|
const AUTO_PROPAGATE: bool = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,6 +159,7 @@ pub mod input;
|
||||||
#[cfg(feature = "bevy_mesh_picking_backend")]
|
#[cfg(feature = "bevy_mesh_picking_backend")]
|
||||||
pub mod mesh_picking;
|
pub mod mesh_picking;
|
||||||
pub mod pointer;
|
pub mod pointer;
|
||||||
|
pub mod window;
|
||||||
|
|
||||||
use bevy_app::{prelude::*, PluginGroupBuilder};
|
use bevy_app::{prelude::*, PluginGroupBuilder};
|
||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
|
@ -298,6 +299,8 @@ pub struct PickingPlugin {
|
||||||
pub is_input_enabled: bool,
|
pub is_input_enabled: bool,
|
||||||
/// Enables and disables updating interaction states of entities.
|
/// Enables and disables updating interaction states of entities.
|
||||||
pub is_focus_enabled: bool,
|
pub is_focus_enabled: bool,
|
||||||
|
/// Enables or disables picking for window entities.
|
||||||
|
pub is_window_picking_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PickingPlugin {
|
impl PickingPlugin {
|
||||||
|
@ -305,11 +308,17 @@ impl PickingPlugin {
|
||||||
pub fn input_should_run(state: Res<Self>) -> bool {
|
pub fn input_should_run(state: Res<Self>) -> bool {
|
||||||
state.is_input_enabled && state.is_enabled
|
state.is_input_enabled && state.is_enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether or not systems updating entities' [`PickingInteraction`](focus::PickingInteraction)
|
/// Whether or not systems updating entities' [`PickingInteraction`](focus::PickingInteraction)
|
||||||
/// component should be running.
|
/// component should be running.
|
||||||
pub fn focus_should_run(state: Res<Self>) -> bool {
|
pub fn focus_should_run(state: Res<Self>) -> bool {
|
||||||
state.is_focus_enabled && state.is_enabled
|
state.is_focus_enabled && state.is_enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether or not window entities should receive pick events.
|
||||||
|
pub fn window_picking_should_run(state: Res<Self>) -> bool {
|
||||||
|
state.is_window_picking_enabled && state.is_enabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PickingPlugin {
|
impl Default for PickingPlugin {
|
||||||
|
@ -318,6 +327,7 @@ impl Default for PickingPlugin {
|
||||||
is_enabled: true,
|
is_enabled: true,
|
||||||
is_input_enabled: true,
|
is_input_enabled: true,
|
||||||
is_focus_enabled: true,
|
is_focus_enabled: true,
|
||||||
|
is_window_picking_enabled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -342,6 +352,12 @@ impl Plugin for PickingPlugin {
|
||||||
)
|
)
|
||||||
.in_set(PickSet::ProcessInput),
|
.in_set(PickSet::ProcessInput),
|
||||||
)
|
)
|
||||||
|
.add_systems(
|
||||||
|
PreUpdate,
|
||||||
|
window::update_window_hits
|
||||||
|
.run_if(Self::window_picking_should_run)
|
||||||
|
.in_set(PickSet::Backend),
|
||||||
|
)
|
||||||
.configure_sets(
|
.configure_sets(
|
||||||
First,
|
First,
|
||||||
(PickSet::Input, PickSet::PostInput)
|
(PickSet::Input, PickSet::PostInput)
|
||||||
|
|
45
crates/bevy_picking/src/window.rs
Normal file
45
crates/bevy_picking/src/window.rs
Normal file
|
@ -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<PointerHits>,
|
||||||
|
) {
|
||||||
|
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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue