diff --git a/Cargo.toml b/Cargo.toml index 6c7361f723631..f50e38dea46e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3771,6 +3771,19 @@ category = "Picking" wasm = true required-features = ["bevy_sprite_picking_backend"] +[[example]] +name = "picking_interaction" +path = "examples/picking/interaction.rs" +doc-scrape-examples = true +required-features = ["bevy_sprite_picking_backend"] + +[package.metadata.example.picking_interaction] +name = "Picking Interaction" +description = "Demonstrates how to use the PickingInteraction component without events or observers." +category = "Picking" +wasm = true +required-features = ["bevy_sprite_picking_backend"] + [[example]] name = "animation_masks" path = "examples/animation/animation_masks.rs" diff --git a/crates/bevy_picking/Cargo.toml b/crates/bevy_picking/Cargo.toml index d4731e001b0cb..ca569324b1c70 100644 --- a/crates/bevy_picking/Cargo.toml +++ b/crates/bevy_picking/Cargo.toml @@ -29,6 +29,7 @@ bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } crossbeam-channel = { version = "0.5", optional = true } uuid = { version = "1.1", features = ["v4"] } +bitflags = { version = "2.3", features = ["serde"] } [lints] workspace = true diff --git a/crates/bevy_picking/src/focus.rs b/crates/bevy_picking/src/focus.rs index d07c0c095f4bc..01e6f0079c81a 100644 --- a/crates/bevy_picking/src/focus.rs +++ b/crates/bevy_picking/src/focus.rs @@ -5,6 +5,7 @@ use alloc::collections::BTreeMap; use core::fmt::Debug; +use core::ops::{BitOr, BitOrAssign}; use std::collections::HashSet; use crate::{ @@ -190,9 +191,10 @@ fn build_hover_map( /// it will be considered hovered. #[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] #[reflect(Component, Default, PartialEq, Debug)] +#[repr(u8)] pub enum PickingInteraction { /// The entity is being pressed down by a pointer. - Pressed = 2, + Pressed(PressedButtons) = 2, /// The entity is being hovered by a pointer. Hovered = 1, /// No pointers are interacting with this entity. @@ -200,6 +202,70 @@ pub enum PickingInteraction { None = 0, } +impl BitOr for PickingInteraction { + type Output = Self; + + fn bitor(mut self, rhs: Self) -> PickingInteraction { + self |= rhs; + self + } +} + +impl BitOrAssign for PickingInteraction { + fn bitor_assign(&mut self, rhs: Self) { + use PickingInteraction::*; + match (self, rhs) { + (Pressed(a), Pressed(b)) => *a |= b, + (Pressed(_), _) => (), + (a, Pressed(b)) => *a = Pressed(b), + (a @ None, Hovered) => *a = Hovered, + _ => (), + } + } +} + +bitflags::bitflags! { + #[repr(transparent)] + #[derive(Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)] + #[reflect(opaque)] + #[reflect(Hash, PartialEq, Debug)] + /// Button pressed in a [`PickingInteraction`] + pub struct PressedButtons: u8 { + /// Left mouse button is pressed. + const LEFT = 1; + /// Right mouse button is pressed. + const RIGHT = 2; + /// Middle mouse button is pressed. + const MIDDLE = 4; + /// Touch input is pressed. + const TOUCH = 8; + /// Custom input is pressed. + const CUSTOM = 16; + /// X1 or back, reserved, currently does nothing. + const X1 = 32; + /// X2 or forward, reserved, currently does nothing. + const X2 = 64; + /// Left mouse button or touch input is pressed. + const LEFT_OR_TOUCH = 1|8; + } +} + +impl From for PressedButtons { + fn from(value: PointerPress) -> Self { + let mut result = PressedButtons::empty(); + if value.is_primary_pressed() { + result |= PressedButtons::LEFT; + } + if value.is_secondary_pressed() { + result |= PressedButtons::RIGHT; + } + if value.is_middle_pressed() { + result |= PressedButtons::MIDDLE; + } + result + } +} + /// Uses pointer events to update [`PointerInteraction`] and [`PickingInteraction`] components. pub fn update_interactions( // Input @@ -235,7 +301,12 @@ pub fn update_interactions( pointer_interaction.sorted_entities = sorted_entities; for hovered_entity in pointers_hovered_entities.iter().map(|(entity, _)| entity) { - merge_interaction_states(pointer_press, hovered_entity, &mut new_interaction_state); + merge_interaction_states( + pointer, + pointer_press, + hovered_entity, + &mut new_interaction_state, + ); } } } @@ -252,28 +323,55 @@ pub fn update_interactions( /// Merge the interaction state of this entity into the aggregated map. fn merge_interaction_states( + id: &PointerId, pointer_press: &PointerPress, hovered_entity: &Entity, new_interaction_state: &mut HashMap, ) { - let new_interaction = match pointer_press.is_any_pressed() { - true => PickingInteraction::Pressed, - false => PickingInteraction::Hovered, + let new_interaction = if !pointer_press.is_any_pressed() { + PickingInteraction::Hovered + } else { + match id { + PointerId::Mouse => PickingInteraction::Pressed((*pointer_press).into()), + PointerId::Touch(_) => PickingInteraction::Pressed(PressedButtons::TOUCH), + PointerId::Custom(_) => PickingInteraction::Pressed(PressedButtons::CUSTOM), + } }; + new_interaction_state + .entry(*hovered_entity) + .and_modify(|old| *old |= new_interaction) + .or_insert(new_interaction); +} - if let Some(old_interaction) = new_interaction_state.get_mut(hovered_entity) { - // Only update if the new value has a higher precedence than the old value. - if *old_interaction != new_interaction - && matches!( - (*old_interaction, new_interaction), - (PickingInteraction::Hovered, PickingInteraction::Pressed) - | (PickingInteraction::None, PickingInteraction::Pressed) - | (PickingInteraction::None, PickingInteraction::Hovered) - ) - { - *old_interaction = new_interaction; - } - } else { - new_interaction_state.insert(*hovered_entity, new_interaction); +#[cfg(test)] +mod test { + use crate::focus::{PickingInteraction::*, PressedButtons}; + + #[test] + fn merge_interaction() { + assert_eq!( + Pressed(PressedButtons::LEFT) | Pressed(PressedButtons::RIGHT), + Pressed(PressedButtons::LEFT | PressedButtons::RIGHT) + ); + assert_eq!( + Pressed(PressedButtons::LEFT) | Hovered, + Pressed(PressedButtons::LEFT) + ); + assert_eq!( + Hovered | Pressed(PressedButtons::LEFT), + Pressed(PressedButtons::LEFT) + ); + assert_eq!( + Pressed(PressedButtons::LEFT) | None, + Pressed(PressedButtons::LEFT) + ); + assert_eq!( + None | Pressed(PressedButtons::LEFT), + Pressed(PressedButtons::LEFT) + ); + assert_eq!(Hovered | None, Hovered); + assert_eq!(None | Hovered, Hovered); + assert_eq!(Hovered | Hovered, Hovered); + assert_eq!(None | None, None); } } diff --git a/crates/bevy_picking/src/pointer.rs b/crates/bevy_picking/src/pointer.rs index 7da790c26cb82..f4e177ccb8ade 100644 --- a/crates/bevy_picking/src/pointer.rs +++ b/crates/bevy_picking/src/pointer.rs @@ -108,7 +108,7 @@ pub fn update_pointer_map(pointers: Query<(Entity, &PointerId)>, mut map: ResMut } /// Tracks the state of the pointer's buttons in response to [`PointerInput`] events. -#[derive(Debug, Default, Clone, Component, Reflect, PartialEq, Eq)] +#[derive(Debug, Default, Clone, Copy, Component, Reflect, PartialEq, Eq)] #[reflect(Component, Default, Debug, PartialEq)] pub struct PointerPress { primary: bool, diff --git a/examples/README.md b/examples/README.md index 3b6359ad71032..649bf34b4eaff 100644 --- a/examples/README.md +++ b/examples/README.md @@ -377,6 +377,7 @@ Example | Description Example | Description --- | --- [Mesh Picking](../examples/picking/mesh_picking.rs) | Demonstrates picking meshes +[Picking Interaction](../examples/picking/interaction.rs) | Demonstrates how to use the PickingInteraction component without events or observers. [Showcases simple picking events and usage](../examples/picking/simple_picking.rs) | Demonstrates how to use picking events to spawn simple objects [Sprite Picking](../examples/picking/sprite_picking.rs) | Demonstrates picking sprites and sprite atlases diff --git a/examples/picking/interaction.rs b/examples/picking/interaction.rs new file mode 100644 index 0000000000000..2454c190a5055 --- /dev/null +++ b/examples/picking/interaction.rs @@ -0,0 +1,79 @@ +//! Demonstrates how to use `PickingInteraction` without using events and observers. + +use bevy::{ + picking::focus::{PickingInteraction, PressedButtons}, + prelude::*, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) + .add_systems(Startup, setup) + .add_systems(Update, (move_sprite, picking)) + .run(); +} + +// Move the sprite for variety. +fn move_sprite(time: Res