diff --git a/crates/bevy_picking/Cargo.toml b/crates/bevy_picking/Cargo.toml index a65a7f42832b6..30601f4631bc7 100644 --- a/crates/bevy_picking/Cargo.toml +++ b/crates/bevy_picking/Cargo.toml @@ -9,7 +9,10 @@ license = "MIT OR Apache-2.0" [dependencies] 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_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_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" } bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs new file mode 100644 index 0000000000000..efb685d55a85c --- /dev/null +++ b/crates/bevy_picking/src/events.rs @@ -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 { + /// 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 Event for Pointer +where + E: Debug + Clone + Reflect, +{ + type Traversal = Parent; + const AUTO_PROPAGATE: bool = true; +} + +impl std::fmt::Display for Pointer { + 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 std::ops::Deref for Pointer { + type Target = E; + + fn deref(&self) -> &Self::Target { + &self.event + } +} + +impl Pointer { + /// 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, + mut input_moves: EventReader, + pointer_map: Res, + pointers: Query<&PointerLocation>, + hover_map: Res, + previous_hover_map: Res, + // Output + mut pointer_move: EventWriter>, + mut pointer_over: EventWriter>, + mut pointer_out: EventWriter>, + mut pointer_up: EventWriter>, + mut pointer_down: EventWriter>, +) { + 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>); + +/// 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>, + mut pointer_up: EventReader>, + mut input_move: EventReader, + mut input_presses: EventReader, + pointer_map: Res, + pointers: Query<&PointerLocation>, + // Locals + mut down_map: Local< + HashMap<(PointerId, PointerButton), HashMap, Instant)>>, + >, + // Output + mut drag_map: ResMut, + mut pointer_click: EventWriter>, + mut pointer_drag_start: EventWriter>, + mut pointer_drag_end: EventWriter>, + mut pointer_drag: EventWriter>, +) { + 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, + mut pointer_over: EventReader>, + mut pointer_move: EventReader>, + mut pointer_out: EventReader>, + mut pointer_drag_end: EventReader>, + // Local + mut drag_over_map: Local>>, + + // Output + mut pointer_drag_enter: EventWriter>, + mut pointer_drag_over: EventWriter>, + mut pointer_drag_leave: EventWriter>, + mut pointer_drop: EventWriter>, +) { + // 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(), + }, + )); + } + } + } +} diff --git a/crates/bevy_picking/src/focus.rs b/crates/bevy_picking/src/focus.rs new file mode 100644 index 0000000000000..8a593335cdc14 --- /dev/null +++ b/crates/bevy_picking/src/focus.rs @@ -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; + +/// 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; + +/// 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>); + +/// 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>); + +/// 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, + mut cancellations: EventReader, + // Local + mut over_map: Local, + // Output + mut hover_map: ResMut, + mut previous_hover_map: ResMut, +) { + 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 = 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, + pointer_over_map: &mut Local, + pointer_cancel: &mut EventReader, +) { + let cancelled_pointers: Vec = 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, + // 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, + previous_hover_map: Res, + // 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::::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, +) { + 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); + } +} diff --git a/crates/bevy_picking/src/input/mod.rs b/crates/bevy_picking/src/input/mod.rs new file mode 100644 index 0000000000000..e3dd9e3695e6d --- /dev/null +++ b/crates/bevy_picking/src/input/mod.rs @@ -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) -> bool { + state.is_mouse_enabled + } + + fn is_touch_enabled(state: Res) -> 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::() + .register_type::(); + } +} diff --git a/crates/bevy_picking/src/input/mouse.rs b/crates/bevy_picking/src/input/mouse.rs new file mode 100644 index 0000000000000..73cf321f61165 --- /dev/null +++ b/crates/bevy_picking/src/input/mouse.rs @@ -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>, + mut cursor_moves: EventReader, + mut cursor_last: Local, + mut mouse_inputs: EventReader, + // Output + mut pointer_move: EventWriter, + mut pointer_presses: EventWriter, +) { + 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)); + } + } + } +} diff --git a/crates/bevy_picking/src/input/touch.rs b/crates/bevy_picking/src/input/touch.rs new file mode 100644 index 0000000000000..b6b7e6a33c85c --- /dev/null +++ b/crates/bevy_picking/src/input/touch.rs @@ -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, + primary_window: Query>, + // Local + mut location_cache: Local>, + // Output + mut commands: Commands, + mut input_moves: EventWriter, + mut input_presses: EventWriter, + mut cancel_events: EventWriter, +) { + 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>, + pointers: Query<(Entity, &PointerId)>, + mut touches: EventReader, +) { + 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(); + } +} diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 4063b34d7121b..522b1ec7d9d0e 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -3,6 +3,9 @@ #![deny(missing_docs)] pub mod backend; +pub mod events; +pub mod focus; +pub mod input; pub mod pointer; use bevy_app::prelude::*; @@ -26,8 +29,6 @@ impl PickingPluginsSettings { pub fn input_should_run(state: Res) -> bool { 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) /// component should be running. pub fn focus_should_run(state: Res) -> bool { @@ -72,11 +73,7 @@ pub struct Pickable { /// /// Entities without the [`Pickable`] component will block by default. 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 /// 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::(); } } + +/// 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::() + .init_resource::() + .init_resource::() + .add_event::() + .add_systems( + PreUpdate, + ( + update_focus, + pointer_events, + update_interactions, + send_click_and_drag_events, + send_drag_over_events, + ) + .chain() + .in_set(PickSet::Focus), + ); + } +}