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
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"
|
category = "3D Rendering"
|
||||||
wasm = false
|
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]
|
[profile.wasm-release]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
|
@ -191,7 +191,7 @@ meshlet_processor = ["bevy_pbr?/meshlet_processor"]
|
|||||||
bevy_dev_tools = ["dep:bevy_dev_tools"]
|
bevy_dev_tools = ["dep:bevy_dev_tools"]
|
||||||
|
|
||||||
# Provides a picking functionality
|
# 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
|
# Enable support for the ios_simulator by downgrading some rendering capabilities
|
||||||
ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"]
|
ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"]
|
||||||
|
@ -56,6 +56,8 @@ plugin_group! {
|
|||||||
bevy_gizmos:::GizmoPlugin,
|
bevy_gizmos:::GizmoPlugin,
|
||||||
#[cfg(feature = "bevy_state")]
|
#[cfg(feature = "bevy_state")]
|
||||||
bevy_state::app:::StatesPlugin,
|
bevy_state::app:::StatesPlugin,
|
||||||
|
#[cfg(feature = "bevy_picking")]
|
||||||
|
bevy_picking:::DefaultPickingPlugins,
|
||||||
#[cfg(feature = "bevy_dev_tools")]
|
#[cfg(feature = "bevy_dev_tools")]
|
||||||
bevy_dev_tools:::DevToolsPlugin,
|
bevy_dev_tools:::DevToolsPlugin,
|
||||||
#[cfg(feature = "bevy_ci_testing")]
|
#[cfg(feature = "bevy_ci_testing")]
|
||||||
|
@ -66,3 +66,7 @@ pub use crate::state::prelude::*;
|
|||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[cfg(feature = "bevy_gltf")]
|
#[cfg(feature = "bevy_gltf")]
|
||||||
pub use crate::gltf::prelude::*;
|
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_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
|
||||||
bevy_window = { path = "../bevy_window", 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"] }
|
uuid = { version = "1.1", features = ["v4"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
@ -205,6 +205,7 @@ pub struct Drop {
|
|||||||
/// Generates pointer events from input and focus data
|
/// Generates pointer events from input and focus data
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn pointer_events(
|
pub fn pointer_events(
|
||||||
|
mut commands: Commands,
|
||||||
// Input
|
// Input
|
||||||
mut input_presses: EventReader<InputPress>,
|
mut input_presses: EventReader<InputPress>,
|
||||||
mut input_moves: EventReader<InputMove>,
|
mut input_moves: EventReader<InputMove>,
|
||||||
@ -237,12 +238,14 @@ pub fn pointer_events(
|
|||||||
.iter()
|
.iter()
|
||||||
.flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned())))
|
.flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned())))
|
||||||
{
|
{
|
||||||
pointer_move.send(Pointer::new(
|
let event = Pointer::new(
|
||||||
pointer_id,
|
pointer_id,
|
||||||
location.clone(),
|
location.clone(),
|
||||||
hovered_entity,
|
hovered_entity,
|
||||||
Move { hit, delta },
|
Move { hit, delta },
|
||||||
));
|
);
|
||||||
|
commands.trigger_targets(event.clone(), event.target);
|
||||||
|
pointer_move.send(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,12 +267,14 @@ pub fn pointer_events(
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
pointer_up.send(Pointer::new(
|
let event = Pointer::new(
|
||||||
press_event.pointer_id,
|
press_event.pointer_id,
|
||||||
location,
|
location,
|
||||||
hovered_entity,
|
hovered_entity,
|
||||||
Up { button, hit },
|
Up { button, hit },
|
||||||
));
|
);
|
||||||
|
commands.trigger_targets(event.clone(), event.target);
|
||||||
|
pointer_up.send(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (hovered_entity, hit) in hover_map
|
for (hovered_entity, hit) in hover_map
|
||||||
@ -285,12 +290,14 @@ pub fn pointer_events(
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
pointer_down.send(Pointer::new(
|
let event = Pointer::new(
|
||||||
press_event.pointer_id,
|
press_event.pointer_id,
|
||||||
location,
|
location,
|
||||||
hovered_entity,
|
hovered_entity,
|
||||||
Down { button, hit },
|
Down { button, hit },
|
||||||
));
|
);
|
||||||
|
commands.trigger_targets(event.clone(), event.target);
|
||||||
|
pointer_down.send(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -313,12 +320,9 @@ pub fn pointer_events(
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
pointer_over.send(Pointer::new(
|
let event = Pointer::new(pointer_id, location, hovered_entity, Over { hit });
|
||||||
pointer_id,
|
commands.trigger_targets(event.clone(), event.target);
|
||||||
location,
|
pointer_over.send(event);
|
||||||
hovered_entity,
|
|
||||||
Over { hit },
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,12 +344,9 @@ pub fn pointer_events(
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
pointer_out.send(Pointer::new(
|
let event = Pointer::new(pointer_id, location, hovered_entity, Out { hit });
|
||||||
pointer_id,
|
commands.trigger_targets(event.clone(), event.target);
|
||||||
location,
|
pointer_out.send(event);
|
||||||
hovered_entity,
|
|
||||||
Out { hit },
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -366,6 +367,11 @@ pub struct DragEntry {
|
|||||||
/// Uses pointer events to determine when click and drag events occur.
|
/// Uses pointer events to determine when click and drag events occur.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn send_click_and_drag_events(
|
pub fn send_click_and_drag_events(
|
||||||
|
// for triggering observers
|
||||||
|
// - Pointer<Click>
|
||||||
|
// - Pointer<Drag>
|
||||||
|
// - Pointer<DragStart>
|
||||||
|
mut commands: Commands,
|
||||||
// Input
|
// Input
|
||||||
mut pointer_down: EventReader<Pointer<Down>>,
|
mut pointer_down: EventReader<Pointer<Down>>,
|
||||||
mut pointer_up: EventReader<Pointer<Up>>,
|
mut pointer_up: EventReader<Pointer<Up>>,
|
||||||
@ -377,12 +383,9 @@ pub fn send_click_and_drag_events(
|
|||||||
mut down_map: Local<
|
mut down_map: Local<
|
||||||
HashMap<(PointerId, PointerButton), HashMap<Entity, (Pointer<Down>, Instant)>>,
|
HashMap<(PointerId, PointerButton), HashMap<Entity, (Pointer<Down>, Instant)>>,
|
||||||
>,
|
>,
|
||||||
// Output
|
// Outputs used for further processing
|
||||||
mut drag_map: ResMut<DragMap>,
|
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_end: EventWriter<Pointer<DragEnd>>,
|
||||||
mut pointer_drag: EventWriter<Pointer<Drag>>,
|
|
||||||
) {
|
) {
|
||||||
let pointer_location = |pointer_id: PointerId| {
|
let pointer_location = |pointer_id: PointerId| {
|
||||||
pointer_map
|
pointer_map
|
||||||
@ -415,7 +418,7 @@ pub fn send_click_and_drag_events(
|
|||||||
latest_pos: down.pointer_location.position,
|
latest_pos: down.pointer_location.position,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
pointer_drag_start.send(Pointer::new(
|
let event = Pointer::new(
|
||||||
pointer_id,
|
pointer_id,
|
||||||
down.pointer_location.clone(),
|
down.pointer_location.clone(),
|
||||||
down.target,
|
down.target,
|
||||||
@ -423,7 +426,8 @@ pub fn send_click_and_drag_events(
|
|||||||
button,
|
button,
|
||||||
hit: down.hit.clone(),
|
hit: down.hit.clone(),
|
||||||
},
|
},
|
||||||
));
|
);
|
||||||
|
commands.trigger_targets(event, down.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (dragged_entity, drag) in drag_list.iter_mut() {
|
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,
|
delta: location.position - drag.latest_pos,
|
||||||
};
|
};
|
||||||
drag.latest_pos = location.position;
|
drag.latest_pos = location.position;
|
||||||
pointer_drag.send(Pointer::new(
|
let target = *dragged_entity;
|
||||||
pointer_id,
|
let event = Pointer::new(pointer_id, location.clone(), target, drag_event);
|
||||||
location.clone(),
|
commands.trigger_targets(event, target);
|
||||||
*dragged_entity,
|
|
||||||
drag_event,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -458,7 +459,7 @@ pub fn send_click_and_drag_events(
|
|||||||
.and_then(|down| down.get(&target))
|
.and_then(|down| down.get(&target))
|
||||||
{
|
{
|
||||||
let duration = now - *down_instant;
|
let duration = now - *down_instant;
|
||||||
pointer_click.send(Pointer::new(
|
let event = Pointer::new(
|
||||||
pointer_id,
|
pointer_id,
|
||||||
pointer_location,
|
pointer_location,
|
||||||
target,
|
target,
|
||||||
@ -467,7 +468,8 @@ pub fn send_click_and_drag_events(
|
|||||||
hit,
|
hit,
|
||||||
duration,
|
duration,
|
||||||
},
|
},
|
||||||
));
|
);
|
||||||
|
commands.trigger_targets(event, target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -501,12 +503,9 @@ pub fn send_click_and_drag_events(
|
|||||||
button: press.button,
|
button: press.button,
|
||||||
distance: drag.latest_pos - drag.start_pos,
|
distance: drag.latest_pos - drag.start_pos,
|
||||||
};
|
};
|
||||||
pointer_drag_end.send(Pointer::new(
|
let event = Pointer::new(press.pointer_id, location.clone(), drag_target, drag_end);
|
||||||
press.pointer_id,
|
commands.trigger_targets(event.clone(), event.target);
|
||||||
location.clone(),
|
pointer_drag_end.send(event);
|
||||||
drag_target,
|
|
||||||
drag_end,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -514,6 +513,12 @@ pub fn send_click_and_drag_events(
|
|||||||
/// Uses pointer events to determine when drag-over events occur
|
/// Uses pointer events to determine when drag-over events occur
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn send_drag_over_events(
|
pub fn send_drag_over_events(
|
||||||
|
// uses this to trigger the following
|
||||||
|
// - Pointer<DragEnter>,
|
||||||
|
// - Pointer<DragOver>,
|
||||||
|
// - Pointer<DragLeave>,
|
||||||
|
// - Pointer<Drop>,
|
||||||
|
mut commands: Commands,
|
||||||
// Input
|
// Input
|
||||||
drag_map: Res<DragMap>,
|
drag_map: Res<DragMap>,
|
||||||
mut pointer_over: EventReader<Pointer<Over>>,
|
mut pointer_over: EventReader<Pointer<Over>>,
|
||||||
@ -522,12 +527,6 @@ pub fn send_drag_over_events(
|
|||||||
mut pointer_drag_end: EventReader<Pointer<DragEnd>>,
|
mut pointer_drag_end: EventReader<Pointer<DragEnd>>,
|
||||||
// Local
|
// Local
|
||||||
mut drag_over_map: Local<HashMap<(PointerId, PointerButton), HashMap<Entity, HitData>>>,
|
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.
|
// Fire PointerDragEnter events.
|
||||||
for Pointer {
|
for Pointer {
|
||||||
@ -548,17 +547,17 @@ pub fn send_drag_over_events(
|
|||||||
{
|
{
|
||||||
let drag_entry = drag_over_map.entry((pointer_id, button)).or_default();
|
let drag_entry = drag_over_map.entry((pointer_id, button)).or_default();
|
||||||
drag_entry.insert(target, hit.clone());
|
drag_entry.insert(target, hit.clone());
|
||||||
let event = DragEnter {
|
let event = Pointer::new(
|
||||||
button,
|
|
||||||
dragged: *drag_target,
|
|
||||||
hit: hit.clone(),
|
|
||||||
};
|
|
||||||
pointer_drag_enter.send(Pointer::new(
|
|
||||||
pointer_id,
|
pointer_id,
|
||||||
pointer_location.clone(),
|
pointer_location.clone(),
|
||||||
target,
|
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 */
|
|&&drag_target| target != drag_target, /* can't drag over itself */
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
pointer_drag_over.send(Pointer::new(
|
let event = Pointer::new(
|
||||||
pointer_id,
|
pointer_id,
|
||||||
pointer_location.clone(),
|
pointer_location.clone(),
|
||||||
target,
|
target,
|
||||||
@ -589,7 +588,8 @@ pub fn send_drag_over_events(
|
|||||||
dragged: *drag_target,
|
dragged: *drag_target,
|
||||||
hit: hit.clone(),
|
hit: hit.clone(),
|
||||||
},
|
},
|
||||||
));
|
);
|
||||||
|
commands.trigger_targets(event, target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -598,7 +598,7 @@ pub fn send_drag_over_events(
|
|||||||
for Pointer {
|
for Pointer {
|
||||||
pointer_id,
|
pointer_id,
|
||||||
pointer_location,
|
pointer_location,
|
||||||
target,
|
target: drag_end_target,
|
||||||
event: DragEnd {
|
event: DragEnd {
|
||||||
button,
|
button,
|
||||||
distance: _,
|
distance: _,
|
||||||
@ -609,26 +609,30 @@ pub fn send_drag_over_events(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
for (dragged_over, hit) in drag_over_set.drain() {
|
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_id,
|
||||||
pointer_location.clone(),
|
pointer_location.clone(),
|
||||||
dragged_over,
|
dragged_over,
|
||||||
DragLeave {
|
DragLeave {
|
||||||
button,
|
button,
|
||||||
dragged: target,
|
dragged: drag_end_target,
|
||||||
hit: hit.clone(),
|
hit: hit.clone(),
|
||||||
},
|
},
|
||||||
));
|
);
|
||||||
pointer_drop.send(Pointer::new(
|
commands.trigger_targets(event, target);
|
||||||
|
|
||||||
|
let event = Pointer::new(
|
||||||
pointer_id,
|
pointer_id,
|
||||||
pointer_location.clone(),
|
pointer_location.clone(),
|
||||||
dragged_over,
|
target,
|
||||||
Drop {
|
Drop {
|
||||||
button,
|
button,
|
||||||
dropped: target,
|
dropped: target,
|
||||||
hit: hit.clone(),
|
hit: hit.clone(),
|
||||||
},
|
},
|
||||||
));
|
);
|
||||||
|
commands.trigger_targets(event, target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -651,7 +655,7 @@ pub fn send_drag_over_events(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
for drag_target in drag_list.keys() {
|
for drag_target in drag_list.keys() {
|
||||||
pointer_drag_leave.send(Pointer::new(
|
let event = Pointer::new(
|
||||||
pointer_id,
|
pointer_id,
|
||||||
pointer_location.clone(),
|
pointer_location.clone(),
|
||||||
target,
|
target,
|
||||||
@ -660,7 +664,8 @@ pub fn send_drag_over_events(
|
|||||||
dragged: *drag_target,
|
dragged: *drag_target,
|
||||||
hit: hit.clone(),
|
hit: hit.clone(),
|
||||||
},
|
},
|
||||||
));
|
);
|
||||||
|
commands.trigger_targets(event, target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,15 @@ use bevy_app::prelude::*;
|
|||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
use bevy_reflect::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.
|
/// Used to globally toggle picking features at runtime.
|
||||||
#[derive(Clone, Debug, Resource, Reflect)]
|
#[derive(Clone, Debug, Resource, Reflect)]
|
||||||
#[reflect(Resource, Default)]
|
#[reflect(Resource, Default)]
|
||||||
@ -167,8 +176,27 @@ pub enum PickSet {
|
|||||||
Last,
|
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
|
/// This plugin sets up the core picking infrastructure. It receives input events, and provides the shared
|
||||||
/// types used by other picking plugins.
|
/// types used by other picking plugins.
|
||||||
|
#[derive(Default)]
|
||||||
pub struct PickingPlugin;
|
pub struct PickingPlugin;
|
||||||
|
|
||||||
impl Plugin for PickingPlugin {
|
impl Plugin for PickingPlugin {
|
||||||
@ -185,11 +213,18 @@ impl Plugin for PickingPlugin {
|
|||||||
pointer::update_pointer_map,
|
pointer::update_pointer_map,
|
||||||
pointer::InputMove::receive,
|
pointer::InputMove::receive,
|
||||||
pointer::InputPress::receive,
|
pointer::InputPress::receive,
|
||||||
backend::ray::RayMap::repopulate,
|
backend::ray::RayMap::repopulate.after(pointer::InputMove::receive),
|
||||||
)
|
)
|
||||||
.in_set(PickSet::ProcessInput),
|
.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(
|
.configure_sets(
|
||||||
PreUpdate,
|
PreUpdate,
|
||||||
(
|
(
|
||||||
@ -200,6 +235,7 @@ impl Plugin for PickingPlugin {
|
|||||||
// Eventually events will need to be dispatched here
|
// Eventually events will need to be dispatched here
|
||||||
PickSet::Last,
|
PickSet::Last,
|
||||||
)
|
)
|
||||||
|
.ambiguous_with(bevy_asset::handle_internal_asset_events)
|
||||||
.chain(),
|
.chain(),
|
||||||
)
|
)
|
||||||
.register_type::<pointer::PointerId>()
|
.register_type::<pointer::PointerId>()
|
||||||
@ -213,6 +249,7 @@ impl Plugin for PickingPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generates [`Pointer`](events::Pointer) events and handles event bubbling.
|
/// Generates [`Pointer`](events::Pointer) events and handles event bubbling.
|
||||||
|
#[derive(Default)]
|
||||||
pub struct InteractionPlugin;
|
pub struct InteractionPlugin;
|
||||||
|
|
||||||
impl Plugin for InteractionPlugin {
|
impl Plugin for InteractionPlugin {
|
||||||
@ -224,6 +261,12 @@ impl Plugin for InteractionPlugin {
|
|||||||
.init_resource::<focus::PreviousHoverMap>()
|
.init_resource::<focus::PreviousHoverMap>()
|
||||||
.init_resource::<DragMap>()
|
.init_resource::<DragMap>()
|
||||||
.add_event::<PointerCancel>()
|
.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(
|
.add_systems(
|
||||||
PreUpdate,
|
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_render = { path = "../bevy_render", version = "0.15.0-dev" }
|
||||||
bevy_sprite = { path = "../bevy_sprite", 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_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_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
|
||||||
bevy_window = { path = "../bevy_window", 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" }
|
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
|
||||||
@ -40,6 +41,7 @@ smallvec = "1.11"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
serialize = ["serde", "smallvec/serde", "bevy_math/serialize"]
|
serialize = ["serde", "smallvec/serde", "bevy_math/serialize"]
|
||||||
|
bevy_picking = ["dep:bevy_picking"]
|
||||||
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
@ -17,6 +17,9 @@ pub mod ui_material;
|
|||||||
pub mod update;
|
pub mod update;
|
||||||
pub mod widget;
|
pub mod widget;
|
||||||
|
|
||||||
|
#[cfg(feature = "bevy_picking")]
|
||||||
|
pub mod picking_backend;
|
||||||
|
|
||||||
use bevy_derive::{Deref, DerefMut};
|
use bevy_derive::{Deref, DerefMut};
|
||||||
use bevy_reflect::Reflect;
|
use bevy_reflect::Reflect;
|
||||||
#[cfg(feature = "bevy_text")]
|
#[cfg(feature = "bevy_text")]
|
||||||
@ -202,6 +205,9 @@ impl Plugin for UiPlugin {
|
|||||||
build_text_interop(app);
|
build_text_interop(app);
|
||||||
|
|
||||||
build_ui_render(app);
|
build_ui_render(app);
|
||||||
|
|
||||||
|
#[cfg(feature = "bevy_picking")]
|
||||||
|
app.add_plugins(picking_backend::UiPickingBackend);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(&self, app: &mut App) {
|
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)
|
- [Input](#input)
|
||||||
- [Math](#math)
|
- [Math](#math)
|
||||||
- [Movement](#movement)
|
- [Movement](#movement)
|
||||||
|
- [Picking](#picking)
|
||||||
- [Reflection](#reflection)
|
- [Reflection](#reflection)
|
||||||
- [Scene](#scene)
|
- [Scene](#scene)
|
||||||
- [Shaders](#shaders)
|
- [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
|
[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
|
## Reflection
|
||||||
|
|
||||||
Example | Description
|
Example | Description
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
f32::consts::PI,
|
f32::consts::PI,
|
||||||
|
ops::Drop,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, AtomicU32, Ordering},
|
atomic::{AtomicBool, AtomicU32, Ordering},
|
||||||
Arc,
|
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 New Issue
Block a user