diff --git a/backends/bevy_picking_sprite/src/lib.rs b/backends/bevy_picking_sprite/src/lib.rs index 78e88e39..1a51889e 100644 --- a/backends/bevy_picking_sprite/src/lib.rs +++ b/backends/bevy_picking_sprite/src/lib.rs @@ -52,7 +52,7 @@ pub fn sprite_picking( pointer_location.location().map(|loc| (pointer, loc)) }) { let mut blocked = false; - let (cam_entity, camera, cam_transform) = cameras + let Some((cam_entity, camera, cam_transform)) = cameras .iter() .find(|(_, camera, _)| { camera @@ -60,12 +60,14 @@ pub fn sprite_picking( .normalize(Some(primary_window.single())) .unwrap() == location.target - }) - .unwrap_or_else(|| panic!("No camera found associated with pointer {:?}.", pointer)); + }) else { + continue; + }; - let Some(cursor_pos_world) = camera.viewport_to_world_2d(cam_transform, location.position) else { - continue; - }; + let Some(cursor_pos_world) = + camera.viewport_to_world_2d(cam_transform, location.position) else { + continue; + }; let picks: Vec<(Entity, HitData)> = sorted_sprites .iter() diff --git a/backends/bevy_picking_ui/src/lib.rs b/backends/bevy_picking_ui/src/lib.rs index 0d85ca99..d02b2002 100644 --- a/backends/bevy_picking_ui/src/lib.rs +++ b/backends/bevy_picking_ui/src/lib.rs @@ -4,8 +4,13 @@ #![allow(clippy::too_many_arguments)] #![deny(missing_docs)] -use bevy::ui::{self, FocusPolicy}; -use bevy::{prelude::*, render::camera::NormalizedRenderTarget, window::PrimaryWindow}; +use bevy::{ + ecs::query::WorldQuery, + prelude::*, + render::camera::NormalizedRenderTarget, + ui::{FocusPolicy, RelativeCursorPosition, UiStack}, + window::PrimaryWindow, +}; use bevy_picking_core::backend::prelude::*; /// Commonly used imports for the [`bevy_picking_ui`](crate) crate. @@ -23,21 +28,30 @@ impl Plugin for BevyUiBackend { } } -/// Computes the UI node entities under each pointer +/// Main query for [`ui_focus_system`] +#[derive(WorldQuery)] +#[world_query(mutable)] +pub struct NodeQuery { + entity: Entity, + node: &'static Node, + global_transform: &'static GlobalTransform, + interaction: Option<&'static mut Interaction>, + relative_cursor_position: Option<&'static mut RelativeCursorPosition>, + focus_policy: Option<&'static FocusPolicy>, + calculated_clip: Option<&'static CalculatedClip>, + computed_visibility: Option<&'static ComputedVisibility>, +} + +/// Computes the UI node entities under each pointer. +/// +/// Bevy's [`UiStack`] orders all nodes in the order they will be rendered, which is the same order +/// we need for determining picking. pub fn ui_picking( pointers: Query<(&PointerId, &PointerLocation)>, - cameras: Query<(Entity, &Camera)>, - primary_window: Query>, - mut node_query: Query< - ( - Entity, - &ui::Node, - &GlobalTransform, - &FocusPolicy, - Option<&CalculatedClip>, - ), - Without, - >, + cameras: Query<(Entity, &Camera, Option<&UiCameraConfig>)>, + primary_window: Query<(Entity, &Window), With>, + ui_stack: Res, + mut node_query: Query, mut output: EventWriter, ) { for (pointer, location) in pointers.iter().filter_map(|(pointer, pointer_location)| { @@ -54,58 +68,94 @@ pub fn ui_picking( }) .map(|loc| (pointer, loc)) }) { - let camera = cameras + let (window_entity, window) = primary_window.single(); + let Some((camera, ui_config)) = cameras .iter() - .find(|(_entity, camera)| { - camera - .target - .normalize(Some(primary_window.single())) - .unwrap() - == location.target + .find(|(_entity, camera, _)| { + camera.target.normalize(Some(window_entity)).unwrap() == location.target }) - .map(|(entity, _camera)| entity) - .unwrap_or_else(|| panic!("No camera found associated with pointer {:?}.", pointer)); + .map(|(entity, _camera, ui_config)| (entity, ui_config)) else { + continue; + }; - let cursor_position = location.position; - let mut blocked = false; + if matches!(ui_config, Some(&UiCameraConfig { show_ui: false, .. })) { + return; + } - let over_list = node_query - .iter_mut() - .filter_map(|(entity, node, global_transform, focus, clip)| { - if blocked { - return None; - } + let mut cursor_position = location.position; + cursor_position.y = window.resolution.height() - cursor_position.y; - blocked = *focus == FocusPolicy::Block; + let mut hovered_nodes = ui_stack + .uinodes + .iter() + // reverse the iterator to traverse the tree from closest nodes to furthest + .rev() + .filter_map(|entity| { + if let Ok(node) = node_query.get_mut(*entity) { + // Nodes that are not rendered should not be interactable + if let Some(computed_visibility) = node.computed_visibility { + if !computed_visibility.is_visible() { + return None; + } + } - let position = global_transform.translation(); - let ui_position = position.truncate(); - let extents = node.size() / 2.0; - let mut min = ui_position - extents; - let mut max = ui_position + extents; - if let Some(clip) = clip { - min = min.max(clip.clip.min); - max = Vec2::min(max, clip.clip.max); - } + let position = node.global_transform.translation(); + let ui_position = position.truncate(); + let extents = node.node.size() / 2.0; + let mut min = ui_position - extents; + if let Some(clip) = node.calculated_clip { + min = Vec2::max(min, clip.clip.min); + } - let contains_cursor = (min.x..max.x).contains(&cursor_position.x) - && (min.y..max.y).contains(&cursor_position.y); + // The mouse position relative to the node + // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner + let relative_cursor_position = Vec2::new( + (cursor_position.x - min.x) / node.node.size().x, + (cursor_position.y - min.y) / node.node.size().y, + ); - contains_cursor.then_some(( - entity, - HitData { - camera, - depth: position.z, - position: None, - normal: None, - }, - )) + if (0.0..1.).contains(&relative_cursor_position.x) + && (0.0..1.).contains(&relative_cursor_position.y) + { + Some(*entity) + } else { + None + } + } else { + None + } }) - .collect::>(); + .collect::>() + .into_iter(); + + // As soon as a node with a `Block` focus policy is detected, the iteration will stop on it + // because it "captures" the interaction. + let mut iter = node_query.iter_many_mut(hovered_nodes.by_ref()); + let mut picks = Vec::new(); + let mut depth = 0.0; + + while let Some(node) = iter.fetch_next() { + picks.push(( + node.entity, + HitData { + camera, + depth, + position: None, + normal: None, + }, + )); + match node.focus_policy.unwrap_or(&FocusPolicy::Block) { + FocusPolicy::Block => { + break; + } + FocusPolicy::Pass => { /* allow the next node to be hovered/clicked */ } + } + depth += 0.00001; // keep depth near 0 for precision + } output.send(PointerHits { pointer: *pointer, - picks: over_list, + picks, order: 10, }) } diff --git a/crates/bevy_picking_core/src/event_listening.rs b/crates/bevy_picking_core/src/event_listening.rs index 9c35f8e5..e214bc0c 100644 --- a/crates/bevy_picking_core/src/event_listening.rs +++ b/crates/bevy_picking_core/src/event_listening.rs @@ -111,10 +111,21 @@ impl OnPointer { /// Get mutable access to the target entity's [`EntityCommands`] using a closure any time this /// event listener is triggered. - pub fn target_commands_mut(func: fn(&ListenedEvent, &mut EntityCommands)) -> Self { + pub fn target_commands_mut(func: fn(&ListenedEvent, EntityCommands)) -> Self { Self::run_callback( move |In(event): In>, mut commands: Commands| { - func(&event, &mut commands.entity(event.target)); + func(&event, commands.entity(event.target)); + Bubble::Up + }, + ) + } + + /// Get mutable access to the listener entity's [`EntityCommands`] using a closure any time this + /// event listener is triggered. + pub fn listener_commands_mut(func: fn(&ListenedEvent, EntityCommands)) -> Self { + Self::run_callback( + move |In(event): In>, mut commands: Commands| { + func(&event, commands.entity(event.listener)); Bubble::Up }, ) @@ -131,6 +142,17 @@ impl OnPointer { ) } + /// Insert a bundle on the listener entity any time this event listener is triggered. + pub fn listener_insert(bundle: impl Bundle + Clone) -> Self { + Self::run_callback( + move |In(event): In>, mut commands: Commands| { + let bundle = bundle.clone(); + commands.entity(event.listener).insert(bundle); + Bubble::Up + }, + ) + } + /// Remove a bundle from the target entity any time this event listener is triggered. pub fn target_remove() -> Self { Self::run_callback( @@ -141,6 +163,16 @@ impl OnPointer { ) } + /// Remove a bundle from the listener entity any time this event listener is triggered. + pub fn listener_remove() -> Self { + Self::run_callback( + move |In(event): In>, mut commands: Commands| { + commands.entity(event.listener).remove::(); + Bubble::Up + }, + ) + } + /// Get mutable access to a specific component on the target entity using a closure any time /// this event listener is triggered. If the component does not exist, an error will be logged. pub fn target_component_mut(func: fn(&ListenedEvent, &mut C)) -> Self { @@ -156,6 +188,21 @@ impl OnPointer { ) } + /// Get mutable access to a specific component on the listener entity using a closure any time + /// this event listener is triggered. If the component does not exist, an error will be logged. + pub fn listener_component_mut(func: fn(&ListenedEvent, &mut C)) -> Self { + Self::run_callback( + move |In(event): In>, mut query: Query<&mut C>| { + if let Ok(mut component) = query.get_mut(event.listener) { + func(&event, &mut component); + } else { + error!("Component {:?} not found on entity {:?} during pointer callback for event {:?}", std::any::type_name::(), event.listener, std::any::type_name::()); + } + Bubble::Up + }, + ) + } + /// Send an event `F` any time this event listener is triggered. `F` must implement /// `From>`. pub fn send_event>>() -> Self { @@ -269,7 +316,9 @@ impl EventCallbackGraph { if let Some(mut event_listener) = event_listener { // If it has an event listener, we need to add it to the map listener_map.insert(this_node, (event_listener.take(), None)); - if let Some((_, prev_nodes_next_node)) = listener_map.get_mut(&prev_node) { + if let Some((_, prev_nodes_next_node @ None)) = + listener_map.get_mut(&prev_node) + { if prev_node != this_node { *prev_nodes_next_node = Some(this_node); } diff --git a/crates/bevy_picking_core/src/events.rs b/crates/bevy_picking_core/src/events.rs index 515396ff..d58e7fef 100644 --- a/crates/bevy_picking_core/src/events.rs +++ b/crates/bevy_picking_core/src/events.rs @@ -337,13 +337,11 @@ pub fn pointer_events( /// Maps pointers to the entities they are dragging. #[derive(Debug, Deref, DerefMut, Default, Resource)] -pub struct DragMap(pub HashMap<(PointerId, PointerButton), Option>); +pub struct DragMap(pub HashMap<(PointerId, PointerButton), HashMap>); /// An entry in the [`DragMap`]. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct DragEntry { - /// The entity being dragged. - pub target: Entity, /// 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. @@ -360,7 +358,7 @@ pub fn send_click_and_drag_events( pointer_map: Res, pointers: Query<&PointerLocation>, // Locals - mut down_map: Local>>>, + mut down_map: Local>>>, // Output mut drag_map: ResMut, mut pointer_click: EventWriter>, @@ -383,18 +381,21 @@ pub fn send_click_and_drag_events( } in input_move.iter().cloned() { for button in PointerButton::iter() { - let Some(Some(down)) = down_map.get(&(pointer_id, button)) else { + let Some(down_list) = down_map.get(&(pointer_id, button)) else { continue; }; + let drag_list = drag_map.entry((pointer_id, button)).or_default(); - if !matches!(drag_map.get(&(pointer_id, button)), Some(Some(_))) { - drag_map.insert( - (pointer_id, button), - Some(DragEntry { - target: down.target, + for down in down_list.values() { + if drag_list.contains_key(&down.target) { + continue; // this entity is already logged as being dragged + } + drag_list.insert( + down.target, + DragEntry { start_pos: down.pointer_location.position, latest_pos: down.pointer_location.position, - }), + }, ); pointer_drag_start.send(PointerEvent::new( pointer_id, @@ -407,7 +408,7 @@ pub fn send_click_and_drag_events( )) } - if let Some(Some(drag)) = drag_map.get_mut(&(pointer_id, button)) { + for (dragged_entity, drag) in drag_list.iter_mut() { let drag_event = Drag { button, distance: location.position - drag.start_pos, @@ -417,7 +418,7 @@ pub fn send_click_and_drag_events( pointer_drag.send(PointerEvent::new( pointer_id, location.clone(), - drag.target, + *dragged_entity, drag_event, )) } @@ -432,24 +433,26 @@ pub fn send_click_and_drag_events( event: Up { button, hit }, } in pointer_up.iter().cloned() { - let Some(Some(down)) = down_map.insert((pointer_id, button), None) else { - continue; // Can't have a click without the button being pressed down first - }; - if down.target != target { - continue; // A click starts and ends on the same target + // Can't have a click without the button being pressed down first + if down_map + .get(&(pointer_id, button)) + .and_then(|down| down.get(&target)) + .is_some() + { + pointer_click.send(PointerEvent::new( + pointer_id, + pointer_location, + target, + Click { button, hit }, + )); } - pointer_click.send(PointerEvent::new( - pointer_id, - pointer_location, - target, - Click { button, hit }, - )); } // Triggers when button is pressed over an entity for event in pointer_down.iter() { let button = event.button; - down_map.insert((event.pointer_id, button), Some(event.clone())); + let down_button_entity_map = down_map.entry((event.pointer_id, button)).or_default(); + down_button_entity_map.insert(event.target, event.clone()); } // Triggered for all button presses @@ -457,26 +460,28 @@ pub fn send_click_and_drag_events( if press.direction != pointer::PressDirection::Up { continue; // We are only interested in button releases } - let Some(Some(drag)) = - drag_map.insert((press.pointer_id, press.button), None) else { + 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 { error!("Unable to get location for pointer {:?}", press.pointer_id); continue; }; - let drag_end = DragEnd { - button: press.button, - distance: drag.latest_pos - drag.start_pos, - }; - pointer_drag_end.send(PointerEvent::new( - press.pointer_id, - location, - drag.target, - drag_end, - )); - down_map.insert((press.pointer_id, press.button), None); + + for (drag_target, drag) in drag_list { + let drag_end = DragEnd { + button: press.button, + distance: drag.latest_pos - drag.start_pos, + }; + pointer_drag_end.send(PointerEvent::new( + press.pointer_id, + location.clone(), + drag_target, + drag_end, + )); + } + down_map.insert((press.pointer_id, press.button), HashMap::new()); } } @@ -506,25 +511,28 @@ pub fn send_drag_over_events( } in pointer_over.iter().cloned() { for button in PointerButton::iter() { - let Some(Some(drag)) = drag_map.get(&(pointer_id, button)) else { - continue; // Get the entity that is being dragged - }; - if target == drag.target { - continue; // You can't drag an entity over itself + 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); + let event = DragEnter { + button, + dragged: *drag_target, + hit, + }; + pointer_drag_enter.send(PointerEvent::new( + pointer_id, + pointer_location.clone(), + target, + event, + )) } - let drag_entry = drag_over_map.entry((pointer_id, button)).or_default(); - drag_entry.insert(target, hit); - let event = DragEnter { - button, - dragged: drag.target, - hit, - }; - pointer_drag_enter.send(PointerEvent::new( - pointer_id, - pointer_location.clone(), - target, - event, - )) } } @@ -537,22 +545,25 @@ pub fn send_drag_over_events( } in pointer_move.iter().cloned() { for button in PointerButton::iter() { - let Some(Some(drag)) = drag_map.get(&(pointer_id, button)) else { - continue; // Get the entity that is being dragged - }; - if target == drag.target { - continue; // You can't drag an entity over itself + 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 */ + ) + { + pointer_drag_over.send(PointerEvent::new( + pointer_id, + pointer_location.clone(), + target, + DragOver { + button, + dragged: *drag_target, + hit, + }, + )) } - pointer_drag_over.send(PointerEvent::new( - pointer_id, - pointer_location.clone(), - target, - DragOver { - button, - dragged: drag.target, - hit, - }, - )) } } @@ -610,19 +621,21 @@ pub fn send_drag_over_events( if dragged_over.remove(&target).is_none() { continue; } - let Some(Some(drag)) = drag_map.get(&(pointer_id, button)) else { + let Some(drag_list) = drag_map.get(&(pointer_id, button)) else { continue; }; - pointer_drag_leave.send(PointerEvent::new( - pointer_id, - pointer_location.clone(), - target, - DragLeave { - button, - dragged: drag.target, - hit, - }, - )) + for drag_target in drag_list.keys() { + pointer_drag_leave.send(PointerEvent::new( + pointer_id, + pointer_location.clone(), + target, + DragLeave { + button, + dragged: *drag_target, + hit, + }, + )) + } } } } diff --git a/examples/bevy_ui.rs b/examples/bevy_ui.rs index 87768e72..f9ccb587 100644 --- a/examples/bevy_ui.rs +++ b/examples/bevy_ui.rs @@ -1,6 +1,6 @@ //! This example demonstrates how to use the plugin with bevy_ui. -use bevy::{prelude::*, ui::FocusPolicy}; +use bevy::{ecs::system::EntityCommands, prelude::*, ui::FocusPolicy}; use bevy_mod_picking::prelude::*; const NORMAL: Color = Color::rgb(0.15, 0.15, 0.15); @@ -8,49 +8,82 @@ const HOVERED: Color = Color::rgb(0.25, 0.25, 0.25); const PRESSED: Color = Color::rgb(0.35, 0.75, 0.35); fn main() { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.set(low_latency_window_plugin())) + App::new() + .add_plugins(DefaultPlugins.set(low_latency_window_plugin())) .add_plugins(DefaultPickingPlugins) - .add_startup_system(setup); - #[cfg(feature = "backend_egui")] - app.add_plugin(bevy_egui::EguiPlugin); - app.run(); + .add_startup_system(setup) + .run(); } fn setup(mut commands: Commands, asset_server: Res) { + let font = asset_server.load("fonts/FiraMono-Medium.ttf"); commands.spawn(Camera2dBundle::default()); - commands - .spawn(( - ButtonBundle { - style: Style { - size: Size::new(Val::Px(200.0), Val::Px(65.0)), - margin: UiRect::all(Val::Auto), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - ..default() - }, - background_color: Color::rgb(0.15, 0.15, 0.15).into(), + let root = commands + .spawn(NodeBundle { + style: Style { + size: Size::width(Val::Px(500.0)), + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::FlexStart, + align_items: AlignItems::FlexStart, + margin: UiRect::horizontal(Val::Auto), ..default() }, - OnPointer::::target_insert(BackgroundColor::from(HOVERED)), - OnPointer::::target_insert(BackgroundColor::from(NORMAL)), - OnPointer::::target_insert(BackgroundColor::from(PRESSED)), - OnPointer::::target_insert(BackgroundColor::from(HOVERED)), - OnPointer::::target_component_mut::