From 82128d778ad9c5572300b0e9eff03195648cf423 Mon Sep 17 00:00:00 2001 From: Miles Silberling-Cook Date: Wed, 4 Sep 2024 14:41:06 -0500 Subject: [PATCH] Picking event ordering (#14862) # Objective Correctly order picking events. Resolves https://github.com/bevyengine/bevy/issues/5984. ## Solution Event ordering [very long standing problem](https://github.com/aevyrie/bevy_mod_picking/issues/294) with mod picking, stemming from two related issues. The first problem was that `Pointer` events of different types couldn't be ordered, but we have already gotten around that in the upstream by switching to observers. Since observers run in the order they are triggered, this isn't an issue. The second problem was that the underlying event streams that picking uses to create it's pointer interaction events *also* lacked ordering, and the systems that generated the points couldn't interleave events. This PR fixes that by unifying the event streams and integrating the various interaction systems. The concrete changes are as follows: + `bevy_winit::WinitEvent` has been moved to `bevy_window::WindowEvent`. This provides a unified (and more importantly, *ordered*) input stream for both `bevy_window` and `bevy_input` events. + Replaces `InputMove` and `InputPress` with `PointerInput`, a new unified input event which drives picking and interaction. This event is built to have drop-in forward compatibility with [winit's upcoming pointer abstraction](https://github.com/rust-windowing/winit/pull/3876). I have added code to emulate it using the current winit input abstractions, but this entire thing will be much more robust when it lands. + Rolls `pointer_events` `send_click_and_drag_events` and `send_drag_over_events` into a single system, which operates directly on `PointerEvent` and triggers observers as output. The PR also improves docs and takes the opportunity to refactor/streamline the pointer event dispatch logic. ## Status & Testing This PR is now feature complete and documented. While it is theoretically possible to add unit tests for the ordering, building the picking mocking for that will take a little while. Feedback on the chosen ordering of events is within-scope. ## Migration Guide For users switching from `bevy_mod_picking` to `bevy_picking`: + Instead of adding an `On` component, use `.observe(|trigger: Trigger|)`. You may now apply multiple handlers to the same entity using this command. + Pointer interaction events now have semi-deterministic ordering which (more or less) aligns with the order of the raw input stream. Consult the docs on `bevy_picking::event::pointer_events` for current information. You may need to adjust your event handling logic accordingly. + `PointerCancel` has been replaced with `Pointer`, which now has the semantics of an OS touch pointer cancel event. + `InputMove` and `InputPress` have been merged into `PointerInput`. The use remains exactly the same. + Picking interaction events are now only accessible through observers, and no `EventReader`. This functionality may be re-implemented later. For users of `bevy_winit`: + The event `bevy_winit::WinitEvent` has moved to `bevy_window::WindowEvent`. If this was the only thing you depended on `bevy_winit` for, you should switch your dependency to `bevy_window`. + `bevy_window` now depends on `bevy_input`. The dependencies of `bevy_input` are a subset of the existing dependencies for `bevy_window` so this should be non-breaking. --- crates/bevy_picking/Cargo.toml | 5 +- crates/bevy_picking/src/events.rs | 798 ++++++++++++------------- crates/bevy_picking/src/focus.rs | 28 +- crates/bevy_picking/src/input.rs | 267 +++++++++ crates/bevy_picking/src/input/mod.rs | 86 --- crates/bevy_picking/src/input/mouse.rs | 67 --- crates/bevy_picking/src/input/touch.rs | 105 ---- crates/bevy_picking/src/lib.rs | 269 ++++++--- crates/bevy_picking/src/pointer.rs | 184 +++--- crates/bevy_window/Cargo.toml | 1 + crates/bevy_window/src/event.rs | 199 +++++- crates/bevy_window/src/lib.rs | 6 +- crates/bevy_winit/src/lib.rs | 13 +- crates/bevy_winit/src/state.rs | 140 ++--- crates/bevy_winit/src/winit_event.rs | 209 ------- 15 files changed, 1229 insertions(+), 1148 deletions(-) create mode 100644 crates/bevy_picking/src/input.rs delete mode 100644 crates/bevy_picking/src/input/mod.rs delete mode 100644 crates/bevy_picking/src/input/mouse.rs delete mode 100644 crates/bevy_picking/src/input/touch.rs delete mode 100644 crates/bevy_winit/src/winit_event.rs diff --git a/crates/bevy_picking/Cargo.toml b/crates/bevy_picking/Cargo.toml index eadd869e2d313..64eada2dbb04e 100644 --- a/crates/bevy_picking/Cargo.toml +++ b/crates/bevy_picking/Cargo.toml @@ -9,6 +9,7 @@ license = "MIT OR Apache-2.0" [dependencies] bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } +bevy_asset = { path = "../bevy_asset", 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" } @@ -16,13 +17,11 @@ 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" } +bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } - uuid = { version = "1.1", features = ["v4"] } [lints] diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index a1d5aee04bace..00293c1d5e297 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -1,8 +1,44 @@ -//! Processes data from input and backends, producing interaction events. +//! This module defines a stateful set of interaction events driven by the `PointerInput` stream +//! and the hover state of each Pointer. +//! +//! # Usage +//! +//! To receive events from this module, you must use an [`Observer`] +//! The simplest example, registering a callback when an entity is hovered over by a pointer, looks like this: +//! +//! ```rust +//! # use bevy_ecs::prelude::*; +//! # use bevy_picking::prelude::*; +//! # let mut world = World::default(); +//! world.spawn_empty() +//! .observe(|trigger: Trigger>| { +//! println!("I am being hovered over"); +//! }); +//! ``` +//! +//! Observers give us three important properties: +//! 1. They allow for attaching event handlers to specific entities, +//! 2. they allow events to bubble up the entity hierarchy, +//! 3. and they allow events of different types to be called in a specific order. +//! +//! The order in which interaction events are received is extremely important, and you can read more +//! about it on the docs for the dispatcher system: [`pointer_events`]. This system runs in +//! [`PreUpdate`](bevy_app::PreUpdate) in [`PickSet::Focus`](crate::PickSet::Focus). All pointer-event +//! observers resolve during the sync point between [`pointer_events`] and +//! [`update_interactions`](crate::focus::update_interactions). +//! +//! # Events Types +//! +//! The events this module defines fall into a few broad categories: +//! + Hovering and movement: [`Over`], [`Move`], and [`Out`]. +//! + Clicking and pressing: [`Down`], [`Up`], and [`Click`]. +//! + Dragging and dropping: [`DragStart`], [`Drag`], [`DragEnd`], [`DragEnter`], [`DragOver`], [`DragDrop`], [`DragLeave`]. +//! +//! When received by an observer, these events will always be wrapped by the [`Pointer`] type, which contains +//! general metadata about the pointer and it's location. use std::fmt::Debug; -use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; use bevy_hierarchy::Parent; use bevy_math::Vec2; @@ -13,15 +49,16 @@ use crate::{ backend::{prelude::PointerLocation, HitData}, focus::{HoverMap, PreviousHoverMap}, pointer::{ - InputMove, InputPress, Location, PointerButton, PointerId, PointerMap, PressDirection, + Location, PointerAction, PointerButton, PointerId, PointerInput, PointerMap, PressDirection, }, }; -/// Stores the common data needed for all `PointerEvent`s. +/// Stores the common data needed for all pointer events. +/// +/// The documentation for the [`pointer_events`] explains the events this module exposes and +/// the order in which they fire. #[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 @@ -36,14 +73,15 @@ 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 + "{:?}, {:.1?}, {:.1?}", + self.pointer_id, self.pointer_location.position, self.event )) } } @@ -57,23 +95,21 @@ impl std::ops::Deref for Pointer { } impl Pointer { - /// Construct a new `PointerEvent`. - pub fn new(id: PointerId, location: Location, target: Entity, event: E) -> Self { + /// Construct a new `Pointer` event. + pub fn new(id: PointerId, location: Location, 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 pointer is canceled, and it's current interaction state is dropped. +#[derive(Clone, PartialEq, Debug, Reflect)] +pub struct Cancel { + /// Information about the picking intersection. + pub hit: HitData, } /// Fires when a the pointer crosses into the bounds of the `target` entity. @@ -202,24 +238,68 @@ pub struct DragDrop { pub hit: HitData, } -/// Generates pointer events from input and focus data +/// Dragging state. +#[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, +} + +/// An entry in the cache that drives the `pointer_events` system, storing additional data +/// about pointer button presses. +#[derive(Debug, Clone, Default)] +pub struct PointerState { + /// Stores the press location and start time for each button currently being pressed by the pointer. + pub pressing: HashMap, + /// Stores the the starting and current locations for each entity currently being dragged by the pointer. + pub dragging: HashMap, + /// Stores the hit data for each entity currently being dragged over by the pointer. + pub dragging_over: HashMap, +} + +/// Dispatches interaction events to the target entities. +/// +/// Within a single frame, events are dispatched in the following order: +/// + The sequence [`DragEnter`], [`Over`]. +/// + Any number of any of the following: +/// + For each movement: The sequence [`DragStart`], [`Drag`], [`DragOver`], [`Move`]. +/// + For each button press: Either [`Down`], or the sequence [`DragDrop`], [`DragEnd`], [`DragLeave`], [`Click`], [`Up`]. +/// + For each pointer cancellation: Simply [`Cancel`]. +/// + Finally the sequence [`DragLeave`], [`Out`]. +/// +/// Only the last event in a given sequence is garenteed to be present. +/// +/// Additionally, across multiple frames, the following are also strictly ordered by the interaction state machine: +/// + When a pointer moves over the target: [`Over`], [`Move`], [`Out`]. +/// + When a pointer presses buttons on the target: [`Down`], [`Up`], [`Click`]. +/// + When a pointer drags the target: [`DragStart`], [`Drag`], [`DragEnd`]. +/// + When a pointer drags something over the target: [`DragEnter`], [`DragOver`], [`DragDrop`], [`DragLeave`]. +/// + When a pointer is canceled: No other events will follow the [`Cancel`] event for that pointer. +/// +/// Two events -- [`Over`] and [`Out`] -- are driven only by the [`HoverMap`]. The rest rely on additional data from the +/// [`PointerInput`] event stream. To receive these events for a custom pointer, you must add [`PointerInput`] events. +/// +/// Note: Though it is common for the [`PointerInput`] stream may contain multiple pointer movements and presses each frame, +/// the hover state is determined only by the pointer's *final position*. Since the hover state ultimately determines which +/// entities receive events, this may mean that an entity can receive events which occurred before it was actually hovered. #[allow(clippy::too_many_arguments)] pub fn pointer_events( - mut commands: Commands, // Input - mut input_presses: EventReader, - mut input_moves: EventReader, - pointer_map: Res, + mut input_events: EventReader, + // ECS State pointers: Query<&PointerLocation>, + pointer_map: Res, hover_map: Res, previous_hover_map: Res, + // Local state + mut pointer_state: Local>, // Output - mut pointer_move: EventWriter>, - mut pointer_over: EventWriter>, - mut pointer_out: EventWriter>, - mut pointer_up: EventWriter>, - mut pointer_down: EventWriter>, + mut commands: Commands, ) { + // Setup utilities + let now = Instant::now(); let pointer_location = |pointer_id: PointerId| { pointer_map .get_entity(pointer_id) @@ -227,81 +307,6 @@ pub fn pointer_events( .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()))) - { - let event = Pointer::new( - pointer_id, - location.clone(), - hovered_entity, - Move { hit, delta }, - ); - commands.trigger_targets(event.clone(), event.target); - pointer_move.send(event); - } - } - - 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; - }; - let event = Pointer::new( - press_event.pointer_id, - location, - hovered_entity, - Up { button, hit }, - ); - commands.trigger_targets(event.clone(), event.target); - pointer_up.send(event); - } - } - 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; - }; - let event = Pointer::new( - press_event.pointer_id, - location, - hovered_entity, - Down { button, hit }, - ); - commands.trigger_targets(event.clone(), event.target); - pointer_down.send(event); - } - } - } - // If the entity is hovered... for (pointer_id, hovered_entity, hit) in hover_map .iter() @@ -320,9 +325,252 @@ pub fn pointer_events( ); continue; }; - let event = Pointer::new(pointer_id, location, hovered_entity, Over { hit }); - commands.trigger_targets(event.clone(), event.target); - pointer_over.send(event); + // Possibly send DragEnter events + for button in PointerButton::iter() { + let state = pointer_state.entry((pointer_id, button)).or_default(); + + for drag_target in state + .dragging + .keys() + .filter(|&&drag_target| hovered_entity != drag_target) + { + state.dragging_over.insert(hovered_entity, hit.clone()); + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + DragEnter { + button, + dragged: *drag_target, + hit: hit.clone(), + }, + ), + hovered_entity, + ); + } + } + // Always send Over events + commands.trigger_targets( + Pointer::new(pointer_id, location.clone(), Over { hit: hit.clone() }), + hovered_entity, + ); + } + } + + // Dispatch input events... + for PointerInput { + pointer_id, + location, + action, + } in input_events.read().cloned() + { + match action { + // Pressed Button + PointerAction::Pressed { direction, button } => { + let state = pointer_state.entry((pointer_id, button)).or_default(); + + // Possibly emit DragEnd, DragDrop, DragLeave on button releases + if direction == PressDirection::Up { + // For each currently dragged entity + for (drag_target, drag) in state.dragging.drain() { + // Emit DragDrop + for (dragged_over, hit) in state.dragging_over.iter() { + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + DragDrop { + button, + dropped: drag_target, + hit: hit.clone(), + }, + ), + *dragged_over, + ); + } + // Emit DragEnd + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + DragEnd { + button, + distance: drag.latest_pos - drag.start_pos, + }, + ), + drag_target, + ); + // Emit DragLeave + for (dragged_over, hit) in state.dragging_over.iter() { + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + DragLeave { + button, + dragged: drag_target, + hit: hit.clone(), + }, + ), + *dragged_over, + ); + } + } + } + + // Send a Down or possibly a Click and an Up button events + for (hovered_entity, hit) in previous_hover_map + .get(&pointer_id) + .iter() + .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.clone()))) + { + match direction { + PressDirection::Down => { + // Send the Down event first + let event = + Pointer::new(pointer_id, location.clone(), Down { button, hit }); + commands.trigger_targets(event.clone(), hovered_entity); + // Also insert the press into the state + state + .pressing + .insert(hovered_entity, (event.pointer_location, now)); + } + PressDirection::Up => { + // If this pointer previously pressed the hovered entity, first send a Click event + if let Some((_location, press_instant)) = + state.pressing.get(&hovered_entity) + { + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + Click { + button, + hit: hit.clone(), + duration: now - *press_instant, + }, + ), + hovered_entity, + ); + } + // Always send the Up event + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + Up { + button, + hit: hit.clone(), + }, + ), + hovered_entity, + ); + // Also clear the state + state.pressing.clear(); + state.dragging.clear(); + state.dragging_over.clear(); + } + }; + } + } + // Moved + PointerAction::Moved { delta } => { + for (hovered_entity, hit) in hover_map + .get(&pointer_id) + .iter() + .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned()))) + { + // Send drag events to entities being dragged or dragged over + for button in PointerButton::iter() { + let state = pointer_state.entry((pointer_id, button)).or_default(); + + // Emit a DragStart the first time the pointer moves while pressing an entity + for (location, _instant) in state.pressing.values() { + if state.dragging.contains_key(&hovered_entity) { + continue; // this entity is already logged as being dragged + } + state.dragging.insert( + hovered_entity, + DragEntry { + start_pos: location.position, + latest_pos: location.position, + }, + ); + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + DragStart { + button, + hit: hit.clone(), + }, + ), + hovered_entity, + ); + } + + // Emit a Drag event to the dragged entity when it is dragged over another entity. + for (dragged_entity, drag) in state.dragging.iter_mut() { + let drag_event = Drag { + button, + distance: location.position - drag.start_pos, + delta: location.position - drag.latest_pos, + }; + drag.latest_pos = location.position; + let target = *dragged_entity; + let event = Pointer::new(pointer_id, location.clone(), drag_event); + commands.trigger_targets(event, target); + } + + // Emit a DragOver to the hovered entity when dragging a different entity over it. + for drag_target in state.dragging.keys() + .filter( + |&&drag_target| hovered_entity != drag_target, /* can't drag over itself */ + ) + { + commands.trigger_targets( + Pointer::new(pointer_id, location.clone(), DragOver { button, dragged: *drag_target, hit: hit.clone() }), + hovered_entity, + ); + } + } + + // Always send Move event + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + Move { + hit: hit.clone(), + delta, + }, + ), + hovered_entity, + ); + } + } + // Canceled + PointerAction::Canceled => { + // Emit a Cancel to the hovered entity. + for (hovered_entity, hit) in hover_map + .get(&pointer_id) + .iter() + .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned()))) + { + commands.trigger_targets( + Pointer::new(pointer_id, location.clone(), Cancel { hit }), + hovered_entity, + ); + } + // Clear the local state for the canceled pointer + for button in PointerButton::iter() { + if let Some(state) = pointer_state.get_mut(&(pointer_id, button)) { + state.pressing.clear(); + state.dragging.clear(); + state.dragging_over.clear(); + } + } + } } } @@ -344,329 +592,31 @@ pub fn pointer_events( ); continue; }; - let event = Pointer::new(pointer_id, location, hovered_entity, Out { hit }); - commands.trigger_targets(event.clone(), event.target); - pointer_out.send(event); - } - } -} - -/// 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( - // for triggering observers - // - Pointer - // - Pointer - // - Pointer - mut commands: Commands, - // 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)>>, - >, - // Outputs used for further processing - mut drag_map: ResMut, - mut pointer_drag_end: 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 + // Possibly send DragLeave events + for button in PointerButton::iter() { + let state = pointer_state.entry((pointer_id, button)).or_default(); + state.dragging_over.remove(&hovered_entity); + for drag_target in state.dragging.keys() { + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + DragLeave { + button, + dragged: *drag_target, + hit: hit.clone(), + }, + ), + hovered_entity, + ); } - drag_list.insert( - down.target, - DragEntry { - start_pos: down.pointer_location.position, - latest_pos: down.pointer_location.position, - }, - ); - let event = Pointer::new( - pointer_id, - down.pointer_location.clone(), - down.target, - DragStart { - button, - hit: down.hit.clone(), - }, - ); - commands.trigger_targets(event, down.target); } - 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; - let target = *dragged_entity; - let event = Pointer::new(pointer_id, location.clone(), target, drag_event); - commands.trigger_targets(event, target); - } - } - } - - // 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; - let event = Pointer::new( - pointer_id, - pointer_location, - target, - Click { - button, - hit, - duration, - }, - ); - commands.trigger_targets(event, target); - } - } - - // 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, - }; - let event = Pointer::new(press.pointer_id, location.clone(), drag_target, drag_end); - commands.trigger_targets(event.clone(), event.target); - pointer_drag_end.send(event); - } - } -} - -/// Uses pointer events to determine when drag-over events occur -#[allow(clippy::too_many_arguments)] -pub fn send_drag_over_events( - // uses this to trigger the following - // - Pointer, - // - Pointer, - // - Pointer, - // - Pointer, - mut commands: Commands, - // 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>>, -) { - // 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 = Pointer::new( - pointer_id, - pointer_location.clone(), - target, - DragEnter { - button, - dragged: *drag_target, - hit: hit.clone(), - }, - ); - commands.trigger_targets(event, target); - } - } - } - - // 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 */ - ) - { - let event = Pointer::new( - pointer_id, - pointer_location.clone(), - target, - DragOver { - button, - dragged: *drag_target, - hit: hit.clone(), - }, - ); - commands.trigger_targets(event, target); - } - } - } - - // Fire PointerDragLeave and PointerDrop events when the pointer stops dragging. - for Pointer { - pointer_id, - pointer_location, - target: drag_end_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() { - let target = dragged_over; - let event = Pointer::new( - pointer_id, - pointer_location.clone(), - dragged_over, - DragLeave { - button, - dragged: drag_end_target, - hit: hit.clone(), - }, - ); - commands.trigger_targets(event, target); - - let event = Pointer::new( - pointer_id, - pointer_location.clone(), - target, - DragDrop { - button, - dropped: target, - hit: hit.clone(), - }, + // Always send Out events + commands.trigger_targets( + Pointer::new(pointer_id, location.clone(), Out { hit: hit.clone() }), + hovered_entity, ); - commands.trigger_targets(event, target); - } - } - - // 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() { - let event = Pointer::new( - pointer_id, - pointer_location.clone(), - target, - DragLeave { - button, - dragged: *drag_target, - hit: hit.clone(), - }, - ); - commands.trigger_targets(event, target); - } } } } diff --git a/crates/bevy_picking/src/focus.rs b/crates/bevy_picking/src/focus.rs index 8ae93ce2befad..c5fd0b989c96d 100644 --- a/crates/bevy_picking/src/focus.rs +++ b/crates/bevy_picking/src/focus.rs @@ -1,11 +1,16 @@ //! Determines which entities are being hovered by which pointers. +//! +//! The most important type in this module is the [`HoverMap`], which maps pointers to the entities +//! they are hovering over. -use std::{collections::BTreeMap, fmt::Debug}; +use std::{ + collections::{BTreeMap, HashSet}, + fmt::Debug, +}; use crate::{ backend::{self, HitData}, - events::PointerCancel, - pointer::{PointerId, PointerInteraction, PointerPress}, + pointer::{PointerAction, PointerId, PointerInput, PointerInteraction, PointerPress}, Pickable, }; @@ -63,7 +68,7 @@ pub fn update_focus( pickable: Query<&Pickable>, pointers: Query<&PointerId>, mut under_pointer: EventReader, - mut cancellations: EventReader, + mut pointer_input: EventReader, // Local mut over_map: Local, // Output @@ -76,7 +81,7 @@ pub fn update_focus( &mut over_map, &pointers, ); - build_over_map(&mut under_pointer, &mut over_map, &mut cancellations); + build_over_map(&mut under_pointer, &mut over_map, &mut pointer_input); build_hover_map(&pointers, pickable, &over_map, &mut hover_map); } @@ -109,9 +114,18 @@ fn reset_maps( fn build_over_map( backend_events: &mut EventReader, pointer_over_map: &mut Local, - pointer_cancel: &mut EventReader, + pointer_input: &mut EventReader, ) { - let cancelled_pointers: Vec = pointer_cancel.read().map(|p| p.pointer_id).collect(); + let cancelled_pointers: HashSet = pointer_input + .read() + .filter_map(|p| { + if let PointerAction::Canceled = p.action { + Some(p.pointer_id) + } else { + None + } + }) + .collect(); for entities_under_pointer in backend_events .read() diff --git a/crates/bevy_picking/src/input.rs b/crates/bevy_picking/src/input.rs new file mode 100644 index 0000000000000..303c694bb4518 --- /dev/null +++ b/crates/bevy_picking/src/input.rs @@ -0,0 +1,267 @@ +//! 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::touch::{TouchInput, TouchPhase}; +use bevy_input::{prelude::*, 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, PressDirection}, + PointerBundle, +}; + +use crate::PickSet; + +/// Common imports for `bevy_picking`. +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) -> bool { + state.is_mouse_enabled + } + + fn is_touch_enabled(state: Res) -> 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::() + .register_type::(); + } +} + +/// 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 + mut window_events: EventReader, + primary_window: Query>, + // Locals + mut cursor_last: Local, + // Output + mut pointer_events: EventWriter, +) { + 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, + primary_window: Query>, + // Locals + mut touch_cache: Local>, + // Output + mut commands: Commands, + mut pointer_events: EventWriter, +) { + 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(PointerBundle::new(pointer).with_location(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>, + pointers: Query<(Entity, &PointerId)>, + mut touches: EventReader, +) { + 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(); + } +} diff --git a/crates/bevy_picking/src/input/mod.rs b/crates/bevy_picking/src/input/mod.rs deleted file mode 100644 index e3dd9e3695e6d..0000000000000 --- a/crates/bevy_picking/src/input/mod.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! `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 deleted file mode 100644 index 73cf321f61165..0000000000000 --- a/crates/bevy_picking/src/input/mouse.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! 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 deleted file mode 100644 index b6b7e6a33c85c..0000000000000 --- a/crates/bevy_picking/src/input/touch.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! 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 034ecfd9e8565..b2887bfbd680d 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -1,4 +1,152 @@ -//! TODO, write module doc +//! This crate provides 'picking' capabilities for the Bevy game engine. That means, in simple terms, figuring out +//! how to connect up a user's clicks or taps to the entities they are trying to interact with. +//! +//! ## Overview +//! +//! In the simplest case, this plugin allows you to click on things in the scene. However, it also +//! allows you to express more complex interactions, like detecting when a touch input drags a UI +//! element and drops it on a 3d mesh rendered to a different camera. The crate also provides a set of +//! interaction callbacks, allowing you to receive input directly on entities like here: +//! +//! ```rust +//! # use bevy_ecs::prelude::*; +//! # use bevy_picking::prelude::*; +//! # #[derive(Component)] +//! # struct MyComponent; +//! # let mut world = World::new(); +//! world.spawn(MyComponent) +//! .observe(|mut trigger: Trigger>| { +//! // Get the underlying event type +//! let click_event: &Pointer = trigger.event(); +//! // Stop the event from bubbling up the entity hierarchjy +//! trigger.propagate(false); +//! }); +//! ``` +//! +//! At its core, this crate provides a robust abstraction for computing picking state regardless of +//! pointing devices, or what you are hit testing against. It is designed to work with any input, including +//! mouse, touch, pens, or virtual pointers controlled by gamepads. +//! +//! ## Expressive Events +//! +//! The events in this module (see [`events`]) cannot be listened to with normal `EventReader`s. +//! Instead, they are dispatched to *ovservers* attached to specific entities. When events are generated, they +//! bubble up the entity hierarchy starting from their target, until they reach the root or bubbling is haulted +//! with a call to [`Trigger::propagate`](bevy_ecs::observer::Trigger::propagate). +//! See [`Observer`] for details. +//! +//! This allows you to run callbacks when any children of an entity are interacted with, and leads +//! to succinct, expressive code: +//! +//! ``` +//! # use bevy_ecs::prelude::*; +//! # use bevy_transform::prelude::*; +//! # use bevy_picking::prelude::*; +//! # #[derive(Event)] +//! # struct Greeting; +//! fn setup(mut commands: Commands) { +//! commands.spawn(Transform::default()) +//! // Spawn your entity here, e.g. a Mesh. +//! // When dragged, mutate the `Transform` component on the dragged target entity: +//! .observe(|trigger: Trigger>, mut transforms: Query<&mut Transform>| { +//! let mut transform = transforms.get_mut(trigger.entity()).unwrap(); +//! let drag = trigger.event(); +//! transform.rotate_local_y(drag.delta.x / 50.0); +//! }) +//! .observe(|trigger: Trigger>, mut commands: Commands| { +//! println!("Entity {:?} goes BOOM!", trigger.entity()); +//! commands.entity(trigger.entity()).despawn(); +//! }) +//! .observe(|trigger: Trigger>, mut events: EventWriter| { +//! events.send(Greeting); +//! }); +//! } +//! ``` +//! +//! ## Modularity +//! +//! #### Mix and Match Hit Testing Backends +//! +//! The plugin attempts to handle all the hard parts for you, all you need to do is tell it when a +//! pointer is hitting any entities. Multiple backends can be used at the same time! [Use this +//! simple API to write your own backend](crate::backend) in about 100 lines of code. +//! +//! #### Input Agnostic +//! +//! Picking provides a generic Pointer abstracton, which is useful for reacting to many different +//! types of input devices. Pointers can be controlled with anything, whether its the included mouse +//! or touch inputs, or a custom gamepad input system you write yourself to control a virtual pointer. +//! +//! ## Robustness +//! +//! In addition to these features, this plugin also correctly handles multitouch, multiple windows, +//! multiple cameras, viewports, and render layers. Using this as a library allows you to write a +//! picking backend that can interoperate with any other picking backend. +//! +//! # Getting Started +//! +//! TODO: This section will need to be re-written once more backends are introduced. +//! +//! #### Next Steps +//! +//! To learn more, take a look at the examples in the +//! [examples](https://github.com/bevyengine/bevy/tree/main/examples/picking). You +//! can read the next section to understand how the plugin works. +//! +//! # The Picking Pipeline +//! +//! This plugin is designed to be extremely modular. To do so, it works in well-defined stages that +//! form a pipeline, where events are used to pass data between each stage. +//! +//! #### Pointers ([`pointer`](mod@pointer)) +//! +//! The first stage of the pipeline is to gather inputs and update pointers. This stage is +//! ultimately responsible for generating [`PointerInput`](pointer::PointerInput) events. The provided +//! crate does this automatically for mouse, touch, and pen inputs. If you wanted to implement your own +//! pointer, controlled by some other input, you can do that here. The ordering of events within the +//! [`PointerInput`](pointer::PointerInput) stream is meaningful for events with the same +//! [`PointerId`](pointer::PointerId), but not between different pointers. +//! +//! Because pointer positions and presses are driven by these events, you can use them to mock +//! inputs for testing. +//! +//! After inputs are generated, they are then collected to update the current +//! [`PointerLocation`](pointer::PointerLocation) for each pointer. +//! +//! #### Backend ([`backend`]) +//! +//! A picking backend only has one job: reading [`PointerLocation`](pointer::PointerLocation) components, +//! and producing [`PointerHits`](backend::PointerHits). You can find all documentation and types needed to +//! implement a backend at [`backend`]. +//! +//! You will eventually need to choose which picking backend(s) you want to use. This crate does not +//! supply any backends, and expects you to select some from the other bevy crates or the third-party +//! ecosystem. You can find all the provided backends in the [`backend`] module. +//! +//! It's important to understand that you can mix and match backends! For example, you might have a +//! backend for your UI, and one for the 3d scene, with each being specialized for their purpose. +//! This crate provides some backends out of the box, but you can even write your own. It's been +//! made as easy as possible intentionally; the `bevy_mod_raycast` backend is 50 lines of code. +//! +//! #### Focus ([`focus`]) +//! +//! The next step is to use the data from the backends, combine and sort the results, and determine +//! what each cursor is hovering over, producing a [`HoverMap`](`crate::focus::HoverMap`). Note that +//! just because a pointer is over an entity, it is not necessarily *hovering* that entity. Although +//! multiple backends may be reporting that a pointer is hitting an entity, the focus system needs +//! to determine which entities are actually being hovered by this pointer based on the pick depth, +//! order of the backend, and the [`Pickable`] state of the entity. In other words, if one entity is +//! in front of another, usually only the topmost one will be hovered. +//! +//! #### Events ([`events`]) +//! +//! In the final step, the high-level pointer events are generated, such as events that trigger when +//! a pointer hovers or clicks an entity. These simple events are then used to generate more complex +//! events for dragging and dropping. +//! +//! Because it is completely agnostic to the the earlier stages of the pipeline, you can easily +//! extend the plugin with arbitrary backends and input methods, yet still use all the high level +//! features. #![deny(missing_docs)] @@ -16,45 +164,11 @@ use bevy_reflect::prelude::*; pub mod prelude { #[doc(hidden)] pub use crate::{ - events::*, input::InputPlugin, pointer::PointerButton, DefaultPickingPlugins, - InteractionPlugin, Pickable, PickingPlugin, PickingPluginsSettings, + events::*, input::PointerInputPlugin, pointer::PointerButton, DefaultPickingPlugins, + InteractionPlugin, Pickable, PickingPlugin, }; } -/// Used to globally toggle picking features at runtime. -#[derive(Clone, Debug, Resource, Reflect)] -#[reflect(Resource, Default)] -pub struct PickingPluginsSettings { - /// Enables and disables all picking features. - pub is_enabled: bool, - /// Enables and disables input collection. - pub is_input_enabled: bool, - /// Enables and disables updating interaction states of entities. - pub is_focus_enabled: bool, -} - -impl PickingPluginsSettings { - /// Whether or not input collection systems should be running. - pub fn input_should_run(state: Res) -> bool { - state.is_input_enabled && state.is_enabled - } - /// Whether or not systems updating entities' [`PickingInteraction`](focus::PickingInteraction) - /// component should be running. - pub fn focus_should_run(state: Res) -> bool { - state.is_focus_enabled && state.is_enabled - } -} - -impl Default for PickingPluginsSettings { - fn default() -> Self { - Self { - is_enabled: true, - is_input_enabled: true, - is_focus_enabled: true, - } - } -} - /// An optional component that overrides default picking behavior for an entity, allowing you to /// make an entity non-hoverable, or allow items below it to be hovered. See the documentation on /// the fields for more details. @@ -176,8 +290,9 @@ pub enum PickSet { Last, } -/// One plugin that contains the [`input::InputPlugin`], [`PickingPlugin`] and the [`InteractionPlugin`], -/// this is probably the plugin that will be most used. +/// One plugin that contains the [`PointerInputPlugin`](input::PointerInputPlugin), [`PickingPlugin`] +/// and the [`InteractionPlugin`], this is probably the plugin that will be most used. +/// /// Note: for any of these plugins to work, they require a picking backend to be active, /// The picking backend is responsible to turn an input, into a [`crate::backend::PointerHits`] /// that [`PickingPlugin`] and [`InteractionPlugin`] will refine into [`bevy_ecs::observer::Trigger`]s. @@ -187,8 +302,8 @@ pub struct DefaultPickingPlugins; impl Plugin for DefaultPickingPlugins { fn build(&self, app: &mut App) { app.add_plugins(( - input::InputPlugin::default(), - PickingPlugin, + input::PointerInputPlugin::default(), + PickingPlugin::default(), InteractionPlugin, )); } @@ -196,16 +311,48 @@ impl Plugin for DefaultPickingPlugins { /// This plugin sets up the core picking infrastructure. It receives input events, and provides the shared /// types used by other picking plugins. -#[derive(Default)] -pub struct PickingPlugin; +/// +/// This plugin contains several settings, and is added to the wrold as a resource after initialization. You +/// can configure picking settings at runtime through the resource. +#[derive(Copy, Clone, Debug, Resource, Reflect)] +#[reflect(Resource, Default)] +pub struct PickingPlugin { + /// Enables and disables all picking features. + pub is_enabled: bool, + /// Enables and disables input collection. + pub is_input_enabled: bool, + /// Enables and disables updating interaction states of entities. + pub is_focus_enabled: bool, +} + +impl PickingPlugin { + /// Whether or not input collection systems should be running. + pub fn input_should_run(state: Res) -> bool { + state.is_input_enabled && state.is_enabled + } + /// Whether or not systems updating entities' [`PickingInteraction`](focus::PickingInteraction) + /// component should be running. + pub fn focus_should_run(state: Res) -> bool { + state.is_focus_enabled && state.is_enabled + } +} + +impl Default for PickingPlugin { + fn default() -> Self { + Self { + is_enabled: true, + is_input_enabled: true, + is_focus_enabled: true, + } + } +} impl Plugin for PickingPlugin { fn build(&self, app: &mut App) { - app.init_resource::() + app.insert_resource(*self) .init_resource::() .init_resource::() - .add_event::() - .add_event::() + .add_event::() .add_event::() // Rather than try to mark all current and future backends as ambiguous with each other, // we allow them to send their hits in any order. These are later sorted, so submission @@ -215,9 +362,8 @@ impl Plugin for PickingPlugin { PreUpdate, ( pointer::update_pointer_map, - pointer::InputMove::receive, - pointer::InputPress::receive, - backend::ray::RayMap::repopulate.after(pointer::InputMove::receive), + pointer::PointerInput::receive, + backend::ray::RayMap::repopulate.after(pointer::PointerInput::receive), ) .in_set(PickSet::ProcessInput), ) @@ -231,21 +377,20 @@ impl Plugin for PickingPlugin { .configure_sets( PreUpdate, ( - PickSet::ProcessInput.run_if(PickingPluginsSettings::input_should_run), + PickSet::ProcessInput.run_if(Self::input_should_run), PickSet::Backend, - PickSet::Focus.run_if(PickingPluginsSettings::focus_should_run), + PickSet::Focus.run_if(Self::focus_should_run), PickSet::PostFocus, - // Eventually events will need to be dispatched here PickSet::Last, ) .chain(), ) + .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() - .register_type::() - .register_type::() .register_type::(); } } @@ -261,23 +406,9 @@ impl Plugin for InteractionPlugin { app.init_resource::() .init_resource::() - .init_resource::() - .add_event::() - .add_event::>() - .add_event::>() - .add_event::>() - .add_event::>() - .add_event::>() - .add_event::>() .add_systems( PreUpdate, - ( - update_focus, - pointer_events, - update_interactions, - send_click_and_drag_events, - send_drag_over_events, - ) + (update_focus, pointer_events, update_interactions) .chain() .in_set(PickSet::Focus), ); diff --git a/crates/bevy_picking/src/pointer.rs b/crates/bevy_picking/src/pointer.rs index 3d1991c1ec246..01c292bc1dec7 100644 --- a/crates/bevy_picking/src/pointer.rs +++ b/crates/bevy_picking/src/pointer.rs @@ -1,4 +1,12 @@ //! Types and systems for pointer inputs, such as position and buttons. +//! +//! The picking system is built around the concept of a 'Pointer', which is an +//! abstract representation of a user input with a specific screen location. The cursor +//! and touch input is provided under [`crate::input`], but you can also implement +//! your own custom pointers by supplying a unique ID. +//! +//! The purpose of this module is primarily to provide a common interface that can be +//! driven by lower-level input devices and consumed by higher-level interaction systems. use bevy_ecs::prelude::*; use bevy_math::{Rect, Vec2}; @@ -83,7 +91,7 @@ pub fn update_pointer_map(pointers: Query<(Entity, &PointerId)>, mut map: ResMut } } -/// Tracks the state of the pointer's buttons in response to [`InputPress`]s. +/// Tracks the state of the pointer's buttons in response to [`PointerInput`] events. #[derive(Debug, Default, Clone, Component, Reflect, PartialEq, Eq)] #[reflect(Component, Default)] pub struct PointerPress { @@ -118,68 +126,6 @@ impl PointerPress { } } -/// Pointer input event for button presses. Fires when a pointer button changes state. -#[derive(Event, Debug, Clone, Copy, PartialEq, Eq, Reflect)] -pub struct InputPress { - /// The [`PointerId`] of the pointer that pressed a button. - pub pointer_id: PointerId, - /// Direction of the button press. - pub direction: PressDirection, - /// Identifies the pointer button changing in this event. - pub button: PointerButton, -} - -impl InputPress { - /// Create a new pointer button down event. - pub fn new_down(id: PointerId, button: PointerButton) -> InputPress { - Self { - pointer_id: id, - direction: PressDirection::Down, - button, - } - } - - /// Create a new pointer button up event. - pub fn new_up(id: PointerId, button: PointerButton) -> InputPress { - Self { - pointer_id: id, - direction: PressDirection::Up, - button, - } - } - - /// Returns true if the `button` of this pointer was just pressed. - #[inline] - pub fn is_just_down(&self, button: PointerButton) -> bool { - self.button == button && self.direction == PressDirection::Down - } - - /// Returns true if the `button` of this pointer was just released. - #[inline] - pub fn is_just_up(&self, button: PointerButton) -> bool { - self.button == button && self.direction == PressDirection::Up - } - - /// Receives [`InputPress`] events and updates corresponding [`PointerPress`] components. - pub fn receive( - mut events: EventReader, - mut pointers: Query<(&PointerId, &mut PointerPress)>, - ) { - for input_press_event in events.read() { - pointers.iter_mut().for_each(|(pointer_id, mut pointer)| { - if *pointer_id == input_press_event.pointer_id { - let is_down = input_press_event.direction == PressDirection::Down; - match input_press_event.button { - PointerButton::Primary => pointer.primary = is_down, - PointerButton::Secondary => pointer.secondary = is_down, - PointerButton::Middle => pointer.middle = is_down, - } - } - }); - } - } -} - /// The stage of the pointer button press event #[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect)] pub enum PressDirection { @@ -225,42 +171,6 @@ impl PointerLocation { } } -/// Pointer input event for pointer moves. Fires when a pointer changes location. -#[derive(Event, Debug, Clone, Reflect)] -pub struct InputMove { - /// The [`PointerId`] of the pointer that is moving. - pub pointer_id: PointerId, - /// The [`Location`] of the pointer. - pub location: Location, - /// The distance moved (change in `position`) since the last event. - pub delta: Vec2, -} - -impl InputMove { - /// Create a new [`InputMove`] event. - pub fn new(id: PointerId, location: Location, delta: Vec2) -> InputMove { - Self { - pointer_id: id, - location, - delta, - } - } - - /// Receives [`InputMove`] events and updates corresponding [`PointerLocation`] components. - pub fn receive( - mut events: EventReader, - mut pointers: Query<(&PointerId, &mut PointerLocation)>, - ) { - for event_pointer in events.read() { - pointers.iter_mut().for_each(|(id, mut pointer)| { - if *id == event_pointer.pointer_id { - pointer.location = Some(event_pointer.location.to_owned()); - } - }); - } - } -} - /// The location of a pointer, including the current [`NormalizedRenderTarget`], and the x/y /// position of the pointer on this render target. /// @@ -309,3 +219,79 @@ impl Location { .unwrap_or(false) } } + +/// Types of actions that can be taken by pointers. +#[derive(Debug, Clone, Copy, Reflect)] +pub enum PointerAction { + /// A button has been pressed on the pointer. + Pressed { + /// The press direction, either down or up. + direction: PressDirection, + /// The button that was pressed. + button: PointerButton, + }, + /// The pointer has moved. + Moved { + /// How much the pointer moved from the previous position. + delta: Vec2, + }, + /// The pointer has been canceled. The OS can cause this to happen to touch events. + Canceled, +} + +/// An input event effecting a pointer. +#[derive(Event, Debug, Clone, Reflect)] +pub struct PointerInput { + /// The id of the pointer. + pub pointer_id: PointerId, + /// The location of the pointer. For [[`PointerAction::Moved`]], this is the location after the movement. + pub location: Location, + /// The action that the event describes. + pub action: PointerAction, +} + +impl PointerInput { + /// Creates a new pointer input event. + /// + /// Note that `location` refers to the position of the pointer *after* the event occurred. + pub fn new(pointer_id: PointerId, location: Location, action: PointerAction) -> PointerInput { + PointerInput { + pointer_id, + location, + action, + } + } + + /// Updates pointer entities according to the input events. + pub fn receive( + mut events: EventReader, + mut pointers: Query<(&PointerId, &mut PointerLocation, &mut PointerPress)>, + ) { + for event in events.read() { + match event.action { + PointerAction::Pressed { direction, button } => { + pointers + .iter_mut() + .for_each(|(pointer_id, _, mut pointer)| { + if *pointer_id == event.pointer_id { + let is_down = direction == PressDirection::Down; + match button { + PointerButton::Primary => pointer.primary = is_down, + PointerButton::Secondary => pointer.secondary = is_down, + PointerButton::Middle => pointer.middle = is_down, + } + } + }); + } + PointerAction::Moved { .. } => { + pointers.iter_mut().for_each(|(id, mut pointer, _)| { + if *id == event.pointer_id { + pointer.location = Some(event.location.to_owned()); + } + }); + } + _ => {} + } + } + } +} diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index 6159c6ec94698..8c922a9c3d10c 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -16,6 +16,7 @@ serialize = ["serde", "smol_str/serde", "bevy_ecs/serialize"] bevy_a11y = { path = "../bevy_a11y", version = "0.15.0-dev" } bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", 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", features = [ "glam", diff --git a/crates/bevy_window/src/event.rs b/crates/bevy_window/src/event.rs index 682ccde3bd80f..caf97be6f41e6 100644 --- a/crates/bevy_window/src/event.rs +++ b/crates/bevy_window/src/event.rs @@ -1,8 +1,13 @@ #![allow(deprecated)] use std::path::PathBuf; -use bevy_ecs::entity::Entity; -use bevy_ecs::event::Event; +use bevy_ecs::{entity::Entity, event::Event}; +use bevy_input::{ + gestures::*, + keyboard::{KeyboardFocusLost, KeyboardInput}, + mouse::{MouseButtonInput, MouseMotion, MouseWheel}, + touch::TouchInput, +}; use bevy_math::{IVec2, Vec2}; use bevy_reflect::Reflect; use smol_str::SmolStr; @@ -413,3 +418,193 @@ impl AppLifecycle { } } } + +/// Wraps all `bevy_window` and `bevy_input` events in a common enum. +/// +/// Read these events with `EventReader` if you need to +/// access window events in the order they were received from the +/// operating system. Otherwise, the event types are individually +/// readable with `EventReader` (e.g. `EventReader`). +#[derive(Event, Debug, Clone, PartialEq, Reflect)] +#[reflect(Debug, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[allow(missing_docs)] +pub enum WindowEvent { + AppLifecycle(AppLifecycle), + CursorEntered(CursorEntered), + CursorLeft(CursorLeft), + CursorMoved(CursorMoved), + FileDragAndDrop(FileDragAndDrop), + Ime(Ime), + ReceivedCharacter(ReceivedCharacter), + RequestRedraw(RequestRedraw), + WindowBackendScaleFactorChanged(WindowBackendScaleFactorChanged), + WindowCloseRequested(WindowCloseRequested), + WindowCreated(WindowCreated), + WindowDestroyed(WindowDestroyed), + WindowFocused(WindowFocused), + WindowMoved(WindowMoved), + WindowOccluded(WindowOccluded), + WindowResized(WindowResized), + WindowScaleFactorChanged(WindowScaleFactorChanged), + WindowThemeChanged(WindowThemeChanged), + + MouseButtonInput(MouseButtonInput), + MouseMotion(MouseMotion), + MouseWheel(MouseWheel), + + PinchGesture(PinchGesture), + RotationGesture(RotationGesture), + DoubleTapGesture(DoubleTapGesture), + PanGesture(PanGesture), + + TouchInput(TouchInput), + + KeyboardInput(KeyboardInput), + KeyboardFocusLost(KeyboardFocusLost), +} + +impl From for WindowEvent { + fn from(e: AppLifecycle) -> Self { + Self::AppLifecycle(e) + } +} +impl From for WindowEvent { + fn from(e: CursorEntered) -> Self { + Self::CursorEntered(e) + } +} +impl From for WindowEvent { + fn from(e: CursorLeft) -> Self { + Self::CursorLeft(e) + } +} +impl From for WindowEvent { + fn from(e: CursorMoved) -> Self { + Self::CursorMoved(e) + } +} +impl From for WindowEvent { + fn from(e: FileDragAndDrop) -> Self { + Self::FileDragAndDrop(e) + } +} +impl From for WindowEvent { + fn from(e: Ime) -> Self { + Self::Ime(e) + } +} +impl From for WindowEvent { + fn from(e: ReceivedCharacter) -> Self { + Self::ReceivedCharacter(e) + } +} +impl From for WindowEvent { + fn from(e: RequestRedraw) -> Self { + Self::RequestRedraw(e) + } +} +impl From for WindowEvent { + fn from(e: WindowBackendScaleFactorChanged) -> Self { + Self::WindowBackendScaleFactorChanged(e) + } +} +impl From for WindowEvent { + fn from(e: WindowCloseRequested) -> Self { + Self::WindowCloseRequested(e) + } +} +impl From for WindowEvent { + fn from(e: WindowCreated) -> Self { + Self::WindowCreated(e) + } +} +impl From for WindowEvent { + fn from(e: WindowDestroyed) -> Self { + Self::WindowDestroyed(e) + } +} +impl From for WindowEvent { + fn from(e: WindowFocused) -> Self { + Self::WindowFocused(e) + } +} +impl From for WindowEvent { + fn from(e: WindowMoved) -> Self { + Self::WindowMoved(e) + } +} +impl From for WindowEvent { + fn from(e: WindowOccluded) -> Self { + Self::WindowOccluded(e) + } +} +impl From for WindowEvent { + fn from(e: WindowResized) -> Self { + Self::WindowResized(e) + } +} +impl From for WindowEvent { + fn from(e: WindowScaleFactorChanged) -> Self { + Self::WindowScaleFactorChanged(e) + } +} +impl From for WindowEvent { + fn from(e: WindowThemeChanged) -> Self { + Self::WindowThemeChanged(e) + } +} +impl From for WindowEvent { + fn from(e: MouseButtonInput) -> Self { + Self::MouseButtonInput(e) + } +} +impl From for WindowEvent { + fn from(e: MouseMotion) -> Self { + Self::MouseMotion(e) + } +} +impl From for WindowEvent { + fn from(e: MouseWheel) -> Self { + Self::MouseWheel(e) + } +} +impl From for WindowEvent { + fn from(e: PinchGesture) -> Self { + Self::PinchGesture(e) + } +} +impl From for WindowEvent { + fn from(e: RotationGesture) -> Self { + Self::RotationGesture(e) + } +} +impl From for WindowEvent { + fn from(e: DoubleTapGesture) -> Self { + Self::DoubleTapGesture(e) + } +} +impl From for WindowEvent { + fn from(e: PanGesture) -> Self { + Self::PanGesture(e) + } +} +impl From for WindowEvent { + fn from(e: TouchInput) -> Self { + Self::TouchInput(e) + } +} +impl From for WindowEvent { + fn from(e: KeyboardInput) -> Self { + Self::KeyboardInput(e) + } +} +impl From for WindowEvent { + fn from(e: KeyboardFocusLost) -> Self { + Self::KeyboardFocusLost(e) + } +} diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index f27af26699424..fb0db3a27dd0d 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -91,7 +91,8 @@ impl Plugin for WindowPlugin { fn build(&self, app: &mut App) { // User convenience events #[allow(deprecated)] - app.add_event::() + app.add_event::() + .add_event::() .add_event::() .add_event::() .add_event::() @@ -143,7 +144,8 @@ impl Plugin for WindowPlugin { // Register event types #[allow(deprecated)] - app.register_type::() + app.register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 4901790ac7915..b70720deceb0c 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -13,7 +13,7 @@ //! See `winit_runner` for details. use bevy_derive::Deref; -use bevy_window::RawHandleWrapperHolder; +use bevy_window::{RawHandleWrapperHolder, WindowEvent}; use std::marker::PhantomData; use winit::event_loop::EventLoop; #[cfg(target_os = "android")] @@ -33,7 +33,6 @@ pub use winit::event_loop::EventLoopProxy; pub use winit::platform::web::CustomCursorExtWebSys; pub use winit::window::{CustomCursor as WinitCustomCursor, CustomCursorSource}; pub use winit_config::*; -pub use winit_event::*; pub use winit_windows::*; use crate::accessibility::{AccessKitAdapters, AccessKitPlugin, WinitActionRequestHandlers}; @@ -45,7 +44,6 @@ mod converters; mod state; mod system; mod winit_config; -pub mod winit_event; mod winit_monitors; mod winit_windows; @@ -122,7 +120,6 @@ impl Plugin for WinitPlugin { app.init_non_send_resource::() .init_resource::() .init_resource::() - .add_event::() .set_runner(winit_runner::) .add_systems( Last, @@ -162,12 +159,12 @@ pub struct WakeUp; pub struct EventLoopProxyWrapper(EventLoopProxy); trait AppSendEvent { - fn send(&mut self, event: impl Into); + fn send(&mut self, event: impl Into); } -impl AppSendEvent for Vec { - fn send(&mut self, event: impl Into) { - self.push(Into::::into(event)); +impl AppSendEvent for Vec { + fn send(&mut self, event: impl Into) { + self.push(Into::::into(event)); } } diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index 871a559b060a8..f515513a325b6 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -28,8 +28,8 @@ use winit::window::WindowId; use bevy_window::{ AppLifecycle, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, ReceivedCharacter, RequestRedraw, Window, WindowBackendScaleFactorChanged, WindowCloseRequested, WindowDestroyed, - WindowFocused, WindowMoved, WindowOccluded, WindowResized, WindowScaleFactorChanged, - WindowThemeChanged, + WindowEvent as BevyWindowEvent, WindowFocused, WindowMoved, WindowOccluded, WindowResized, + WindowScaleFactorChanged, WindowThemeChanged, }; #[cfg(target_os = "android")] use bevy_window::{PrimaryWindow, RawHandleWrapper}; @@ -38,7 +38,7 @@ use crate::accessibility::AccessKitAdapters; use crate::system::{create_monitors, CachedWindow}; use crate::{ converters, create_windows, AppSendEvent, CreateMonitorParams, CreateWindowParams, - EventLoopProxyWrapper, UpdateMode, WinitEvent, WinitSettings, WinitWindows, + EventLoopProxyWrapper, UpdateMode, WinitSettings, WinitWindows, }; /// Persistent state that is used to run the [`App`] according to the current @@ -69,8 +69,8 @@ struct WinitAppRunnerState { lifecycle: AppLifecycle, /// The previous app lifecycle state. previous_lifecycle: AppLifecycle, - /// Winit events to send - winit_events: Vec, + /// Bevy window events to send + bevy_window_events: Vec, _marker: PhantomData, event_writer_system_state: SystemState<( @@ -110,7 +110,7 @@ impl WinitAppRunnerState { wait_elapsed: false, // 3 seems to be enough, 5 is a safe margin startup_forced_updates: 5, - winit_events: Vec::new(), + bevy_window_events: Vec::new(), _marker: PhantomData, event_writer_system_state, } @@ -258,7 +258,9 @@ impl ApplicationHandler for WinitAppRunnerState { &mut window_scale_factor_changed, ); } - WindowEvent::CloseRequested => self.winit_events.send(WindowCloseRequested { window }), + WindowEvent::CloseRequested => self + .bevy_window_events + .send(WindowCloseRequested { window }), WindowEvent::KeyboardInput { ref event, is_synthetic, @@ -274,10 +276,11 @@ impl ApplicationHandler for WinitAppRunnerState { if let Some(char) = &event.text { let char = char.clone(); #[allow(deprecated)] - self.winit_events.send(ReceivedCharacter { window, char }); + self.bevy_window_events + .send(ReceivedCharacter { window, char }); } } - self.winit_events + self.bevy_window_events .send(converters::convert_keyboard_input(event, window)); } } @@ -291,44 +294,44 @@ impl ApplicationHandler for WinitAppRunnerState { win.set_physical_cursor_position(Some(physical_position)); let position = (physical_position / win.resolution.scale_factor() as f64).as_vec2(); - self.winit_events.send(CursorMoved { + self.bevy_window_events.send(CursorMoved { window, position, delta, }); } WindowEvent::CursorEntered { .. } => { - self.winit_events.send(CursorEntered { window }); + self.bevy_window_events.send(CursorEntered { window }); } WindowEvent::CursorLeft { .. } => { win.set_physical_cursor_position(None); - self.winit_events.send(CursorLeft { window }); + self.bevy_window_events.send(CursorLeft { window }); } WindowEvent::MouseInput { state, button, .. } => { - self.winit_events.send(MouseButtonInput { + self.bevy_window_events.send(MouseButtonInput { button: converters::convert_mouse_button(button), state: converters::convert_element_state(state), window, }); } WindowEvent::PinchGesture { delta, .. } => { - self.winit_events.send(PinchGesture(delta as f32)); + self.bevy_window_events.send(PinchGesture(delta as f32)); } WindowEvent::RotationGesture { delta, .. } => { - self.winit_events.send(RotationGesture(delta)); + self.bevy_window_events.send(RotationGesture(delta)); } WindowEvent::DoubleTapGesture { .. } => { - self.winit_events.send(DoubleTapGesture); + self.bevy_window_events.send(DoubleTapGesture); } WindowEvent::PanGesture { delta, .. } => { - self.winit_events.send(PanGesture(Vec2 { + self.bevy_window_events.send(PanGesture(Vec2 { x: delta.x, y: delta.y, })); } WindowEvent::MouseWheel { delta, .. } => match delta { event::MouseScrollDelta::LineDelta(x, y) => { - self.winit_events.send(MouseWheel { + self.bevy_window_events.send(MouseWheel { unit: MouseScrollUnit::Line, x, y, @@ -336,7 +339,7 @@ impl ApplicationHandler for WinitAppRunnerState { }); } event::MouseScrollDelta::PixelDelta(p) => { - self.winit_events.send(MouseWheel { + self.bevy_window_events.send(MouseWheel { unit: MouseScrollUnit::Pixel, x: p.x as f32, y: p.y as f32, @@ -348,62 +351,65 @@ impl ApplicationHandler for WinitAppRunnerState { let location = touch .location .to_logical(win.resolution.scale_factor() as f64); - self.winit_events + self.bevy_window_events .send(converters::convert_touch_input(touch, location, window)); } WindowEvent::Focused(focused) => { win.focused = focused; - self.winit_events.send(WindowFocused { window, focused }); + self.bevy_window_events + .send(WindowFocused { window, focused }); if !focused { - self.winit_events.send(KeyboardFocusLost); + self.bevy_window_events.send(KeyboardFocusLost); } } WindowEvent::Occluded(occluded) => { - self.winit_events.send(WindowOccluded { window, occluded }); + self.bevy_window_events + .send(WindowOccluded { window, occluded }); } WindowEvent::DroppedFile(path_buf) => { - self.winit_events + self.bevy_window_events .send(FileDragAndDrop::DroppedFile { window, path_buf }); } WindowEvent::HoveredFile(path_buf) => { - self.winit_events + self.bevy_window_events .send(FileDragAndDrop::HoveredFile { window, path_buf }); } WindowEvent::HoveredFileCancelled => { - self.winit_events + self.bevy_window_events .send(FileDragAndDrop::HoveredFileCanceled { window }); } WindowEvent::Moved(position) => { let position = ivec2(position.x, position.y); win.position.set(position); - self.winit_events.send(WindowMoved { window, position }); + self.bevy_window_events + .send(WindowMoved { window, position }); } WindowEvent::Ime(event) => match event { event::Ime::Preedit(value, cursor) => { - self.winit_events.send(Ime::Preedit { + self.bevy_window_events.send(Ime::Preedit { window, value, cursor, }); } event::Ime::Commit(value) => { - self.winit_events.send(Ime::Commit { window, value }); + self.bevy_window_events.send(Ime::Commit { window, value }); } event::Ime::Enabled => { - self.winit_events.send(Ime::Enabled { window }); + self.bevy_window_events.send(Ime::Enabled { window }); } event::Ime::Disabled => { - self.winit_events.send(Ime::Disabled { window }); + self.bevy_window_events.send(Ime::Disabled { window }); } }, WindowEvent::ThemeChanged(theme) => { - self.winit_events.send(WindowThemeChanged { + self.bevy_window_events.send(WindowThemeChanged { window, theme: converters::convert_winit_theme(theme), }); } WindowEvent::Destroyed => { - self.winit_events.send(WindowDestroyed { window }); + self.bevy_window_events.send(WindowDestroyed { window }); } WindowEvent::RedrawRequested => { self.ran_update_since_last_redraw = false; @@ -429,7 +435,7 @@ impl ApplicationHandler for WinitAppRunnerState { if let DeviceEvent::MouseMotion { delta: (x, y) } = event { let delta = Vec2::new(x as f32, y as f32); - self.winit_events.send(MouseMotion { delta }); + self.bevy_window_events.send(MouseMotion { delta }); } } @@ -534,7 +540,7 @@ impl ApplicationHandler for WinitAppRunnerState { // Notifies a lifecycle change if self.lifecycle != self.previous_lifecycle { self.previous_lifecycle = self.lifecycle; - self.winit_events.send(self.lifecycle); + self.bevy_window_events.send(self.lifecycle); } // This is recorded before running app.update(), to run the next cycle after a correct timeout. @@ -674,15 +680,15 @@ impl WinitAppRunnerState { fn run_app_update(&mut self) { self.reset_on_update(); - self.forward_winit_events(); + self.forward_bevy_events(); if self.app.plugins_state() == PluginsState::Cleaned { self.app.update(); } } - fn forward_winit_events(&mut self) { - let buffered_events = self.winit_events.drain(..).collect::>(); + fn forward_bevy_events(&mut self) { + let buffered_events = self.bevy_window_events.drain(..).collect::>(); if buffered_events.is_empty() { return; @@ -692,95 +698,95 @@ impl WinitAppRunnerState { for winit_event in buffered_events.iter() { match winit_event.clone() { - WinitEvent::AppLifecycle(e) => { + BevyWindowEvent::AppLifecycle(e) => { world.send_event(e); } - WinitEvent::CursorEntered(e) => { + BevyWindowEvent::CursorEntered(e) => { world.send_event(e); } - WinitEvent::CursorLeft(e) => { + BevyWindowEvent::CursorLeft(e) => { world.send_event(e); } - WinitEvent::CursorMoved(e) => { + BevyWindowEvent::CursorMoved(e) => { world.send_event(e); } - WinitEvent::FileDragAndDrop(e) => { + BevyWindowEvent::FileDragAndDrop(e) => { world.send_event(e); } - WinitEvent::Ime(e) => { + BevyWindowEvent::Ime(e) => { world.send_event(e); } - WinitEvent::ReceivedCharacter(e) => { + BevyWindowEvent::ReceivedCharacter(e) => { world.send_event(e); } - WinitEvent::RequestRedraw(e) => { + BevyWindowEvent::RequestRedraw(e) => { world.send_event(e); } - WinitEvent::WindowBackendScaleFactorChanged(e) => { + BevyWindowEvent::WindowBackendScaleFactorChanged(e) => { world.send_event(e); } - WinitEvent::WindowCloseRequested(e) => { + BevyWindowEvent::WindowCloseRequested(e) => { world.send_event(e); } - WinitEvent::WindowCreated(e) => { + BevyWindowEvent::WindowCreated(e) => { world.send_event(e); } - WinitEvent::WindowDestroyed(e) => { + BevyWindowEvent::WindowDestroyed(e) => { world.send_event(e); } - WinitEvent::WindowFocused(e) => { + BevyWindowEvent::WindowFocused(e) => { world.send_event(e); } - WinitEvent::WindowMoved(e) => { + BevyWindowEvent::WindowMoved(e) => { world.send_event(e); } - WinitEvent::WindowOccluded(e) => { + BevyWindowEvent::WindowOccluded(e) => { world.send_event(e); } - WinitEvent::WindowResized(e) => { + BevyWindowEvent::WindowResized(e) => { world.send_event(e); } - WinitEvent::WindowScaleFactorChanged(e) => { + BevyWindowEvent::WindowScaleFactorChanged(e) => { world.send_event(e); } - WinitEvent::WindowThemeChanged(e) => { + BevyWindowEvent::WindowThemeChanged(e) => { world.send_event(e); } - WinitEvent::MouseButtonInput(e) => { + BevyWindowEvent::MouseButtonInput(e) => { world.send_event(e); } - WinitEvent::MouseMotion(e) => { + BevyWindowEvent::MouseMotion(e) => { world.send_event(e); } - WinitEvent::MouseWheel(e) => { + BevyWindowEvent::MouseWheel(e) => { world.send_event(e); } - WinitEvent::PinchGesture(e) => { + BevyWindowEvent::PinchGesture(e) => { world.send_event(e); } - WinitEvent::RotationGesture(e) => { + BevyWindowEvent::RotationGesture(e) => { world.send_event(e); } - WinitEvent::DoubleTapGesture(e) => { + BevyWindowEvent::DoubleTapGesture(e) => { world.send_event(e); } - WinitEvent::PanGesture(e) => { + BevyWindowEvent::PanGesture(e) => { world.send_event(e); } - WinitEvent::TouchInput(e) => { + BevyWindowEvent::TouchInput(e) => { world.send_event(e); } - WinitEvent::KeyboardInput(e) => { + BevyWindowEvent::KeyboardInput(e) => { world.send_event(e); } - WinitEvent::KeyboardFocusLost(e) => { + BevyWindowEvent::KeyboardFocusLost(e) => { world.send_event(e); } } } world - .resource_mut::>() + .resource_mut::>() .send_batch(buffered_events); } diff --git a/crates/bevy_winit/src/winit_event.rs b/crates/bevy_winit/src/winit_event.rs deleted file mode 100644 index 9244b1265db73..0000000000000 --- a/crates/bevy_winit/src/winit_event.rs +++ /dev/null @@ -1,209 +0,0 @@ -#![allow(deprecated)] -#![allow(missing_docs)] - -use bevy_ecs::prelude::*; -use bevy_input::keyboard::KeyboardInput; -use bevy_input::touch::TouchInput; -use bevy_input::{ - gestures::*, - keyboard::KeyboardFocusLost, - mouse::{MouseButtonInput, MouseMotion, MouseWheel}, -}; -use bevy_reflect::Reflect; -#[cfg(feature = "serialize")] -use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -use bevy_window::{ - AppLifecycle, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, ReceivedCharacter, - RequestRedraw, WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, - WindowDestroyed, WindowFocused, WindowMoved, WindowOccluded, WindowResized, - WindowScaleFactorChanged, WindowThemeChanged, -}; - -/// Wraps all `bevy_window` events in a common enum. -/// -/// Read these events with `EventReader` if you need to -/// access window events in the order they were received from `winit`. -/// Otherwise, the event types are individually readable with -/// `EventReader` (e.g. `EventReader`). -#[derive(Event, Debug, Clone, PartialEq, Reflect)] -#[reflect(Debug, PartialEq)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -pub enum WinitEvent { - AppLifecycle(AppLifecycle), - CursorEntered(CursorEntered), - CursorLeft(CursorLeft), - CursorMoved(CursorMoved), - FileDragAndDrop(FileDragAndDrop), - Ime(Ime), - ReceivedCharacter(ReceivedCharacter), - RequestRedraw(RequestRedraw), - WindowBackendScaleFactorChanged(WindowBackendScaleFactorChanged), - WindowCloseRequested(WindowCloseRequested), - WindowCreated(WindowCreated), - WindowDestroyed(WindowDestroyed), - WindowFocused(WindowFocused), - WindowMoved(WindowMoved), - WindowOccluded(WindowOccluded), - WindowResized(WindowResized), - WindowScaleFactorChanged(WindowScaleFactorChanged), - WindowThemeChanged(WindowThemeChanged), - - MouseButtonInput(MouseButtonInput), - MouseMotion(MouseMotion), - MouseWheel(MouseWheel), - - PinchGesture(PinchGesture), - RotationGesture(RotationGesture), - DoubleTapGesture(DoubleTapGesture), - PanGesture(PanGesture), - - TouchInput(TouchInput), - - KeyboardInput(KeyboardInput), - KeyboardFocusLost(KeyboardFocusLost), -} - -impl From for WinitEvent { - fn from(e: AppLifecycle) -> Self { - Self::AppLifecycle(e) - } -} -impl From for WinitEvent { - fn from(e: CursorEntered) -> Self { - Self::CursorEntered(e) - } -} -impl From for WinitEvent { - fn from(e: CursorLeft) -> Self { - Self::CursorLeft(e) - } -} -impl From for WinitEvent { - fn from(e: CursorMoved) -> Self { - Self::CursorMoved(e) - } -} -impl From for WinitEvent { - fn from(e: FileDragAndDrop) -> Self { - Self::FileDragAndDrop(e) - } -} -impl From for WinitEvent { - fn from(e: Ime) -> Self { - Self::Ime(e) - } -} -impl From for WinitEvent { - fn from(e: ReceivedCharacter) -> Self { - Self::ReceivedCharacter(e) - } -} -impl From for WinitEvent { - fn from(e: RequestRedraw) -> Self { - Self::RequestRedraw(e) - } -} -impl From for WinitEvent { - fn from(e: WindowBackendScaleFactorChanged) -> Self { - Self::WindowBackendScaleFactorChanged(e) - } -} -impl From for WinitEvent { - fn from(e: WindowCloseRequested) -> Self { - Self::WindowCloseRequested(e) - } -} -impl From for WinitEvent { - fn from(e: WindowCreated) -> Self { - Self::WindowCreated(e) - } -} -impl From for WinitEvent { - fn from(e: WindowDestroyed) -> Self { - Self::WindowDestroyed(e) - } -} -impl From for WinitEvent { - fn from(e: WindowFocused) -> Self { - Self::WindowFocused(e) - } -} -impl From for WinitEvent { - fn from(e: WindowMoved) -> Self { - Self::WindowMoved(e) - } -} -impl From for WinitEvent { - fn from(e: WindowOccluded) -> Self { - Self::WindowOccluded(e) - } -} -impl From for WinitEvent { - fn from(e: WindowResized) -> Self { - Self::WindowResized(e) - } -} -impl From for WinitEvent { - fn from(e: WindowScaleFactorChanged) -> Self { - Self::WindowScaleFactorChanged(e) - } -} -impl From for WinitEvent { - fn from(e: WindowThemeChanged) -> Self { - Self::WindowThemeChanged(e) - } -} -impl From for WinitEvent { - fn from(e: MouseButtonInput) -> Self { - Self::MouseButtonInput(e) - } -} -impl From for WinitEvent { - fn from(e: MouseMotion) -> Self { - Self::MouseMotion(e) - } -} -impl From for WinitEvent { - fn from(e: MouseWheel) -> Self { - Self::MouseWheel(e) - } -} -impl From for WinitEvent { - fn from(e: PinchGesture) -> Self { - Self::PinchGesture(e) - } -} -impl From for WinitEvent { - fn from(e: RotationGesture) -> Self { - Self::RotationGesture(e) - } -} -impl From for WinitEvent { - fn from(e: DoubleTapGesture) -> Self { - Self::DoubleTapGesture(e) - } -} -impl From for WinitEvent { - fn from(e: PanGesture) -> Self { - Self::PanGesture(e) - } -} -impl From for WinitEvent { - fn from(e: TouchInput) -> Self { - Self::TouchInput(e) - } -} -impl From for WinitEvent { - fn from(e: KeyboardInput) -> Self { - Self::KeyboardInput(e) - } -} -impl From for WinitEvent { - fn from(e: KeyboardFocusLost) -> Self { - Self::KeyboardFocusLost(e) - } -}