Mod picking upstream 2 (#14686)
Ci fixed version of: #14541 Upstream the remainder of bevy_picking_core and all of bevy_picking_input. This work is intentionally nonfunctional and has minimal changes, but does compile. More work is necessary to replace bevy_eventlistener with propagating observers. This work is being coordinated as part of "bevy_mod_picking upstream" working group. Come say hi on discord! --------- Co-authored-by: Miles Silberling-Cook <nth.tensor@gmail.com> Co-authored-by: Aevyrie <aevyrie@gmail.com>
This commit is contained in:
parent
e490b919df
commit
3e10fd8534
@ -9,7 +9,10 @@ license = "MIT OR Apache-2.0"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy_app = { path = "../bevy_app", version = "0.15.0-dev" }
|
bevy_app = { path = "../bevy_app", version = "0.15.0-dev" }
|
||||||
|
bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" }
|
||||||
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" }
|
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" }
|
||||||
|
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" }
|
||||||
|
bevy_input = { path = "../bevy_input", version = "0.15.0-dev" }
|
||||||
bevy_math = { path = "../bevy_math", version = "0.15.0-dev" }
|
bevy_math = { path = "../bevy_math", version = "0.15.0-dev" }
|
||||||
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" }
|
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" }
|
||||||
bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
|
bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
|
||||||
|
667
crates/bevy_picking/src/events.rs
Normal file
667
crates/bevy_picking/src/events.rs
Normal file
@ -0,0 +1,667 @@
|
|||||||
|
//! Processes data from input and backends, producing interaction events.
|
||||||
|
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use bevy_derive::{Deref, DerefMut};
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use bevy_hierarchy::Parent;
|
||||||
|
use bevy_math::Vec2;
|
||||||
|
use bevy_reflect::prelude::*;
|
||||||
|
use bevy_utils::{tracing::debug, Duration, HashMap, Instant};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
backend::{prelude::PointerLocation, HitData},
|
||||||
|
focus::{HoverMap, PreviousHoverMap},
|
||||||
|
pointer::{
|
||||||
|
InputMove, InputPress, Location, PointerButton, PointerId, PointerMap, PressDirection,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Stores the common data needed for all `PointerEvent`s.
|
||||||
|
#[derive(Clone, PartialEq, Debug, Reflect, Component)]
|
||||||
|
pub struct Pointer<E: Debug + Clone + Reflect> {
|
||||||
|
/// The target of this event
|
||||||
|
pub target: Entity,
|
||||||
|
/// The pointer that triggered this event
|
||||||
|
pub pointer_id: PointerId,
|
||||||
|
/// The location of the pointer during this event
|
||||||
|
pub pointer_location: Location,
|
||||||
|
/// Additional event-specific data. [`Drop`] for example, has an additional field to describe
|
||||||
|
/// the `Entity` that is being dropped on the target.
|
||||||
|
pub event: E,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> Event for Pointer<E>
|
||||||
|
where
|
||||||
|
E: Debug + Clone + Reflect,
|
||||||
|
{
|
||||||
|
type Traversal = Parent;
|
||||||
|
const AUTO_PROPAGATE: bool = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Debug + Clone + Reflect> std::fmt::Display for Pointer<E> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
"{:?}, {:.1?}, {:?}, {:.1?}",
|
||||||
|
self.pointer_id, self.pointer_location.position, self.target, self.event
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Debug + Clone + Reflect> std::ops::Deref for Pointer<E> {
|
||||||
|
type Target = E;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Debug + Clone + Reflect> Pointer<E> {
|
||||||
|
/// Construct a new `PointerEvent`.
|
||||||
|
pub fn new(id: PointerId, location: Location, target: Entity, event: E) -> Self {
|
||||||
|
Self {
|
||||||
|
pointer_id: id,
|
||||||
|
pointer_location: location,
|
||||||
|
target,
|
||||||
|
event,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when a pointer is no longer available.
|
||||||
|
#[derive(Event, Clone, PartialEq, Debug, Reflect)]
|
||||||
|
pub struct PointerCancel {
|
||||||
|
/// ID of the pointer that was cancelled.
|
||||||
|
#[reflect(ignore)]
|
||||||
|
pub pointer_id: PointerId,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when a the pointer crosses into the bounds of the `target` entity.
|
||||||
|
#[derive(Clone, PartialEq, Debug, Reflect)]
|
||||||
|
pub struct Over {
|
||||||
|
/// Information about the picking intersection.
|
||||||
|
pub hit: HitData,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when a the pointer crosses out of the bounds of the `target` entity.
|
||||||
|
#[derive(Clone, PartialEq, Debug, Reflect)]
|
||||||
|
pub struct Out {
|
||||||
|
/// Information about the latest prior picking intersection.
|
||||||
|
pub hit: HitData,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when a pointer button is pressed over the `target` entity.
|
||||||
|
#[derive(Clone, PartialEq, Debug, Reflect)]
|
||||||
|
pub struct Down {
|
||||||
|
/// Pointer button pressed to trigger this event.
|
||||||
|
pub button: PointerButton,
|
||||||
|
/// Information about the picking intersection.
|
||||||
|
pub hit: HitData,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when a pointer button is released over the `target` entity.
|
||||||
|
#[derive(Clone, PartialEq, Debug, Reflect)]
|
||||||
|
pub struct Up {
|
||||||
|
/// Pointer button lifted to trigger this event.
|
||||||
|
pub button: PointerButton,
|
||||||
|
/// Information about the picking intersection.
|
||||||
|
pub hit: HitData,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when a pointer sends a pointer down event followed by a pointer up event, with the same
|
||||||
|
/// `target` entity for both events.
|
||||||
|
#[derive(Clone, PartialEq, Debug, Reflect)]
|
||||||
|
pub struct Click {
|
||||||
|
/// Pointer button pressed and lifted to trigger this event.
|
||||||
|
pub button: PointerButton,
|
||||||
|
/// Information about the picking intersection.
|
||||||
|
pub hit: HitData,
|
||||||
|
/// Duration between the pointer pressed and lifted for this click
|
||||||
|
pub duration: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires while a pointer is moving over the `target` entity.
|
||||||
|
#[derive(Clone, PartialEq, Debug, Reflect)]
|
||||||
|
pub struct Move {
|
||||||
|
/// Information about the picking intersection.
|
||||||
|
pub hit: HitData,
|
||||||
|
/// The change in position since the last move event.
|
||||||
|
pub delta: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when the `target` entity receives a pointer down event followed by a pointer move event.
|
||||||
|
#[derive(Clone, PartialEq, Debug, Reflect)]
|
||||||
|
pub struct DragStart {
|
||||||
|
/// Pointer button pressed and moved to trigger this event.
|
||||||
|
pub button: PointerButton,
|
||||||
|
/// Information about the picking intersection.
|
||||||
|
pub hit: HitData,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires while the `target` entity is being dragged.
|
||||||
|
#[derive(Clone, PartialEq, Debug, Reflect)]
|
||||||
|
pub struct Drag {
|
||||||
|
/// Pointer button pressed and moved to trigger this event.
|
||||||
|
pub button: PointerButton,
|
||||||
|
/// The total distance vector of a drag, measured from drag start to the current position.
|
||||||
|
pub distance: Vec2,
|
||||||
|
/// The change in position since the last drag event.
|
||||||
|
pub delta: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when a pointer is dragging the `target` entity and a pointer up event is received.
|
||||||
|
#[derive(Clone, PartialEq, Debug, Reflect)]
|
||||||
|
pub struct DragEnd {
|
||||||
|
/// Pointer button pressed, moved, and lifted to trigger this event.
|
||||||
|
pub button: PointerButton,
|
||||||
|
/// The vector of drag movement measured from start to final pointer position.
|
||||||
|
pub distance: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when a pointer dragging the `dragged` entity enters the `target` entity.
|
||||||
|
#[derive(Clone, PartialEq, Debug, Reflect)]
|
||||||
|
pub struct DragEnter {
|
||||||
|
/// Pointer button pressed to enter drag.
|
||||||
|
pub button: PointerButton,
|
||||||
|
/// The entity that was being dragged when the pointer entered the `target` entity.
|
||||||
|
pub dragged: Entity,
|
||||||
|
/// Information about the picking intersection.
|
||||||
|
pub hit: HitData,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires while the `dragged` entity is being dragged over the `target` entity.
|
||||||
|
#[derive(Clone, PartialEq, Debug, Reflect)]
|
||||||
|
pub struct DragOver {
|
||||||
|
/// Pointer button pressed while dragging over.
|
||||||
|
pub button: PointerButton,
|
||||||
|
/// The entity that was being dragged when the pointer was over the `target` entity.
|
||||||
|
pub dragged: Entity,
|
||||||
|
/// Information about the picking intersection.
|
||||||
|
pub hit: HitData,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when a pointer dragging the `dragged` entity leaves the `target` entity.
|
||||||
|
#[derive(Clone, PartialEq, Debug, Reflect)]
|
||||||
|
pub struct DragLeave {
|
||||||
|
/// Pointer button pressed while leaving drag.
|
||||||
|
pub button: PointerButton,
|
||||||
|
/// The entity that was being dragged when the pointer left the `target` entity.
|
||||||
|
pub dragged: Entity,
|
||||||
|
/// Information about the latest prior picking intersection.
|
||||||
|
pub hit: HitData,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when a pointer drops the `dropped` entity onto the `target` entity.
|
||||||
|
#[derive(Clone, PartialEq, Debug, Reflect)]
|
||||||
|
pub struct Drop {
|
||||||
|
/// Pointer button lifted to drop.
|
||||||
|
pub button: PointerButton,
|
||||||
|
/// The entity that was dropped onto the `target` entity.
|
||||||
|
pub dropped: Entity,
|
||||||
|
/// Information about the picking intersection.
|
||||||
|
pub hit: HitData,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates pointer events from input and focus data
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn pointer_events(
|
||||||
|
// Input
|
||||||
|
mut input_presses: EventReader<InputPress>,
|
||||||
|
mut input_moves: EventReader<InputMove>,
|
||||||
|
pointer_map: Res<PointerMap>,
|
||||||
|
pointers: Query<&PointerLocation>,
|
||||||
|
hover_map: Res<HoverMap>,
|
||||||
|
previous_hover_map: Res<PreviousHoverMap>,
|
||||||
|
// Output
|
||||||
|
mut pointer_move: EventWriter<Pointer<Move>>,
|
||||||
|
mut pointer_over: EventWriter<Pointer<Over>>,
|
||||||
|
mut pointer_out: EventWriter<Pointer<Out>>,
|
||||||
|
mut pointer_up: EventWriter<Pointer<Up>>,
|
||||||
|
mut pointer_down: EventWriter<Pointer<Down>>,
|
||||||
|
) {
|
||||||
|
let pointer_location = |pointer_id: PointerId| {
|
||||||
|
pointer_map
|
||||||
|
.get_entity(pointer_id)
|
||||||
|
.and_then(|entity| pointers.get(entity).ok())
|
||||||
|
.and_then(|pointer| pointer.location.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
for InputMove {
|
||||||
|
pointer_id,
|
||||||
|
location,
|
||||||
|
delta,
|
||||||
|
} in input_moves.read().cloned()
|
||||||
|
{
|
||||||
|
for (hovered_entity, hit) in hover_map
|
||||||
|
.get(&pointer_id)
|
||||||
|
.iter()
|
||||||
|
.flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned())))
|
||||||
|
{
|
||||||
|
pointer_move.send(Pointer::new(
|
||||||
|
pointer_id,
|
||||||
|
location.clone(),
|
||||||
|
hovered_entity,
|
||||||
|
Move { hit, delta },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for press_event in input_presses.read() {
|
||||||
|
let button = press_event.button;
|
||||||
|
// We use the previous hover map because we want to consider pointers that just left the
|
||||||
|
// entity. Without this, touch inputs would never send up events because they are lifted up
|
||||||
|
// and leave the bounds of the entity at the same time.
|
||||||
|
for (hovered_entity, hit) in previous_hover_map
|
||||||
|
.get(&press_event.pointer_id)
|
||||||
|
.iter()
|
||||||
|
.flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.clone())))
|
||||||
|
{
|
||||||
|
if let PressDirection::Up = press_event.direction {
|
||||||
|
let Some(location) = pointer_location(press_event.pointer_id) else {
|
||||||
|
debug!(
|
||||||
|
"Unable to get location for pointer {:?} during event {:?}",
|
||||||
|
press_event.pointer_id, press_event
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
pointer_up.send(Pointer::new(
|
||||||
|
press_event.pointer_id,
|
||||||
|
location,
|
||||||
|
hovered_entity,
|
||||||
|
Up { button, hit },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (hovered_entity, hit) in hover_map
|
||||||
|
.get(&press_event.pointer_id)
|
||||||
|
.iter()
|
||||||
|
.flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.clone())))
|
||||||
|
{
|
||||||
|
if let PressDirection::Down = press_event.direction {
|
||||||
|
let Some(location) = pointer_location(press_event.pointer_id) else {
|
||||||
|
debug!(
|
||||||
|
"Unable to get location for pointer {:?} during event {:?}",
|
||||||
|
press_event.pointer_id, press_event
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
pointer_down.send(Pointer::new(
|
||||||
|
press_event.pointer_id,
|
||||||
|
location,
|
||||||
|
hovered_entity,
|
||||||
|
Down { button, hit },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the entity is hovered...
|
||||||
|
for (pointer_id, hovered_entity, hit) in hover_map
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(id, hashmap)| hashmap.iter().map(|data| (*id, *data.0, data.1.clone())))
|
||||||
|
{
|
||||||
|
// ...but was not hovered last frame...
|
||||||
|
if !previous_hover_map
|
||||||
|
.get(&pointer_id)
|
||||||
|
.iter()
|
||||||
|
.any(|e| e.contains_key(&hovered_entity))
|
||||||
|
{
|
||||||
|
let Some(location) = pointer_location(pointer_id) else {
|
||||||
|
debug!(
|
||||||
|
"Unable to get location for pointer {:?} during pointer over",
|
||||||
|
pointer_id
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
pointer_over.send(Pointer::new(
|
||||||
|
pointer_id,
|
||||||
|
location,
|
||||||
|
hovered_entity,
|
||||||
|
Over { hit },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the entity was hovered by a specific pointer last frame...
|
||||||
|
for (pointer_id, hovered_entity, hit) in previous_hover_map
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(id, hashmap)| hashmap.iter().map(|data| (*id, *data.0, data.1.clone())))
|
||||||
|
{
|
||||||
|
// ...but is now not being hovered by that same pointer...
|
||||||
|
if !hover_map
|
||||||
|
.get(&pointer_id)
|
||||||
|
.iter()
|
||||||
|
.any(|e| e.contains_key(&hovered_entity))
|
||||||
|
{
|
||||||
|
let Some(location) = pointer_location(pointer_id) else {
|
||||||
|
debug!(
|
||||||
|
"Unable to get location for pointer {:?} during pointer out",
|
||||||
|
pointer_id
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
pointer_out.send(Pointer::new(
|
||||||
|
pointer_id,
|
||||||
|
location,
|
||||||
|
hovered_entity,
|
||||||
|
Out { hit },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maps pointers to the entities they are dragging.
|
||||||
|
#[derive(Debug, Deref, DerefMut, Default, Resource)]
|
||||||
|
pub struct DragMap(pub HashMap<(PointerId, PointerButton), HashMap<Entity, DragEntry>>);
|
||||||
|
|
||||||
|
/// An entry in the [`DragMap`].
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DragEntry {
|
||||||
|
/// The position of the pointer at drag start.
|
||||||
|
pub start_pos: Vec2,
|
||||||
|
/// The latest position of the pointer during this drag, used to compute deltas.
|
||||||
|
pub latest_pos: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uses pointer events to determine when click and drag events occur.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn send_click_and_drag_events(
|
||||||
|
// Input
|
||||||
|
mut pointer_down: EventReader<Pointer<Down>>,
|
||||||
|
mut pointer_up: EventReader<Pointer<Up>>,
|
||||||
|
mut input_move: EventReader<InputMove>,
|
||||||
|
mut input_presses: EventReader<InputPress>,
|
||||||
|
pointer_map: Res<PointerMap>,
|
||||||
|
pointers: Query<&PointerLocation>,
|
||||||
|
// Locals
|
||||||
|
mut down_map: Local<
|
||||||
|
HashMap<(PointerId, PointerButton), HashMap<Entity, (Pointer<Down>, Instant)>>,
|
||||||
|
>,
|
||||||
|
// Output
|
||||||
|
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
|
||||||
|
.get_entity(pointer_id)
|
||||||
|
.and_then(|entity| pointers.get(entity).ok())
|
||||||
|
.and_then(|pointer| pointer.location.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
// Triggers during movement even if not over an entity
|
||||||
|
for InputMove {
|
||||||
|
pointer_id,
|
||||||
|
location,
|
||||||
|
delta: _,
|
||||||
|
} in input_move.read().cloned()
|
||||||
|
{
|
||||||
|
for button in PointerButton::iter() {
|
||||||
|
let Some(down_list) = down_map.get(&(pointer_id, button)) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let drag_list = drag_map.entry((pointer_id, button)).or_default();
|
||||||
|
|
||||||
|
for (down, _instant) in down_list.values() {
|
||||||
|
if drag_list.contains_key(&down.target) {
|
||||||
|
continue; // this entity is already logged as being dragged
|
||||||
|
}
|
||||||
|
drag_list.insert(
|
||||||
|
down.target,
|
||||||
|
DragEntry {
|
||||||
|
start_pos: down.pointer_location.position,
|
||||||
|
latest_pos: down.pointer_location.position,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pointer_drag_start.send(Pointer::new(
|
||||||
|
pointer_id,
|
||||||
|
down.pointer_location.clone(),
|
||||||
|
down.target,
|
||||||
|
DragStart {
|
||||||
|
button,
|
||||||
|
hit: down.hit.clone(),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (dragged_entity, drag) in drag_list.iter_mut() {
|
||||||
|
let drag_event = Drag {
|
||||||
|
button,
|
||||||
|
distance: location.position - drag.start_pos,
|
||||||
|
delta: location.position - drag.latest_pos,
|
||||||
|
};
|
||||||
|
drag.latest_pos = location.position;
|
||||||
|
pointer_drag.send(Pointer::new(
|
||||||
|
pointer_id,
|
||||||
|
location.clone(),
|
||||||
|
*dragged_entity,
|
||||||
|
drag_event,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Triggers when button is released over an entity
|
||||||
|
let now = Instant::now();
|
||||||
|
for Pointer {
|
||||||
|
pointer_id,
|
||||||
|
pointer_location,
|
||||||
|
target,
|
||||||
|
event: Up { button, hit },
|
||||||
|
} in pointer_up.read().cloned()
|
||||||
|
{
|
||||||
|
// Can't have a click without the button being pressed down first
|
||||||
|
if let Some((_down, down_instant)) = down_map
|
||||||
|
.get(&(pointer_id, button))
|
||||||
|
.and_then(|down| down.get(&target))
|
||||||
|
{
|
||||||
|
let duration = now - *down_instant;
|
||||||
|
pointer_click.send(Pointer::new(
|
||||||
|
pointer_id,
|
||||||
|
pointer_location,
|
||||||
|
target,
|
||||||
|
Click {
|
||||||
|
button,
|
||||||
|
hit,
|
||||||
|
duration,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Triggers when button is pressed over an entity
|
||||||
|
for event in pointer_down.read() {
|
||||||
|
let button = event.button;
|
||||||
|
let down_button_entity_map = down_map.entry((event.pointer_id, button)).or_default();
|
||||||
|
down_button_entity_map.insert(event.target, (event.clone(), now));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Triggered for all button presses
|
||||||
|
for press in input_presses.read() {
|
||||||
|
if press.direction != PressDirection::Up {
|
||||||
|
continue; // We are only interested in button releases
|
||||||
|
}
|
||||||
|
down_map.insert((press.pointer_id, press.button), HashMap::new());
|
||||||
|
let Some(drag_list) = drag_map.insert((press.pointer_id, press.button), HashMap::new())
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(location) = pointer_location(press.pointer_id) else {
|
||||||
|
debug!(
|
||||||
|
"Unable to get location for pointer {:?} during event {:?}",
|
||||||
|
press.pointer_id, press
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (drag_target, drag) in drag_list {
|
||||||
|
let drag_end = DragEnd {
|
||||||
|
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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uses pointer events to determine when drag-over events occur
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn send_drag_over_events(
|
||||||
|
// Input
|
||||||
|
drag_map: Res<DragMap>,
|
||||||
|
mut pointer_over: EventReader<Pointer<Over>>,
|
||||||
|
mut pointer_move: EventReader<Pointer<Move>>,
|
||||||
|
mut pointer_out: EventReader<Pointer<Out>>,
|
||||||
|
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 {
|
||||||
|
pointer_id,
|
||||||
|
pointer_location,
|
||||||
|
target,
|
||||||
|
event: Over { hit },
|
||||||
|
} in pointer_over.read().cloned()
|
||||||
|
{
|
||||||
|
for button in PointerButton::iter() {
|
||||||
|
for drag_target in drag_map
|
||||||
|
.get(&(pointer_id, button))
|
||||||
|
.iter()
|
||||||
|
.flat_map(|drag_list| drag_list.keys())
|
||||||
|
.filter(
|
||||||
|
|&&drag_target| target != drag_target, /* can't drag over itself */
|
||||||
|
)
|
||||||
|
{
|
||||||
|
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(
|
||||||
|
pointer_id,
|
||||||
|
pointer_location.clone(),
|
||||||
|
target,
|
||||||
|
event,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire PointerDragOver events.
|
||||||
|
for Pointer {
|
||||||
|
pointer_id,
|
||||||
|
pointer_location,
|
||||||
|
target,
|
||||||
|
event: Move { hit, delta: _ },
|
||||||
|
} in pointer_move.read().cloned()
|
||||||
|
{
|
||||||
|
for button in PointerButton::iter() {
|
||||||
|
for drag_target in drag_map
|
||||||
|
.get(&(pointer_id, button))
|
||||||
|
.iter()
|
||||||
|
.flat_map(|drag_list| drag_list.keys())
|
||||||
|
.filter(
|
||||||
|
|&&drag_target| target != drag_target, /* can't drag over itself */
|
||||||
|
)
|
||||||
|
{
|
||||||
|
pointer_drag_over.send(Pointer::new(
|
||||||
|
pointer_id,
|
||||||
|
pointer_location.clone(),
|
||||||
|
target,
|
||||||
|
DragOver {
|
||||||
|
button,
|
||||||
|
dragged: *drag_target,
|
||||||
|
hit: hit.clone(),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire PointerDragLeave and PointerDrop events when the pointer stops dragging.
|
||||||
|
for Pointer {
|
||||||
|
pointer_id,
|
||||||
|
pointer_location,
|
||||||
|
target,
|
||||||
|
event: DragEnd {
|
||||||
|
button,
|
||||||
|
distance: _,
|
||||||
|
},
|
||||||
|
} in pointer_drag_end.read().cloned()
|
||||||
|
{
|
||||||
|
let Some(drag_over_set) = drag_over_map.get_mut(&(pointer_id, button)) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for (dragged_over, hit) in drag_over_set.drain() {
|
||||||
|
pointer_drag_leave.send(Pointer::new(
|
||||||
|
pointer_id,
|
||||||
|
pointer_location.clone(),
|
||||||
|
dragged_over,
|
||||||
|
DragLeave {
|
||||||
|
button,
|
||||||
|
dragged: target,
|
||||||
|
hit: hit.clone(),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
pointer_drop.send(Pointer::new(
|
||||||
|
pointer_id,
|
||||||
|
pointer_location.clone(),
|
||||||
|
dragged_over,
|
||||||
|
Drop {
|
||||||
|
button,
|
||||||
|
dropped: target,
|
||||||
|
hit: hit.clone(),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire PointerDragLeave events when the pointer goes out of the target.
|
||||||
|
for Pointer {
|
||||||
|
pointer_id,
|
||||||
|
pointer_location,
|
||||||
|
target,
|
||||||
|
event: Out { hit },
|
||||||
|
} in pointer_out.read().cloned()
|
||||||
|
{
|
||||||
|
for button in PointerButton::iter() {
|
||||||
|
let Some(dragged_over) = drag_over_map.get_mut(&(pointer_id, button)) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if dragged_over.remove(&target).is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(drag_list) = drag_map.get(&(pointer_id, button)) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for drag_target in drag_list.keys() {
|
||||||
|
pointer_drag_leave.send(Pointer::new(
|
||||||
|
pointer_id,
|
||||||
|
pointer_location.clone(),
|
||||||
|
target,
|
||||||
|
DragLeave {
|
||||||
|
button,
|
||||||
|
dragged: *drag_target,
|
||||||
|
hit: hit.clone(),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
266
crates/bevy_picking/src/focus.rs
Normal file
266
crates/bevy_picking/src/focus.rs
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
//! Determines which entities are being hovered by which pointers.
|
||||||
|
|
||||||
|
use std::{collections::BTreeMap, fmt::Debug};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
backend::{self, HitData},
|
||||||
|
events::PointerCancel,
|
||||||
|
pointer::{PointerId, PointerInteraction, PointerPress},
|
||||||
|
Pickable,
|
||||||
|
};
|
||||||
|
|
||||||
|
use bevy_derive::{Deref, DerefMut};
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use bevy_math::FloatOrd;
|
||||||
|
use bevy_reflect::prelude::*;
|
||||||
|
use bevy_utils::HashMap;
|
||||||
|
|
||||||
|
type DepthSortedHits = Vec<(Entity, HitData)>;
|
||||||
|
|
||||||
|
/// Events returned from backends can be grouped with an order field. This allows picking to work
|
||||||
|
/// with multiple layers of rendered output to the same render target.
|
||||||
|
type PickLayer = FloatOrd;
|
||||||
|
|
||||||
|
/// Maps [`PickLayer`]s to the map of entities within that pick layer, sorted by depth.
|
||||||
|
type LayerMap = BTreeMap<PickLayer, DepthSortedHits>;
|
||||||
|
|
||||||
|
/// Maps Pointers to a [`LayerMap`]. Note this is much more complex than the [`HoverMap`] because
|
||||||
|
/// this data structure is used to sort entities by layer then depth for every pointer.
|
||||||
|
type OverMap = HashMap<PointerId, LayerMap>;
|
||||||
|
|
||||||
|
/// The source of truth for all hover state. This is used to determine what events to send, and what
|
||||||
|
/// state components should be in.
|
||||||
|
///
|
||||||
|
/// Maps pointers to the entities they are hovering over.
|
||||||
|
///
|
||||||
|
/// "Hovering" refers to the *hover* state, which is not the same as whether or not a picking
|
||||||
|
/// backend is reporting hits between a pointer and an entity. A pointer is "hovering" an entity
|
||||||
|
/// only if the pointer is hitting the entity (as reported by a picking backend) *and* no entities
|
||||||
|
/// between it and the pointer block interactions.
|
||||||
|
///
|
||||||
|
/// For example, if a pointer is hitting a UI button and a 3d mesh, but the button is in front of
|
||||||
|
/// the mesh, and [`Pickable::should_block_lower`], the UI button will be hovered, but the mesh will
|
||||||
|
/// not.
|
||||||
|
///
|
||||||
|
/// # Advanced Users
|
||||||
|
///
|
||||||
|
/// If you want to completely replace the provided picking events or state produced by this plugin,
|
||||||
|
/// you can use this resource to do that. All of the event systems for picking are built *on top of*
|
||||||
|
/// this authoritative hover state, and you can do the same. You can also use the
|
||||||
|
/// [`PreviousHoverMap`] as a robust way of determining changes in hover state from the previous
|
||||||
|
/// update.
|
||||||
|
#[derive(Debug, Deref, DerefMut, Default, Resource)]
|
||||||
|
pub struct HoverMap(pub HashMap<PointerId, HashMap<Entity, HitData>>);
|
||||||
|
|
||||||
|
/// The previous state of the hover map, used to track changes to hover state.
|
||||||
|
#[derive(Debug, Deref, DerefMut, Default, Resource)]
|
||||||
|
pub struct PreviousHoverMap(pub HashMap<PointerId, HashMap<Entity, HitData>>);
|
||||||
|
|
||||||
|
/// Coalesces all data from inputs and backends to generate a map of the currently hovered entities.
|
||||||
|
/// This is the final focusing step to determine which entity the pointer is hovering over.
|
||||||
|
pub fn update_focus(
|
||||||
|
// Inputs
|
||||||
|
pickable: Query<&Pickable>,
|
||||||
|
pointers: Query<&PointerId>,
|
||||||
|
mut under_pointer: EventReader<backend::PointerHits>,
|
||||||
|
mut cancellations: EventReader<PointerCancel>,
|
||||||
|
// Local
|
||||||
|
mut over_map: Local<OverMap>,
|
||||||
|
// Output
|
||||||
|
mut hover_map: ResMut<HoverMap>,
|
||||||
|
mut previous_hover_map: ResMut<PreviousHoverMap>,
|
||||||
|
) {
|
||||||
|
reset_maps(
|
||||||
|
&mut hover_map,
|
||||||
|
&mut previous_hover_map,
|
||||||
|
&mut over_map,
|
||||||
|
&pointers,
|
||||||
|
);
|
||||||
|
build_over_map(&mut under_pointer, &mut over_map, &mut cancellations);
|
||||||
|
build_hover_map(&pointers, pickable, &over_map, &mut hover_map);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear non-empty local maps, reusing allocated memory.
|
||||||
|
fn reset_maps(
|
||||||
|
hover_map: &mut HoverMap,
|
||||||
|
previous_hover_map: &mut PreviousHoverMap,
|
||||||
|
over_map: &mut OverMap,
|
||||||
|
pointers: &Query<&PointerId>,
|
||||||
|
) {
|
||||||
|
// Swap the previous and current hover maps. This results in the previous values being stored in
|
||||||
|
// `PreviousHoverMap`. Swapping is okay because we clear the `HoverMap` which now holds stale
|
||||||
|
// data. This process is done without any allocations.
|
||||||
|
core::mem::swap(&mut previous_hover_map.0, &mut hover_map.0);
|
||||||
|
|
||||||
|
for entity_set in hover_map.values_mut() {
|
||||||
|
entity_set.clear();
|
||||||
|
}
|
||||||
|
for layer_map in over_map.values_mut() {
|
||||||
|
layer_map.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear pointers from the maps if they have been removed.
|
||||||
|
let active_pointers: Vec<PointerId> = pointers.iter().copied().collect();
|
||||||
|
hover_map.retain(|pointer, _| active_pointers.contains(pointer));
|
||||||
|
over_map.retain(|pointer, _| active_pointers.contains(pointer));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build an ordered map of entities that are under each pointer
|
||||||
|
fn build_over_map(
|
||||||
|
backend_events: &mut EventReader<backend::PointerHits>,
|
||||||
|
pointer_over_map: &mut Local<OverMap>,
|
||||||
|
pointer_cancel: &mut EventReader<PointerCancel>,
|
||||||
|
) {
|
||||||
|
let cancelled_pointers: Vec<PointerId> = pointer_cancel.read().map(|p| p.pointer_id).collect();
|
||||||
|
|
||||||
|
for entities_under_pointer in backend_events
|
||||||
|
.read()
|
||||||
|
.filter(|e| !cancelled_pointers.contains(&e.pointer))
|
||||||
|
{
|
||||||
|
let pointer = entities_under_pointer.pointer;
|
||||||
|
let layer_map = pointer_over_map
|
||||||
|
.entry(pointer)
|
||||||
|
.or_insert_with(BTreeMap::new);
|
||||||
|
for (entity, pick_data) in entities_under_pointer.picks.iter() {
|
||||||
|
let layer = entities_under_pointer.order;
|
||||||
|
let hits = layer_map.entry(FloatOrd(layer)).or_insert_with(Vec::new);
|
||||||
|
hits.push((*entity, pick_data.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for layers in pointer_over_map.values_mut() {
|
||||||
|
for hits in layers.values_mut() {
|
||||||
|
hits.sort_by_key(|(_, hit)| FloatOrd(hit.depth));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build an unsorted set of hovered entities, accounting for depth, layer, and [`Pickable`]. Note
|
||||||
|
/// that unlike the pointer map, this uses [`Pickable`] to determine if lower entities receive hover
|
||||||
|
/// focus. Often, only a single entity per pointer will be hovered.
|
||||||
|
fn build_hover_map(
|
||||||
|
pointers: &Query<&PointerId>,
|
||||||
|
pickable: Query<&Pickable>,
|
||||||
|
over_map: &Local<OverMap>,
|
||||||
|
// Output
|
||||||
|
hover_map: &mut HoverMap,
|
||||||
|
) {
|
||||||
|
for pointer_id in pointers.iter() {
|
||||||
|
let pointer_entity_set = hover_map.entry(*pointer_id).or_insert_with(HashMap::new);
|
||||||
|
if let Some(layer_map) = over_map.get(pointer_id) {
|
||||||
|
// Note we reverse here to start from the highest layer first.
|
||||||
|
for (entity, pick_data) in layer_map.values().rev().flatten() {
|
||||||
|
if let Ok(pickable) = pickable.get(*entity) {
|
||||||
|
if pickable.is_hoverable {
|
||||||
|
pointer_entity_set.insert(*entity, pick_data.clone());
|
||||||
|
}
|
||||||
|
if pickable.should_block_lower {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pointer_entity_set.insert(*entity, pick_data.clone()); // Emit events by default
|
||||||
|
break; // Entities block by default so we break out of the loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A component that aggregates picking interaction state of this entity across all pointers.
|
||||||
|
///
|
||||||
|
/// Unlike bevy's `Interaction` component, this is an aggregate of the state of all pointers
|
||||||
|
/// interacting with this entity. Aggregation is done by taking the interaction with the highest
|
||||||
|
/// precedence.
|
||||||
|
///
|
||||||
|
/// For example, if we have an entity that is being hovered by one pointer, and pressed by another,
|
||||||
|
/// the entity will be considered pressed. If that entity is instead being hovered by both pointers,
|
||||||
|
/// it will be considered hovered.
|
||||||
|
#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)]
|
||||||
|
#[reflect(Component, Default)]
|
||||||
|
pub enum PickingInteraction {
|
||||||
|
/// The entity is being pressed down by a pointer.
|
||||||
|
Pressed = 2,
|
||||||
|
/// The entity is being hovered by a pointer.
|
||||||
|
Hovered = 1,
|
||||||
|
/// No pointers are interacting with this entity.
|
||||||
|
#[default]
|
||||||
|
None = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uses pointer events to update [`PointerInteraction`] and [`PickingInteraction`] components.
|
||||||
|
pub fn update_interactions(
|
||||||
|
// Input
|
||||||
|
hover_map: Res<HoverMap>,
|
||||||
|
previous_hover_map: Res<PreviousHoverMap>,
|
||||||
|
// Outputs
|
||||||
|
mut commands: Commands,
|
||||||
|
mut pointers: Query<(&PointerId, &PointerPress, &mut PointerInteraction)>,
|
||||||
|
mut interact: Query<&mut PickingInteraction>,
|
||||||
|
) {
|
||||||
|
// Clear all previous hover data from pointers and entities
|
||||||
|
for (pointer, _, mut pointer_interaction) in &mut pointers {
|
||||||
|
pointer_interaction.sorted_entities.clear();
|
||||||
|
if let Some(previously_hovered_entities) = previous_hover_map.get(pointer) {
|
||||||
|
for entity in previously_hovered_entities.keys() {
|
||||||
|
if let Ok(mut interaction) = interact.get_mut(*entity) {
|
||||||
|
*interaction = PickingInteraction::None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a map to hold the aggregated interaction for each entity. This is needed because we
|
||||||
|
// need to be able to insert the interaction component on entities if they do not exist. To do
|
||||||
|
// so we need to know the final aggregated interaction state to avoid the scenario where we set
|
||||||
|
// an entity to `Pressed`, then overwrite that with a lower precedent like `Hovered`.
|
||||||
|
let mut new_interaction_state = HashMap::<Entity, PickingInteraction>::new();
|
||||||
|
for (pointer, pointer_press, mut pointer_interaction) in &mut pointers {
|
||||||
|
if let Some(pointers_hovered_entities) = hover_map.get(pointer) {
|
||||||
|
// Insert a sorted list of hit entities into the pointer's interaction component.
|
||||||
|
let mut sorted_entities: Vec<_> = pointers_hovered_entities.clone().drain().collect();
|
||||||
|
sorted_entities.sort_by_key(|(_entity, hit)| FloatOrd(hit.depth));
|
||||||
|
pointer_interaction.sorted_entities = sorted_entities;
|
||||||
|
|
||||||
|
for hovered_entity in pointers_hovered_entities.iter().map(|(entity, _)| entity) {
|
||||||
|
merge_interaction_states(pointer_press, hovered_entity, &mut new_interaction_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the aggregated entity states and update or insert the component if missing.
|
||||||
|
for (hovered_entity, new_interaction) in new_interaction_state.drain() {
|
||||||
|
if let Ok(mut interaction) = interact.get_mut(hovered_entity) {
|
||||||
|
*interaction = new_interaction;
|
||||||
|
} else if let Some(mut entity_commands) = commands.get_entity(hovered_entity) {
|
||||||
|
entity_commands.try_insert(new_interaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge the interaction state of this entity into the aggregated map.
|
||||||
|
fn merge_interaction_states(
|
||||||
|
pointer_press: &PointerPress,
|
||||||
|
hovered_entity: &Entity,
|
||||||
|
new_interaction_state: &mut HashMap<Entity, PickingInteraction>,
|
||||||
|
) {
|
||||||
|
let new_interaction = match pointer_press.is_any_pressed() {
|
||||||
|
true => PickingInteraction::Pressed,
|
||||||
|
false => PickingInteraction::Hovered,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(old_interaction) = new_interaction_state.get_mut(hovered_entity) {
|
||||||
|
// Only update if the new value has a higher precedence than the old value.
|
||||||
|
if *old_interaction != new_interaction
|
||||||
|
&& matches!(
|
||||||
|
(*old_interaction, new_interaction),
|
||||||
|
(PickingInteraction::Hovered, PickingInteraction::Pressed)
|
||||||
|
| (PickingInteraction::None, PickingInteraction::Pressed)
|
||||||
|
| (PickingInteraction::None, PickingInteraction::Hovered)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
*old_interaction = new_interaction;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
new_interaction_state.insert(*hovered_entity, new_interaction);
|
||||||
|
}
|
||||||
|
}
|
86
crates/bevy_picking/src/input/mod.rs
Normal file
86
crates/bevy_picking/src/input/mod.rs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
//! `bevy_picking::input` is a thin layer that provides unsurprising default inputs to `bevy_picking`.
|
||||||
|
//! The included systems are responsible for sending mouse and touch inputs to their
|
||||||
|
//! respective `Pointer`s.
|
||||||
|
//!
|
||||||
|
//! Because this resides in its own crate, it's easy to omit it, and provide your own inputs as
|
||||||
|
//! needed. Because `Pointer`s aren't coupled to the underlying input hardware, you can easily mock
|
||||||
|
//! inputs, and allow users full accessibility to map whatever inputs they need to pointer input.
|
||||||
|
//!
|
||||||
|
//! If, for example, you wanted to add support for VR input, all you need to do is spawn a pointer
|
||||||
|
//! entity with a custom [`PointerId`](crate::pointer::PointerId), and write a system
|
||||||
|
//! that updates its position.
|
||||||
|
//!
|
||||||
|
//! TODO: Update docs
|
||||||
|
|
||||||
|
use bevy_app::prelude::*;
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use bevy_reflect::prelude::*;
|
||||||
|
|
||||||
|
use crate::PickSet;
|
||||||
|
|
||||||
|
pub mod mouse;
|
||||||
|
pub mod touch;
|
||||||
|
|
||||||
|
/// Common imports for `bevy_picking_input`.
|
||||||
|
pub mod prelude {
|
||||||
|
pub use crate::input::InputPlugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds mouse and touch inputs for picking pointers to your app. This is a default input plugin,
|
||||||
|
/// that you can replace with your own plugin as needed.
|
||||||
|
///
|
||||||
|
/// [`crate::PickingPluginsSettings::is_input_enabled`] can be used to toggle whether
|
||||||
|
/// the core picking plugin processes the inputs sent by this, or other input plugins, in one place.
|
||||||
|
#[derive(Copy, Clone, Resource, Debug, Reflect)]
|
||||||
|
#[reflect(Resource, Default)]
|
||||||
|
pub struct InputPlugin {
|
||||||
|
/// Should touch inputs be updated?
|
||||||
|
pub is_touch_enabled: bool,
|
||||||
|
/// Should mouse inputs be updated?
|
||||||
|
pub is_mouse_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputPlugin {
|
||||||
|
fn is_mouse_enabled(state: Res<Self>) -> bool {
|
||||||
|
state.is_mouse_enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_touch_enabled(state: Res<Self>) -> bool {
|
||||||
|
state.is_touch_enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InputPlugin {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
is_touch_enabled: true,
|
||||||
|
is_mouse_enabled: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for InputPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.insert_resource(*self)
|
||||||
|
.add_systems(Startup, mouse::spawn_mouse_pointer)
|
||||||
|
.add_systems(
|
||||||
|
First,
|
||||||
|
(
|
||||||
|
mouse::mouse_pick_events.run_if(InputPlugin::is_mouse_enabled),
|
||||||
|
touch::touch_pick_events.run_if(InputPlugin::is_touch_enabled),
|
||||||
|
// IMPORTANT: the commands must be flushed after `touch_pick_events` is run
|
||||||
|
// because we need pointer spawning to happen immediately to prevent issues with
|
||||||
|
// missed events during drag and drop.
|
||||||
|
apply_deferred,
|
||||||
|
)
|
||||||
|
.chain()
|
||||||
|
.in_set(PickSet::Input),
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
Last,
|
||||||
|
touch::deactivate_touch_pointers.run_if(InputPlugin::is_touch_enabled),
|
||||||
|
)
|
||||||
|
.register_type::<Self>()
|
||||||
|
.register_type::<InputPlugin>();
|
||||||
|
}
|
||||||
|
}
|
67
crates/bevy_picking/src/input/mouse.rs
Normal file
67
crates/bevy_picking/src/input/mouse.rs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
//! Provides sensible defaults for mouse picking inputs.
|
||||||
|
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use bevy_input::{mouse::MouseButtonInput, prelude::*, ButtonState};
|
||||||
|
use bevy_math::Vec2;
|
||||||
|
use bevy_render::camera::RenderTarget;
|
||||||
|
use bevy_window::{CursorMoved, PrimaryWindow, Window, WindowRef};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
pointer::{InputMove, InputPress, Location, PointerButton, PointerId},
|
||||||
|
PointerBundle,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Spawns the default mouse pointer.
|
||||||
|
pub fn spawn_mouse_pointer(mut commands: Commands) {
|
||||||
|
commands.spawn((PointerBundle::new(PointerId::Mouse),));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends mouse pointer events to be processed by the core plugin
|
||||||
|
pub fn mouse_pick_events(
|
||||||
|
// Input
|
||||||
|
windows: Query<(Entity, &Window), With<PrimaryWindow>>,
|
||||||
|
mut cursor_moves: EventReader<CursorMoved>,
|
||||||
|
mut cursor_last: Local<Vec2>,
|
||||||
|
mut mouse_inputs: EventReader<MouseButtonInput>,
|
||||||
|
// Output
|
||||||
|
mut pointer_move: EventWriter<InputMove>,
|
||||||
|
mut pointer_presses: EventWriter<InputPress>,
|
||||||
|
) {
|
||||||
|
for event in cursor_moves.read() {
|
||||||
|
pointer_move.send(InputMove::new(
|
||||||
|
PointerId::Mouse,
|
||||||
|
Location {
|
||||||
|
target: RenderTarget::Window(WindowRef::Entity(event.window))
|
||||||
|
.normalize(Some(
|
||||||
|
match windows.get_single() {
|
||||||
|
Ok(w) => w,
|
||||||
|
Err(_) => continue,
|
||||||
|
}
|
||||||
|
.0,
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
position: event.position,
|
||||||
|
},
|
||||||
|
event.position - *cursor_last,
|
||||||
|
));
|
||||||
|
*cursor_last = event.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
for input in mouse_inputs.read() {
|
||||||
|
let button = match input.button {
|
||||||
|
MouseButton::Left => PointerButton::Primary,
|
||||||
|
MouseButton::Right => PointerButton::Secondary,
|
||||||
|
MouseButton::Middle => PointerButton::Middle,
|
||||||
|
MouseButton::Other(_) | MouseButton::Back | MouseButton::Forward => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
match input.state {
|
||||||
|
ButtonState::Pressed => {
|
||||||
|
pointer_presses.send(InputPress::new_down(PointerId::Mouse, button));
|
||||||
|
}
|
||||||
|
ButtonState::Released => {
|
||||||
|
pointer_presses.send(InputPress::new_up(PointerId::Mouse, button));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
105
crates/bevy_picking/src/input/touch.rs
Normal file
105
crates/bevy_picking/src/input/touch.rs
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
//! Provides sensible defaults for touch picking inputs.
|
||||||
|
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use bevy_hierarchy::DespawnRecursiveExt;
|
||||||
|
use bevy_input::touch::{TouchInput, TouchPhase};
|
||||||
|
use bevy_math::Vec2;
|
||||||
|
use bevy_render::camera::RenderTarget;
|
||||||
|
use bevy_utils::{tracing::debug, HashMap, HashSet};
|
||||||
|
use bevy_window::{PrimaryWindow, WindowRef};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
events::PointerCancel,
|
||||||
|
pointer::{InputMove, InputPress, Location, PointerButton, PointerId},
|
||||||
|
PointerBundle,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Sends touch pointer events to be consumed by the core plugin
|
||||||
|
///
|
||||||
|
/// IMPORTANT: the commands must be flushed after this system is run because we need spawning to
|
||||||
|
/// happen immediately to prevent issues with missed events needed for drag and drop.
|
||||||
|
pub fn touch_pick_events(
|
||||||
|
// Input
|
||||||
|
mut touches: EventReader<TouchInput>,
|
||||||
|
primary_window: Query<Entity, With<PrimaryWindow>>,
|
||||||
|
// Local
|
||||||
|
mut location_cache: Local<HashMap<u64, TouchInput>>,
|
||||||
|
// Output
|
||||||
|
mut commands: Commands,
|
||||||
|
mut input_moves: EventWriter<InputMove>,
|
||||||
|
mut input_presses: EventWriter<InputPress>,
|
||||||
|
mut cancel_events: EventWriter<PointerCancel>,
|
||||||
|
) {
|
||||||
|
for touch in touches.read() {
|
||||||
|
let pointer = PointerId::Touch(touch.id);
|
||||||
|
let location = Location {
|
||||||
|
target: match RenderTarget::Window(WindowRef::Entity(touch.window))
|
||||||
|
.normalize(primary_window.get_single().ok())
|
||||||
|
{
|
||||||
|
Some(target) => target,
|
||||||
|
None => continue,
|
||||||
|
},
|
||||||
|
position: touch.position,
|
||||||
|
};
|
||||||
|
match touch.phase {
|
||||||
|
TouchPhase::Started => {
|
||||||
|
debug!("Spawning pointer {:?}", pointer);
|
||||||
|
commands.spawn((PointerBundle::new(pointer).with_location(location.clone()),));
|
||||||
|
|
||||||
|
input_moves.send(InputMove::new(pointer, location, Vec2::ZERO));
|
||||||
|
input_presses.send(InputPress::new_down(pointer, PointerButton::Primary));
|
||||||
|
location_cache.insert(touch.id, *touch);
|
||||||
|
}
|
||||||
|
TouchPhase::Moved => {
|
||||||
|
// Send a move event only if it isn't the same as the last one
|
||||||
|
if let Some(last_touch) = location_cache.get(&touch.id) {
|
||||||
|
if last_touch == touch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
input_moves.send(InputMove::new(
|
||||||
|
pointer,
|
||||||
|
location,
|
||||||
|
touch.position - last_touch.position,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
location_cache.insert(touch.id, *touch);
|
||||||
|
}
|
||||||
|
TouchPhase::Ended | TouchPhase::Canceled => {
|
||||||
|
input_presses.send(InputPress::new_up(pointer, PointerButton::Primary));
|
||||||
|
location_cache.remove(&touch.id);
|
||||||
|
cancel_events.send(PointerCancel {
|
||||||
|
pointer_id: pointer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deactivates unused touch pointers.
|
||||||
|
///
|
||||||
|
/// Because each new touch gets assigned a new ID, we need to remove the pointers associated with
|
||||||
|
/// touches that are no longer active.
|
||||||
|
pub fn deactivate_touch_pointers(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut despawn_list: Local<HashSet<(Entity, PointerId)>>,
|
||||||
|
pointers: Query<(Entity, &PointerId)>,
|
||||||
|
mut touches: EventReader<TouchInput>,
|
||||||
|
) {
|
||||||
|
for touch in touches.read() {
|
||||||
|
match touch.phase {
|
||||||
|
TouchPhase::Ended | TouchPhase::Canceled => {
|
||||||
|
for (entity, pointer) in &pointers {
|
||||||
|
if pointer.get_touch_id() == Some(touch.id) {
|
||||||
|
despawn_list.insert((entity, *pointer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A hash set is used to prevent despawning the same entity twice.
|
||||||
|
for (entity, pointer) in despawn_list.drain() {
|
||||||
|
debug!("Despawning pointer {:?}", pointer);
|
||||||
|
commands.entity(entity).despawn_recursive();
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,9 @@
|
|||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
pub mod backend;
|
pub mod backend;
|
||||||
|
pub mod events;
|
||||||
|
pub mod focus;
|
||||||
|
pub mod input;
|
||||||
pub mod pointer;
|
pub mod pointer;
|
||||||
|
|
||||||
use bevy_app::prelude::*;
|
use bevy_app::prelude::*;
|
||||||
@ -26,8 +29,6 @@ impl PickingPluginsSettings {
|
|||||||
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
|
||||||
}
|
}
|
||||||
// TODO: remove this allow after focus/hover is implemented in bevy_picking
|
|
||||||
#[allow(rustdoc::broken_intra_doc_links)]
|
|
||||||
/// 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 {
|
||||||
@ -72,11 +73,7 @@ pub struct Pickable {
|
|||||||
///
|
///
|
||||||
/// Entities without the [`Pickable`] component will block by default.
|
/// Entities without the [`Pickable`] component will block by default.
|
||||||
pub should_block_lower: bool,
|
pub should_block_lower: bool,
|
||||||
// TODO: remove this allow after focus/hover is implemented in bevy_picking
|
|
||||||
#[allow(rustdoc::broken_intra_doc_links)]
|
|
||||||
/// Should this entity be added to the [`HoverMap`](focus::HoverMap) and thus emit events when
|
|
||||||
/// targeted?
|
|
||||||
///
|
|
||||||
/// If this is set to `false` and `should_block_lower` is set to true, this entity will block
|
/// If this is set to `false` and `should_block_lower` is set to true, this entity will block
|
||||||
/// lower entities from being interacted and at the same time will itself not emit any events.
|
/// lower entities from being interacted and at the same time will itself not emit any events.
|
||||||
///
|
///
|
||||||
@ -214,3 +211,30 @@ impl Plugin for PickingPlugin {
|
|||||||
.register_type::<backend::ray::RayId>();
|
.register_type::<backend::ray::RayId>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates [`Pointer`](events::Pointer) events and handles event bubbling.
|
||||||
|
pub struct InteractionPlugin;
|
||||||
|
|
||||||
|
impl Plugin for InteractionPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
use events::*;
|
||||||
|
use focus::{update_focus, update_interactions};
|
||||||
|
|
||||||
|
app.init_resource::<focus::HoverMap>()
|
||||||
|
.init_resource::<focus::PreviousHoverMap>()
|
||||||
|
.init_resource::<DragMap>()
|
||||||
|
.add_event::<PointerCancel>()
|
||||||
|
.add_systems(
|
||||||
|
PreUpdate,
|
||||||
|
(
|
||||||
|
update_focus,
|
||||||
|
pointer_events,
|
||||||
|
update_interactions,
|
||||||
|
send_click_and_drag_events,
|
||||||
|
send_drag_over_events,
|
||||||
|
)
|
||||||
|
.chain()
|
||||||
|
.in_set(PickSet::Focus),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user