bevy/crates/bevy_picking/src/input.rs
Alice Cecile 2ad5908e58
Make Query::single (and friends) return a Result (#18082)
# Objective

As discussed in #14275, Bevy is currently too prone to panic, and makes
the easy / beginner-friendly way to do a large number of operations just
to panic on failure.

This is seriously frustrating in library code, but also slows down
development, as many of the `Query::single` panics can actually safely
be an early return (these panics are often due to a small ordering issue
or a change in game state.

More critically, in most "finished" products, panics are unacceptable:
any unexpected failures should be handled elsewhere. That's where the
new

With the advent of good system error handling, we can now remove this.

Note: I was instrumental in a) introducing this idea in the first place
and b) pushing to make the panicking variant the default. The
introduction of both `let else` statements in Rust and the fancy system
error handling work in 0.16 have changed my mind on the right balance
here.

## Solution

1. Make `Query::single` and `Query::single_mut` (and other random
related methods) return a `Result`.
2. Handle all of Bevy's internal usage of these APIs.
3. Deprecate `Query::get_single` and friends, since we've moved their
functionality to the nice names.
4. Add detailed advice on how to best handle these errors.

Generally I like the diff here, although `get_single().unwrap()` in
tests is a bit of a downgrade.

## Testing

I've done a global search for `.single` to track down any missed
deprecated usages.

As to whether or not all the migrations were successful, that's what CI
is for :)

## Future work

~~Rename `Query::get_single` and friends to `Query::single`!~~

~~I've opted not to do this in this PR, and smear it across two releases
in order to ease the migration. Successive deprecations are much easier
to manage than the semantics and types shifting under your feet.~~

Cart has convinced me to change my mind on this; see
https://github.com/bevyengine/bevy/pull/18082#discussion_r1974536085.

## Migration guide

`Query::single`, `Query::single_mut` and their `QueryState` equivalents
now return a `Result`. Generally, you'll want to:

1. Use Bevy 0.16's system error handling to return a `Result` using the
`?` operator.
2. Use a `let else Ok(data)` block to early return if it's an expected
failure.
3. Use `unwrap()` or `Ok` destructuring inside of tests.

The old `Query::get_single` (etc) methods which did this have been
deprecated.
2025-03-02 19:51:56 +00:00

280 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_input::{
mouse::MouseWheel,
prelude::*,
touch::{TouchInput, TouchPhase},
ButtonState,
};
use bevy_math::Vec2;
use bevy_platform_support::collections::{HashMap, HashSet};
use bevy_reflect::prelude::*;
use bevy_render::camera::RenderTarget;
use bevy_window::{PrimaryWindow, WindowEvent, WindowRef};
use tracing::debug;
use crate::pointer::{
Location, PointerAction, PointerButton, PointerId, PointerInput, PointerLocation,
};
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.single().ok())
{
Some(target) => target,
None => continue,
},
position: event.position,
};
pointer_events.write(PointerInput::new(
PointerId::Mouse,
location,
PointerAction::Move {
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.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 action = match input.state {
ButtonState::Pressed => PointerAction::Press(button),
ButtonState::Released => PointerAction::Release(button),
};
pointer_events.write(PointerInput::new(PointerId::Mouse, location, action));
}
WindowEvent::MouseWheel(event) => {
let MouseWheel { unit, x, y, window } = *event;
let location = Location {
target: match RenderTarget::Window(WindowRef::Entity(window))
.normalize(primary_window.single().ok())
{
Some(target) => target,
None => continue,
},
position: *cursor_last,
};
let action = PointerAction::Scroll { x, y, unit };
pointer_events.write(PointerInput::new(PointerId::Mouse, location, action));
}
_ => {}
}
}
}
/// 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.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.write(PointerInput::new(
pointer,
location,
PointerAction::Press(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.write(PointerInput::new(
pointer,
location,
PointerAction::Move {
delta: touch.position - last_touch.position,
},
));
}
touch_cache.insert(touch.id, *touch);
}
TouchPhase::Ended => {
pointer_events.write(PointerInput::new(
pointer,
location,
PointerAction::Release(PointerButton::Primary),
));
touch_cache.remove(&touch.id);
}
TouchPhase::Canceled => {
pointer_events.write(PointerInput::new(
pointer,
location,
PointerAction::Cancel,
));
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();
}
}