Skip to content

Commit

Permalink
Fix inverted bevy_ui backend (#218)
Browse files Browse the repository at this point in the history
* fix inverted ui

* avoid panics

* Formatting
  • Loading branch information
aevyrie authored Jun 17, 2023
1 parent d6eca9d commit ed2359e
Show file tree
Hide file tree
Showing 6 changed files with 334 additions and 187 deletions.
14 changes: 8 additions & 6 deletions backends/bevy_picking_sprite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,22 @@ 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
.target
.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()
Expand Down
160 changes: 105 additions & 55 deletions backends/bevy_picking_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<Entity, With<PrimaryWindow>>,
mut node_query: Query<
(
Entity,
&ui::Node,
&GlobalTransform,
&FocusPolicy,
Option<&CalculatedClip>,
),
Without<PointerId>,
>,
cameras: Query<(Entity, &Camera, Option<&UiCameraConfig>)>,
primary_window: Query<(Entity, &Window), With<PrimaryWindow>>,
ui_stack: Res<UiStack>,
mut node_query: Query<NodeQuery>,
mut output: EventWriter<PointerHits>,
) {
for (pointer, location) in pointers.iter().filter_map(|(pointer, pointer_location)| {
Expand All @@ -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::<Vec<_>>();
.collect::<Vec<Entity>>()
.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,
})
}
Expand Down
55 changes: 52 additions & 3 deletions crates/bevy_picking_core/src/event_listening.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,21 @@ impl<E: IsPointerEvent> OnPointer<E> {

/// 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<E>, &mut EntityCommands)) -> Self {
pub fn target_commands_mut(func: fn(&ListenedEvent<E>, EntityCommands)) -> Self {
Self::run_callback(
move |In(event): In<ListenedEvent<E>>, 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<E>, EntityCommands)) -> Self {
Self::run_callback(
move |In(event): In<ListenedEvent<E>>, mut commands: Commands| {
func(&event, commands.entity(event.listener));
Bubble::Up
},
)
Expand All @@ -131,6 +142,17 @@ impl<E: IsPointerEvent> OnPointer<E> {
)
}

/// 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<ListenedEvent<E>>, 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<B: Bundle>() -> Self {
Self::run_callback(
Expand All @@ -141,6 +163,16 @@ impl<E: IsPointerEvent> OnPointer<E> {
)
}

/// Remove a bundle from the listener entity any time this event listener is triggered.
pub fn listener_remove<B: Bundle>() -> Self {
Self::run_callback(
move |In(event): In<ListenedEvent<E>>, mut commands: Commands| {
commands.entity(event.listener).remove::<B>();
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<C: Component>(func: fn(&ListenedEvent<E>, &mut C)) -> Self {
Expand All @@ -156,6 +188,21 @@ impl<E: IsPointerEvent> OnPointer<E> {
)
}

/// 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<C: Component>(func: fn(&ListenedEvent<E>, &mut C)) -> Self {
Self::run_callback(
move |In(event): In<ListenedEvent<E>>, 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::<C>(), event.listener, std::any::type_name::<E>());
}
Bubble::Up
},
)
}

/// Send an event `F` any time this event listener is triggered. `F` must implement
/// `From<ListenedEvent<E>>`.
pub fn send_event<F: Event + From<ListenedEvent<E>>>() -> Self {
Expand Down Expand Up @@ -269,7 +316,9 @@ impl<E: IsPointerEvent> EventCallbackGraph<E> {
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);
}
Expand Down
Loading

0 comments on commit ed2359e

Please sign in to comment.