bevy/crates/bevy_picking/src/input.rs
Emerson Coskey d1927736de
Migrate bevy picking (#15690)
# Objective

Migrate `bevy_picking` to the required components API

## Solution
- Made `PointerId` require `PointerLocation`, `PointerPress`, and
`PointerInteraction`
- Removed `PointerBundle`
- Removed all engine uses of `PointerBundle`

- Added convenience constructor `PointerLocation::new(location:
Location)`

## Testing

- ran unit tests
- ran `sprite_picking` example, everything seemed fine.

## Migration Guide

This API hasn't shipped yet, so I didn't bother with a deprecation.
However, for any crates tracking main the changes are as follows:

Previous api:
```rs
commands.insert(PointerBundle::new(PointerId::Mouse));
commands.insert(PointerBundle::new(PointerId::Mouse).with_location(location));
```

New api:
```rs
commands.insert(PointerId::Mouse);
commands.insert((PointerId::Mouse, PointerLocation::new(location)));
```
2024-10-07 16:26:37 +00:00

272 lines
10 KiB
Rust

//! This module provides unsurprising default inputs to `bevy_picking` through [`PointerInput`].
//! The included systems are responsible for sending mouse and touch inputs to their
//! respective `Pointer`s.
//!
//! Because this has it's own plugin, 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`], and write a system
//! that updates its position. If you want this to work properly with the existing interaction events,
//! you need to be sure that you also write a [`PointerInput`] event stream.
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_hierarchy::DespawnRecursiveExt;
use bevy_input::{
prelude::*,
touch::{TouchInput, TouchPhase},
ButtonState,
};
use bevy_math::Vec2;
use bevy_reflect::prelude::*;
use bevy_render::camera::RenderTarget;
use bevy_utils::{tracing::debug, HashMap, HashSet};
use bevy_window::{PrimaryWindow, WindowEvent, WindowRef};
use crate::pointer::{
Location, PointerAction, PointerButton, PointerId, PointerInput, PointerLocation,
PressDirection,
};
use crate::PickSet;
/// The picking input prelude.
///
/// This includes the most common types in this module, re-exported for your convenience.
pub mod prelude {
pub use crate::input::PointerInputPlugin;
}
/// 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::PickingPlugin::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.
///
/// This plugin contains several settings, and is added to the world as a resource after initialization.
/// You can configure pointer input settings at runtime by accessing the resource.
#[derive(Copy, Clone, Resource, Debug, Reflect)]
#[reflect(Resource, Default)]
pub struct PointerInputPlugin {
/// Should touch inputs be updated?
pub is_touch_enabled: bool,
/// Should mouse inputs be updated?
pub is_mouse_enabled: bool,
}
impl PointerInputPlugin {
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 PointerInputPlugin {
fn default() -> Self {
Self {
is_touch_enabled: true,
is_mouse_enabled: true,
}
}
}
impl Plugin for PointerInputPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(*self)
.add_systems(Startup, spawn_mouse_pointer)
.add_systems(
First,
(
mouse_pick_events.run_if(PointerInputPlugin::is_mouse_enabled),
touch_pick_events.run_if(PointerInputPlugin::is_touch_enabled),
)
.chain()
.in_set(PickSet::Input),
)
.add_systems(
Last,
deactivate_touch_pointers.run_if(PointerInputPlugin::is_touch_enabled),
)
.register_type::<Self>()
.register_type::<PointerInputPlugin>();
}
}
/// Spawns the default mouse pointer.
pub fn spawn_mouse_pointer(mut commands: Commands) {
commands.spawn(PointerId::Mouse);
}
/// Sends mouse pointer events to be processed by the core plugin
pub fn mouse_pick_events(
// Input
mut window_events: EventReader<WindowEvent>,
primary_window: Query<Entity, With<PrimaryWindow>>,
// Locals
mut cursor_last: Local<Vec2>,
// Output
mut pointer_events: EventWriter<PointerInput>,
) {
for window_event in window_events.read() {
match window_event {
// Handle cursor movement events
WindowEvent::CursorMoved(event) => {
let location = Location {
target: match RenderTarget::Window(WindowRef::Entity(event.window))
.normalize(primary_window.get_single().ok())
{
Some(target) => target,
None => continue,
},
position: event.position,
};
pointer_events.send(PointerInput::new(
PointerId::Mouse,
location,
PointerAction::Moved {
delta: event.position - *cursor_last,
},
));
*cursor_last = event.position;
}
// Handle mouse button press events
WindowEvent::MouseButtonInput(input) => {
let location = Location {
target: match RenderTarget::Window(WindowRef::Entity(input.window))
.normalize(primary_window.get_single().ok())
{
Some(target) => target,
None => continue,
},
position: *cursor_last,
};
let button = match input.button {
MouseButton::Left => PointerButton::Primary,
MouseButton::Right => PointerButton::Secondary,
MouseButton::Middle => PointerButton::Middle,
MouseButton::Other(_) | MouseButton::Back | MouseButton::Forward => continue,
};
let direction = match input.state {
ButtonState::Pressed => PressDirection::Down,
ButtonState::Released => PressDirection::Up,
};
pointer_events.send(PointerInput::new(
PointerId::Mouse,
location,
PointerAction::Pressed { direction, button },
));
}
_ => {}
}
}
}
/// Sends touch pointer events to be consumed by the core plugin
pub fn touch_pick_events(
// Input
mut window_events: EventReader<WindowEvent>,
primary_window: Query<Entity, With<PrimaryWindow>>,
// Locals
mut touch_cache: Local<HashMap<u64, TouchInput>>,
// Output
mut commands: Commands,
mut pointer_events: EventWriter<PointerInput>,
) {
for window_event in window_events.read() {
if let WindowEvent::TouchInput(touch) = window_event {
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((pointer, PointerLocation::new(location.clone())));
pointer_events.send(PointerInput::new(
pointer,
location,
PointerAction::Pressed {
direction: PressDirection::Down,
button: PointerButton::Primary,
},
));
touch_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) = touch_cache.get(&touch.id) {
if last_touch == touch {
continue;
}
pointer_events.send(PointerInput::new(
pointer,
location,
PointerAction::Moved {
delta: touch.position - last_touch.position,
},
));
}
touch_cache.insert(touch.id, *touch);
}
TouchPhase::Ended => {
pointer_events.send(PointerInput::new(
pointer,
location,
PointerAction::Pressed {
direction: PressDirection::Up,
button: PointerButton::Primary,
},
));
touch_cache.remove(&touch.id);
}
TouchPhase::Canceled => {
pointer_events.send(PointerInput::new(
pointer,
location,
PointerAction::Canceled,
));
touch_cache.remove(&touch.id);
}
}
}
}
}
/// 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() {
if let TouchPhase::Ended | TouchPhase::Canceled = touch.phase {
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();
}
}