Skip to content

Added PressedButtons to PickingInteraction::Pressed. #16004

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_picking/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
136 changes: 117 additions & 19 deletions crates/bevy_picking/src/focus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use alloc::collections::BTreeMap;
use core::fmt::Debug;
use core::ops::{BitOr, BitOrAssign};
use std::collections::HashSet;

use crate::{
Expand Down Expand Up @@ -190,16 +191,81 @@ 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.
#[default]
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is correct. Touch inputs are probably akin to a primary press, and some touch devices, like pens, can have a primary secondary, tertiary press, just like a mouse.

Copy link
Contributor

@BenjaminBrienen BenjaminBrienen Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had something like that before: my Note 9 has a pen and it supports hovering over something with the pen in the air and clicking a side-button to perform a secondary action.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A stylus is not a touch pointer. Winit does not currently support stylus input, so neither do we.

Copy link
Member

@aevyrie aevyrie Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It definitely works, I've tested it. It's treated like a touch input. This was on a MS surface with a surface pen.

Regardless, if you are matching on whether a button was pressed, why would you not want it to be considered pressed if it was touched by a different pointing device?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why I specifically used the wording "primary/secondary". A primary press can come from many types of devices.

Copy link
Contributor Author

@mintlu8 mintlu8 Oct 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR just maps the input from picking in a form that makes the most sense to me. Definitely would like to see more detailed information and more buttons, but that has to come from picking first.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to left and right.

/// 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<PointerPress> 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
Expand Down Expand Up @@ -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,
);
}
}
}
Expand All @@ -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<Entity, PickingInteraction>,
) {
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),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See here for the mapping from Touch. I think that this is the right approach for the current PR.

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);
}
}
2 changes: 1 addition & 1 deletion crates/bevy_picking/src/pointer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
79 changes: 79 additions & 0 deletions examples/picking/interaction.rs
Original file line number Diff line number Diff line change
@@ -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<Time>, mut sprite: Query<&mut Transform, With<Sprite>>) {
let t = time.elapsed_secs() * 0.1;
for mut transform in &mut sprite {
let new = Vec2 {
x: 50.0 * ops::sin(t),
y: 50.0 * ops::sin(t * 2.0),
};
transform.translation.x = new.x;
transform.translation.y = new.y;
}
}

// Display the current picking state.
fn picking(sprite: Query<(&PickingInteraction, &Children)>, mut text: Query<&mut Text2d>) {
for (interaction, children) in sprite.iter() {
let mut iter = text.iter_many_mut(children);
while let Some(mut text) = iter.fetch_next() {
match interaction {
PickingInteraction::Pressed(pressed_buttons)
if pressed_buttons.contains(PressedButtons::LEFT) =>
{
text.0 = "Left Clicked!".into();
}
PickingInteraction::Pressed(pressed_buttons)
if pressed_buttons.contains(PressedButtons::RIGHT) =>
{
text.0 = "Right Clicked!".into();
}
PickingInteraction::Pressed(pressed_buttons)
if pressed_buttons.contains(PressedButtons::TOUCH) =>
{
text.0 = "Touched!".into();
}
// We choose to ignore other mouse buttons like MMB,
// in this pattern we treat them as hover.
PickingInteraction::Hovered | PickingInteraction::Pressed(..) => {
text.0 = "Hovered!".into();
}
PickingInteraction::None => {
text.0 = "Hover Me!".into();
}
}
}
}
}

/// Set up the scene.
fn setup(mut commands: Commands) {
commands.spawn(Camera2d);

commands
.spawn((
Sprite {
custom_size: Some(Vec2::new(200., 50.)),
color: Color::BLACK,
..Default::default()
},
PickingInteraction::None,
))
.with_children(|s| {
s.spawn((Text2d::new("Hover Me!"), Transform::from_xyz(0., 0., 1.)));
});
}