mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
hooking up observers and clicking for ui node (#14695)
Makes the newly merged picking usable for UI elements. currently it both triggers the events, as well as sends them as throught commands.trigger_targets. We should probably figure out if this is needed for them all. # Objective Hooks up obserers and picking for a very simple example ## Solution upstreamed the UI picking backend from bevy_mod_picking ## Testing tested with the new example picking/simple_picking.rs --- --------- Co-authored-by: Lixou <82600264+DasLixou@users.noreply.github.com> Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Kristoffer Søholm <k.soeholm@gmail.com>
This commit is contained in:
parent
0ea46663b0
commit
6adf31babf
13 changed files with 455 additions and 66 deletions
12
Cargo.toml
12
Cargo.toml
|
@ -3352,6 +3352,18 @@ description = "Demonstrates how to rotate the skybox and the environment map sim
|
|||
category = "3D Rendering"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "simple_picking"
|
||||
path = "examples/picking/simple_picking.rs"
|
||||
doc-scrape-examples = true
|
||||
required-features = ["bevy_picking"]
|
||||
|
||||
[package.metadata.example.simple_picking]
|
||||
name = "Showcases simple picking events and usage"
|
||||
description = "Demonstrates how to use picking events to spawn simple objects"
|
||||
category = "Picking"
|
||||
wasm = true
|
||||
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
opt-level = "z"
|
||||
|
|
|
@ -191,7 +191,7 @@ meshlet_processor = ["bevy_pbr?/meshlet_processor"]
|
|||
bevy_dev_tools = ["dep:bevy_dev_tools"]
|
||||
|
||||
# Provides a picking functionality
|
||||
bevy_picking = ["dep:bevy_picking"]
|
||||
bevy_picking = ["dep:bevy_picking", "bevy_ui?/bevy_picking"]
|
||||
|
||||
# Enable support for the ios_simulator by downgrading some rendering capabilities
|
||||
ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"]
|
||||
|
|
|
@ -56,6 +56,8 @@ plugin_group! {
|
|||
bevy_gizmos:::GizmoPlugin,
|
||||
#[cfg(feature = "bevy_state")]
|
||||
bevy_state::app:::StatesPlugin,
|
||||
#[cfg(feature = "bevy_picking")]
|
||||
bevy_picking:::DefaultPickingPlugins,
|
||||
#[cfg(feature = "bevy_dev_tools")]
|
||||
bevy_dev_tools:::DevToolsPlugin,
|
||||
#[cfg(feature = "bevy_ci_testing")]
|
||||
|
|
|
@ -66,3 +66,7 @@ pub use crate::state::prelude::*;
|
|||
#[doc(hidden)]
|
||||
#[cfg(feature = "bevy_gltf")]
|
||||
pub use crate::gltf::prelude::*;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "bevy_picking")]
|
||||
pub use crate::picking::prelude::*;
|
||||
|
|
|
@ -20,6 +20,9 @@ bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
|
|||
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
|
||||
bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }
|
||||
|
||||
bevy_time = { path = "../bevy_time", version = "0.15.0-dev" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" }
|
||||
|
||||
uuid = { version = "1.1", features = ["v4"] }
|
||||
|
||||
[lints]
|
||||
|
|
|
@ -205,6 +205,7 @@ pub struct Drop {
|
|||
/// Generates pointer events from input and focus data
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn pointer_events(
|
||||
mut commands: Commands,
|
||||
// Input
|
||||
mut input_presses: EventReader<InputPress>,
|
||||
mut input_moves: EventReader<InputMove>,
|
||||
|
@ -237,12 +238,14 @@ pub fn pointer_events(
|
|||
.iter()
|
||||
.flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned())))
|
||||
{
|
||||
pointer_move.send(Pointer::new(
|
||||
let event = Pointer::new(
|
||||
pointer_id,
|
||||
location.clone(),
|
||||
hovered_entity,
|
||||
Move { hit, delta },
|
||||
));
|
||||
);
|
||||
commands.trigger_targets(event.clone(), event.target);
|
||||
pointer_move.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -264,12 +267,14 @@ pub fn pointer_events(
|
|||
);
|
||||
continue;
|
||||
};
|
||||
pointer_up.send(Pointer::new(
|
||||
let event = Pointer::new(
|
||||
press_event.pointer_id,
|
||||
location,
|
||||
hovered_entity,
|
||||
Up { button, hit },
|
||||
));
|
||||
);
|
||||
commands.trigger_targets(event.clone(), event.target);
|
||||
pointer_up.send(event);
|
||||
}
|
||||
}
|
||||
for (hovered_entity, hit) in hover_map
|
||||
|
@ -285,12 +290,14 @@ pub fn pointer_events(
|
|||
);
|
||||
continue;
|
||||
};
|
||||
pointer_down.send(Pointer::new(
|
||||
let event = Pointer::new(
|
||||
press_event.pointer_id,
|
||||
location,
|
||||
hovered_entity,
|
||||
Down { button, hit },
|
||||
));
|
||||
);
|
||||
commands.trigger_targets(event.clone(), event.target);
|
||||
pointer_down.send(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -313,12 +320,9 @@ pub fn pointer_events(
|
|||
);
|
||||
continue;
|
||||
};
|
||||
pointer_over.send(Pointer::new(
|
||||
pointer_id,
|
||||
location,
|
||||
hovered_entity,
|
||||
Over { hit },
|
||||
));
|
||||
let event = Pointer::new(pointer_id, location, hovered_entity, Over { hit });
|
||||
commands.trigger_targets(event.clone(), event.target);
|
||||
pointer_over.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -340,12 +344,9 @@ pub fn pointer_events(
|
|||
);
|
||||
continue;
|
||||
};
|
||||
pointer_out.send(Pointer::new(
|
||||
pointer_id,
|
||||
location,
|
||||
hovered_entity,
|
||||
Out { hit },
|
||||
));
|
||||
let event = Pointer::new(pointer_id, location, hovered_entity, Out { hit });
|
||||
commands.trigger_targets(event.clone(), event.target);
|
||||
pointer_out.send(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -366,6 +367,11 @@ pub struct DragEntry {
|
|||
/// Uses pointer events to determine when click and drag events occur.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn send_click_and_drag_events(
|
||||
// for triggering observers
|
||||
// - Pointer<Click>
|
||||
// - Pointer<Drag>
|
||||
// - Pointer<DragStart>
|
||||
mut commands: Commands,
|
||||
// Input
|
||||
mut pointer_down: EventReader<Pointer<Down>>,
|
||||
mut pointer_up: EventReader<Pointer<Up>>,
|
||||
|
@ -377,12 +383,9 @@ pub fn send_click_and_drag_events(
|
|||
mut down_map: Local<
|
||||
HashMap<(PointerId, PointerButton), HashMap<Entity, (Pointer<Down>, Instant)>>,
|
||||
>,
|
||||
// Output
|
||||
// Outputs used for further processing
|
||||
mut drag_map: ResMut<DragMap>,
|
||||
mut pointer_click: EventWriter<Pointer<Click>>,
|
||||
mut pointer_drag_start: EventWriter<Pointer<DragStart>>,
|
||||
mut pointer_drag_end: EventWriter<Pointer<DragEnd>>,
|
||||
mut pointer_drag: EventWriter<Pointer<Drag>>,
|
||||
) {
|
||||
let pointer_location = |pointer_id: PointerId| {
|
||||
pointer_map
|
||||
|
@ -415,7 +418,7 @@ pub fn send_click_and_drag_events(
|
|||
latest_pos: down.pointer_location.position,
|
||||
},
|
||||
);
|
||||
pointer_drag_start.send(Pointer::new(
|
||||
let event = Pointer::new(
|
||||
pointer_id,
|
||||
down.pointer_location.clone(),
|
||||
down.target,
|
||||
|
@ -423,7 +426,8 @@ pub fn send_click_and_drag_events(
|
|||
button,
|
||||
hit: down.hit.clone(),
|
||||
},
|
||||
));
|
||||
);
|
||||
commands.trigger_targets(event, down.target);
|
||||
}
|
||||
|
||||
for (dragged_entity, drag) in drag_list.iter_mut() {
|
||||
|
@ -433,12 +437,9 @@ pub fn send_click_and_drag_events(
|
|||
delta: location.position - drag.latest_pos,
|
||||
};
|
||||
drag.latest_pos = location.position;
|
||||
pointer_drag.send(Pointer::new(
|
||||
pointer_id,
|
||||
location.clone(),
|
||||
*dragged_entity,
|
||||
drag_event,
|
||||
));
|
||||
let target = *dragged_entity;
|
||||
let event = Pointer::new(pointer_id, location.clone(), target, drag_event);
|
||||
commands.trigger_targets(event, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -458,7 +459,7 @@ pub fn send_click_and_drag_events(
|
|||
.and_then(|down| down.get(&target))
|
||||
{
|
||||
let duration = now - *down_instant;
|
||||
pointer_click.send(Pointer::new(
|
||||
let event = Pointer::new(
|
||||
pointer_id,
|
||||
pointer_location,
|
||||
target,
|
||||
|
@ -467,7 +468,8 @@ pub fn send_click_and_drag_events(
|
|||
hit,
|
||||
duration,
|
||||
},
|
||||
));
|
||||
);
|
||||
commands.trigger_targets(event, target);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -501,12 +503,9 @@ pub fn send_click_and_drag_events(
|
|||
button: press.button,
|
||||
distance: drag.latest_pos - drag.start_pos,
|
||||
};
|
||||
pointer_drag_end.send(Pointer::new(
|
||||
press.pointer_id,
|
||||
location.clone(),
|
||||
drag_target,
|
||||
drag_end,
|
||||
));
|
||||
let event = Pointer::new(press.pointer_id, location.clone(), drag_target, drag_end);
|
||||
commands.trigger_targets(event.clone(), event.target);
|
||||
pointer_drag_end.send(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -514,6 +513,12 @@ pub fn send_click_and_drag_events(
|
|||
/// Uses pointer events to determine when drag-over events occur
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn send_drag_over_events(
|
||||
// uses this to trigger the following
|
||||
// - Pointer<DragEnter>,
|
||||
// - Pointer<DragOver>,
|
||||
// - Pointer<DragLeave>,
|
||||
// - Pointer<Drop>,
|
||||
mut commands: Commands,
|
||||
// Input
|
||||
drag_map: Res<DragMap>,
|
||||
mut pointer_over: EventReader<Pointer<Over>>,
|
||||
|
@ -522,12 +527,6 @@ pub fn send_drag_over_events(
|
|||
mut pointer_drag_end: EventReader<Pointer<DragEnd>>,
|
||||
// Local
|
||||
mut drag_over_map: Local<HashMap<(PointerId, PointerButton), HashMap<Entity, HitData>>>,
|
||||
|
||||
// Output
|
||||
mut pointer_drag_enter: EventWriter<Pointer<DragEnter>>,
|
||||
mut pointer_drag_over: EventWriter<Pointer<DragOver>>,
|
||||
mut pointer_drag_leave: EventWriter<Pointer<DragLeave>>,
|
||||
mut pointer_drop: EventWriter<Pointer<Drop>>,
|
||||
) {
|
||||
// Fire PointerDragEnter events.
|
||||
for Pointer {
|
||||
|
@ -548,17 +547,17 @@ pub fn send_drag_over_events(
|
|||
{
|
||||
let drag_entry = drag_over_map.entry((pointer_id, button)).or_default();
|
||||
drag_entry.insert(target, hit.clone());
|
||||
let event = DragEnter {
|
||||
button,
|
||||
dragged: *drag_target,
|
||||
hit: hit.clone(),
|
||||
};
|
||||
pointer_drag_enter.send(Pointer::new(
|
||||
let event = Pointer::new(
|
||||
pointer_id,
|
||||
pointer_location.clone(),
|
||||
target,
|
||||
event,
|
||||
));
|
||||
DragEnter {
|
||||
button,
|
||||
dragged: *drag_target,
|
||||
hit: hit.clone(),
|
||||
},
|
||||
);
|
||||
commands.trigger_targets(event, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -580,7 +579,7 @@ pub fn send_drag_over_events(
|
|||
|&&drag_target| target != drag_target, /* can't drag over itself */
|
||||
)
|
||||
{
|
||||
pointer_drag_over.send(Pointer::new(
|
||||
let event = Pointer::new(
|
||||
pointer_id,
|
||||
pointer_location.clone(),
|
||||
target,
|
||||
|
@ -589,7 +588,8 @@ pub fn send_drag_over_events(
|
|||
dragged: *drag_target,
|
||||
hit: hit.clone(),
|
||||
},
|
||||
));
|
||||
);
|
||||
commands.trigger_targets(event, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -598,7 +598,7 @@ pub fn send_drag_over_events(
|
|||
for Pointer {
|
||||
pointer_id,
|
||||
pointer_location,
|
||||
target,
|
||||
target: drag_end_target,
|
||||
event: DragEnd {
|
||||
button,
|
||||
distance: _,
|
||||
|
@ -609,26 +609,30 @@ pub fn send_drag_over_events(
|
|||
continue;
|
||||
};
|
||||
for (dragged_over, hit) in drag_over_set.drain() {
|
||||
pointer_drag_leave.send(Pointer::new(
|
||||
let target = dragged_over;
|
||||
let event = Pointer::new(
|
||||
pointer_id,
|
||||
pointer_location.clone(),
|
||||
dragged_over,
|
||||
DragLeave {
|
||||
button,
|
||||
dragged: target,
|
||||
dragged: drag_end_target,
|
||||
hit: hit.clone(),
|
||||
},
|
||||
));
|
||||
pointer_drop.send(Pointer::new(
|
||||
);
|
||||
commands.trigger_targets(event, target);
|
||||
|
||||
let event = Pointer::new(
|
||||
pointer_id,
|
||||
pointer_location.clone(),
|
||||
dragged_over,
|
||||
target,
|
||||
Drop {
|
||||
button,
|
||||
dropped: target,
|
||||
hit: hit.clone(),
|
||||
},
|
||||
));
|
||||
);
|
||||
commands.trigger_targets(event, target);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -651,7 +655,7 @@ pub fn send_drag_over_events(
|
|||
continue;
|
||||
};
|
||||
for drag_target in drag_list.keys() {
|
||||
pointer_drag_leave.send(Pointer::new(
|
||||
let event = Pointer::new(
|
||||
pointer_id,
|
||||
pointer_location.clone(),
|
||||
target,
|
||||
|
@ -660,7 +664,8 @@ pub fn send_drag_over_events(
|
|||
dragged: *drag_target,
|
||||
hit: hit.clone(),
|
||||
},
|
||||
));
|
||||
);
|
||||
commands.trigger_targets(event, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,15 @@ use bevy_app::prelude::*;
|
|||
use bevy_ecs::prelude::*;
|
||||
use bevy_reflect::prelude::*;
|
||||
|
||||
/// common exports for picking interaction
|
||||
pub mod prelude {
|
||||
#[doc(hidden)]
|
||||
pub use crate::{
|
||||
events::*, input::InputPlugin, pointer::PointerButton, DefaultPickingPlugins,
|
||||
InteractionPlugin, Pickable, PickingPlugin, PickingPluginsSettings,
|
||||
};
|
||||
}
|
||||
|
||||
/// Used to globally toggle picking features at runtime.
|
||||
#[derive(Clone, Debug, Resource, Reflect)]
|
||||
#[reflect(Resource, Default)]
|
||||
|
@ -167,8 +176,27 @@ pub enum PickSet {
|
|||
Last,
|
||||
}
|
||||
|
||||
/// One plugin that contains the [`input::InputPlugin`], [`PickingPlugin`] and the [`InteractionPlugin`],
|
||||
/// this is probably the plugin that will be most used.
|
||||
/// Note: for any of these plugins to work, they require a picking backend to be active,
|
||||
/// The picking backend is responsible to turn an input, into a [`crate::backend::PointerHits`]
|
||||
/// that [`PickingPlugin`] and [`InteractionPlugin`] will refine into [`bevy_ecs::observer::Trigger`]s.
|
||||
#[derive(Default)]
|
||||
pub struct DefaultPickingPlugins;
|
||||
|
||||
impl Plugin for DefaultPickingPlugins {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins((
|
||||
input::InputPlugin::default(),
|
||||
PickingPlugin,
|
||||
InteractionPlugin,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// This plugin sets up the core picking infrastructure. It receives input events, and provides the shared
|
||||
/// types used by other picking plugins.
|
||||
#[derive(Default)]
|
||||
pub struct PickingPlugin;
|
||||
|
||||
impl Plugin for PickingPlugin {
|
||||
|
@ -185,11 +213,18 @@ impl Plugin for PickingPlugin {
|
|||
pointer::update_pointer_map,
|
||||
pointer::InputMove::receive,
|
||||
pointer::InputPress::receive,
|
||||
backend::ray::RayMap::repopulate,
|
||||
backend::ray::RayMap::repopulate.after(pointer::InputMove::receive),
|
||||
)
|
||||
.in_set(PickSet::ProcessInput),
|
||||
)
|
||||
.configure_sets(First, (PickSet::Input, PickSet::PostInput).chain())
|
||||
.configure_sets(
|
||||
First,
|
||||
(PickSet::Input, PickSet::PostInput)
|
||||
.after(bevy_time::TimeSystem)
|
||||
.ambiguous_with(bevy_asset::handle_internal_asset_events)
|
||||
.after(bevy_ecs::event::EventUpdates)
|
||||
.chain(),
|
||||
)
|
||||
.configure_sets(
|
||||
PreUpdate,
|
||||
(
|
||||
|
@ -200,6 +235,7 @@ impl Plugin for PickingPlugin {
|
|||
// Eventually events will need to be dispatched here
|
||||
PickSet::Last,
|
||||
)
|
||||
.ambiguous_with(bevy_asset::handle_internal_asset_events)
|
||||
.chain(),
|
||||
)
|
||||
.register_type::<pointer::PointerId>()
|
||||
|
@ -213,6 +249,7 @@ impl Plugin for PickingPlugin {
|
|||
}
|
||||
|
||||
/// Generates [`Pointer`](events::Pointer) events and handles event bubbling.
|
||||
#[derive(Default)]
|
||||
pub struct InteractionPlugin;
|
||||
|
||||
impl Plugin for InteractionPlugin {
|
||||
|
@ -224,6 +261,12 @@ impl Plugin for InteractionPlugin {
|
|||
.init_resource::<focus::PreviousHoverMap>()
|
||||
.init_resource::<DragMap>()
|
||||
.add_event::<PointerCancel>()
|
||||
.add_event::<Pointer<Down>>()
|
||||
.add_event::<Pointer<Up>>()
|
||||
.add_event::<Pointer<Move>>()
|
||||
.add_event::<Pointer<Over>>()
|
||||
.add_event::<Pointer<Out>>()
|
||||
.add_event::<Pointer<DragEnd>>()
|
||||
.add_systems(
|
||||
PreUpdate,
|
||||
(
|
||||
|
|
|
@ -26,6 +26,7 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [
|
|||
bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
|
||||
bevy_sprite = { path = "../bevy_sprite", version = "0.15.0-dev" }
|
||||
bevy_text = { path = "../bevy_text", version = "0.15.0-dev", optional = true }
|
||||
bevy_picking = { path = "../bevy_picking", version = "0.15.0-dev", optional = true }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
|
||||
bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
|
||||
|
@ -40,6 +41,7 @@ smallvec = "1.11"
|
|||
|
||||
[features]
|
||||
serialize = ["serde", "smallvec/serde", "bevy_math/serialize"]
|
||||
bevy_picking = ["dep:bevy_picking"]
|
||||
|
||||
|
||||
[lints]
|
||||
|
|
|
@ -17,6 +17,9 @@ pub mod ui_material;
|
|||
pub mod update;
|
||||
pub mod widget;
|
||||
|
||||
#[cfg(feature = "bevy_picking")]
|
||||
pub mod picking_backend;
|
||||
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_reflect::Reflect;
|
||||
#[cfg(feature = "bevy_text")]
|
||||
|
@ -202,6 +205,9 @@ impl Plugin for UiPlugin {
|
|||
build_text_interop(app);
|
||||
|
||||
build_ui_render(app);
|
||||
|
||||
#[cfg(feature = "bevy_picking")]
|
||||
app.add_plugins(picking_backend::UiPickingBackend);
|
||||
}
|
||||
|
||||
fn finish(&self, app: &mut App) {
|
||||
|
|
214
crates/bevy_ui/src/picking_backend.rs
Normal file
214
crates/bevy_ui/src/picking_backend.rs
Normal file
|
@ -0,0 +1,214 @@
|
|||
//! A picking backend for UI nodes.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! This backend does not require markers on cameras or entities to function. It will look for any
|
||||
//! pointers using the same render target as the UI camera, and run hit tests on the UI node tree.
|
||||
//!
|
||||
//! ## Important Note
|
||||
//!
|
||||
//! This backend completely ignores [`FocusPolicy`](crate::FocusPolicy). The design of `bevy_ui`'s
|
||||
//! focus systems and the picking plugin are not compatible. Instead, use the [`Pickable`] component
|
||||
//! to customize how an entity responds to picking focus. Nodes without the [`Pickable`] component
|
||||
//! will not trigger events.
|
||||
//!
|
||||
//! ## Implementation Notes
|
||||
//!
|
||||
//! - `bevy_ui` can only render to the primary window
|
||||
//! - `bevy_ui` can render on any camera with a flag, it is special, and is not tied to a particular
|
||||
//! camera.
|
||||
//! - To correctly sort picks, the order of `bevy_ui` is set to be the camera order plus 0.5.
|
||||
|
||||
#![allow(clippy::type_complexity)]
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use crate::{prelude::*, UiStack};
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_ecs::{prelude::*, query::QueryData};
|
||||
use bevy_math::Vec2;
|
||||
use bevy_render::prelude::*;
|
||||
use bevy_transform::prelude::*;
|
||||
use bevy_utils::hashbrown::HashMap;
|
||||
use bevy_window::PrimaryWindow;
|
||||
|
||||
use bevy_picking::backend::prelude::*;
|
||||
|
||||
/// A plugin that adds picking support for UI nodes.
|
||||
#[derive(Clone)]
|
||||
pub struct UiPickingBackend;
|
||||
impl Plugin for UiPickingBackend {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(PreUpdate, ui_picking.in_set(PickSet::Backend));
|
||||
}
|
||||
}
|
||||
|
||||
/// Main query from bevy's `ui_focus_system`
|
||||
#[derive(QueryData)]
|
||||
#[query_data(mutable)]
|
||||
pub struct NodeQuery {
|
||||
entity: Entity,
|
||||
node: &'static Node,
|
||||
global_transform: &'static GlobalTransform,
|
||||
pickable: Option<&'static Pickable>,
|
||||
calculated_clip: Option<&'static CalculatedClip>,
|
||||
view_visibility: Option<&'static ViewVisibility>,
|
||||
target_camera: Option<&'static TargetCamera>,
|
||||
}
|
||||
|
||||
/// Computes the UI node entities under each pointer.
|
||||
///
|
||||
/// Bevy's [`UiStack`] orders all nodes in the order they will be rendered, which is the same order
|
||||
/// we need for determining picking.
|
||||
pub fn ui_picking(
|
||||
pointers: Query<(&PointerId, &PointerLocation)>,
|
||||
camera_query: Query<(Entity, &Camera, Has<IsDefaultUiCamera>)>,
|
||||
default_ui_camera: DefaultUiCamera,
|
||||
primary_window: Query<Entity, With<PrimaryWindow>>,
|
||||
ui_scale: Res<UiScale>,
|
||||
ui_stack: Res<UiStack>,
|
||||
mut node_query: Query<NodeQuery>,
|
||||
mut output: EventWriter<PointerHits>,
|
||||
) {
|
||||
// For each camera, the pointer and its position
|
||||
let mut pointer_pos_by_camera = HashMap::<Entity, HashMap<PointerId, Vec2>>::new();
|
||||
|
||||
for (pointer_id, pointer_location) in
|
||||
pointers.iter().filter_map(|(pointer, pointer_location)| {
|
||||
Some(*pointer).zip(pointer_location.location().cloned())
|
||||
})
|
||||
{
|
||||
// This pointer is associated with a render target, which could be used by multiple
|
||||
// cameras. We want to ensure we return all cameras with a matching target.
|
||||
for camera in camera_query
|
||||
.iter()
|
||||
.map(|(entity, camera, _)| {
|
||||
(
|
||||
entity,
|
||||
camera.target.normalize(primary_window.get_single().ok()),
|
||||
)
|
||||
})
|
||||
.filter_map(|(entity, target)| Some(entity).zip(target))
|
||||
.filter(|(_entity, target)| target == &pointer_location.target)
|
||||
.map(|(cam_entity, _target)| cam_entity)
|
||||
{
|
||||
let Ok((_, camera_data, _)) = camera_query.get(camera) else {
|
||||
continue;
|
||||
};
|
||||
let mut pointer_pos = pointer_location.position;
|
||||
if let Some(viewport) = camera_data.logical_viewport_rect() {
|
||||
pointer_pos -= viewport.min;
|
||||
}
|
||||
let scaled_pointer_pos = pointer_pos / **ui_scale;
|
||||
pointer_pos_by_camera
|
||||
.entry(camera)
|
||||
.or_default()
|
||||
.insert(pointer_id, scaled_pointer_pos);
|
||||
}
|
||||
}
|
||||
|
||||
// The list of node entities hovered for each (camera, pointer) combo
|
||||
let mut hit_nodes = HashMap::<(Entity, PointerId), Vec<Entity>>::new();
|
||||
|
||||
// prepare an iterator that contains all the nodes that have the cursor in their rect,
|
||||
// from the top node to the bottom one. this will also reset the interaction to `None`
|
||||
// for all nodes encountered that are no longer hovered.
|
||||
for node_entity in ui_stack
|
||||
.uinodes
|
||||
.iter()
|
||||
// reverse the iterator to traverse the tree from closest nodes to furthest
|
||||
.rev()
|
||||
{
|
||||
let Ok(node) = node_query.get_mut(*node_entity) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Nodes that are not rendered should not be interactable
|
||||
if node
|
||||
.view_visibility
|
||||
.map(|view_visibility| view_visibility.get())
|
||||
!= Some(true)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Some(camera_entity) = node
|
||||
.target_camera
|
||||
.map(TargetCamera::entity)
|
||||
.or(default_ui_camera.get())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let node_rect = node.node.logical_rect(node.global_transform);
|
||||
|
||||
// Nodes with Display::None have a (0., 0.) logical rect and can be ignored
|
||||
if node_rect.size() == Vec2::ZERO {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Intersect with the calculated clip rect to find the bounds of the visible region of the node
|
||||
let visible_rect = node
|
||||
.calculated_clip
|
||||
.map(|clip| node_rect.intersect(clip.clip))
|
||||
.unwrap_or(node_rect);
|
||||
|
||||
let pointers_on_this_cam = pointer_pos_by_camera.get(&camera_entity);
|
||||
|
||||
// The mouse position relative to the node
|
||||
// (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner
|
||||
// Coordinates are relative to the entire node, not just the visible region.
|
||||
for (pointer_id, cursor_position) in pointers_on_this_cam.iter().flat_map(|h| h.iter()) {
|
||||
let relative_cursor_position = (*cursor_position - node_rect.min) / node_rect.size();
|
||||
|
||||
if visible_rect
|
||||
.normalize(node_rect)
|
||||
.contains(relative_cursor_position)
|
||||
{
|
||||
hit_nodes
|
||||
.entry((camera_entity, *pointer_id))
|
||||
.or_default()
|
||||
.push(*node_entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ((camera, pointer), hovered_nodes) in hit_nodes.iter() {
|
||||
// As soon as a node with a `Block` focus policy is detected, the iteration will stop on it
|
||||
// because it "captures" the interaction.
|
||||
let mut iter = node_query.iter_many_mut(hovered_nodes.iter());
|
||||
let mut picks = Vec::new();
|
||||
let mut depth = 0.0;
|
||||
|
||||
while let Some(node) = iter.fetch_next() {
|
||||
let Some(camera_entity) = node
|
||||
.target_camera
|
||||
.map(TargetCamera::entity)
|
||||
.or(default_ui_camera.get())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
picks.push((node.entity, HitData::new(camera_entity, depth, None, None)));
|
||||
|
||||
if let Some(pickable) = node.pickable {
|
||||
// If an entity has a `Pickable` component, we will use that as the source of truth.
|
||||
if pickable.should_block_lower {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// If the Pickable component doesn't exist, default behavior is to block.
|
||||
break;
|
||||
}
|
||||
|
||||
depth += 0.00001; // keep depth near 0 for precision
|
||||
}
|
||||
|
||||
let order = camera_query
|
||||
.get(*camera)
|
||||
.map(|(_, cam, _)| cam.order)
|
||||
.unwrap_or_default() as f32
|
||||
+ 0.5; // bevy ui can run on any camera, it's a special case
|
||||
|
||||
output.send(PointerHits::new(*pointer, picks, order));
|
||||
}
|
||||
}
|
|
@ -54,6 +54,7 @@ git checkout v0.4.0
|
|||
- [Input](#input)
|
||||
- [Math](#math)
|
||||
- [Movement](#movement)
|
||||
- [Picking](#picking)
|
||||
- [Reflection](#reflection)
|
||||
- [Scene](#scene)
|
||||
- [Shaders](#shaders)
|
||||
|
@ -350,6 +351,12 @@ Example | Description
|
|||
--- | ---
|
||||
[Run physics in a fixed timestep](../examples/movement/physics_in_fixed_timestep.rs) | Handles input, physics, and rendering in an industry-standard way by using a fixed timestep
|
||||
|
||||
## Picking
|
||||
|
||||
Example | Description
|
||||
--- | ---
|
||||
[Showcases simple picking events and usage](../examples/picking/simple_picking.rs) | Demonstrates how to use picking events to spawn simple objects
|
||||
|
||||
## Reflection
|
||||
|
||||
Example | Description
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use std::{
|
||||
f32::consts::PI,
|
||||
ops::Drop,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU32, Ordering},
|
||||
Arc,
|
||||
|
|
90
examples/picking/simple_picking.rs
Normal file
90
examples/picking/simple_picking.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
//! A simple scene to demonstrate picking events
|
||||
|
||||
use bevy::{color::palettes::css::*, prelude::*};
|
||||
|
||||
fn main() {
|
||||
let mut app = App::new();
|
||||
app.add_plugins(DefaultPlugins);
|
||||
|
||||
app.add_systems(Startup, setup);
|
||||
|
||||
app.run();
|
||||
}
|
||||
|
||||
/// set up a simple 3D scene
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
commands
|
||||
.spawn((
|
||||
TextBundle {
|
||||
text: Text::from_section("Click Me to get a box", TextStyle::default()),
|
||||
style: Style {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Percent(10.0),
|
||||
left: Val::Percent(10.0),
|
||||
..default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Pickable::default(),
|
||||
))
|
||||
.observe(
|
||||
|_click: Trigger<Pointer<Click>>,
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
mut num: Local<usize>| {
|
||||
commands.spawn(PbrBundle {
|
||||
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
|
||||
material: materials.add(Color::srgb_u8(124, 144, 255)),
|
||||
transform: Transform::from_xyz(0.0, 0.5 + 1.1 * *num as f32, 0.0),
|
||||
..default()
|
||||
});
|
||||
*num += 1;
|
||||
},
|
||||
)
|
||||
.observe(|evt: Trigger<Pointer<Out>>, mut texts: Query<&mut Text>| {
|
||||
let mut text = texts.get_mut(evt.entity()).unwrap();
|
||||
let first = text.sections.first_mut().unwrap();
|
||||
first.style.color = WHITE.into();
|
||||
})
|
||||
.observe(|evt: Trigger<Pointer<Over>>, mut texts: Query<&mut Text>| {
|
||||
let mut text = texts.get_mut(evt.entity()).unwrap();
|
||||
let first = text.sections.first_mut().unwrap();
|
||||
first.style.color = BLUE.into();
|
||||
});
|
||||
// circular base
|
||||
commands
|
||||
.spawn((
|
||||
PbrBundle {
|
||||
mesh: meshes.add(Circle::new(4.0)),
|
||||
material: materials.add(Color::WHITE),
|
||||
transform: Transform::from_rotation(Quat::from_rotation_x(
|
||||
-std::f32::consts::FRAC_PI_2,
|
||||
)),
|
||||
..default()
|
||||
},
|
||||
Pickable::default(),
|
||||
))
|
||||
.observe(|click: Trigger<Pointer<Click>>| {
|
||||
let click = click.event();
|
||||
println!("{click:?}");
|
||||
});
|
||||
// light
|
||||
commands.spawn(PointLightBundle {
|
||||
point_light: PointLight {
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(4.0, 8.0, 4.0),
|
||||
..default()
|
||||
});
|
||||
// camera
|
||||
commands.spawn(Camera3dBundle {
|
||||
transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||
..default()
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue