Add relative position reporting to UI picking (#17681)

# Objective

Add position reporting to `HitData` sent from the UI picking backend.

## Solution

Add the computed normalized relative cursor position to `hit_data`
alongside the `Entity`.

The position reported in `HitData` is normalized relative to the node,
with `(0.,0.,0.)` at the top left and `(1., 1., 0.)` in the bottom
right. Coordinates are relative to the entire node, not just the visible
region.

`HitData` needs a `Vec3` so I just extended with 0.0. I considered
inserting the `depth` here but thought it would be redundant.

I also considered putting the screen space position in the `normal`
field of `HitData`, but that would require renaming of the field or a
separate data structure.

## Testing

Tested with mouse on X11 with entities that have `Node` components.

---

## Showcase

```rs
// Get click position relative to node
fn hit_position(trigger: Trigger<Pointer<Click>>) {
    let hit_pos = trigger.event.hit.position.expect("no position");
    info!("{}", hit_pos);
}
```

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
sam edelsten 2025-02-11 18:38:13 +00:00 committed by GitHub
parent fd67ca7eb0
commit 5eff6e80e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -18,6 +18,8 @@
//! - `bevy_ui` can render on any camera with a flag, it is special, and is not tied to a particular
//! camera.
//! - To correctly sort picks, the order of `bevy_ui` is set to be the camera order plus 0.5.
//! - The position reported in `HitData` is normalized relative to the node, with `(0.,0.,0.)` at the top
//! left and `(1., 1., 0.)` in the bottom right. Coordinates are relative to the entire node, not just the visible region.
#![deny(missing_docs)]
@ -104,7 +106,7 @@ pub fn ui_picking(
}
// The list of node entities hovered for each (camera, pointer) combo
let mut hit_nodes = HashMap::<(Entity, PointerId), Vec<Entity>>::default();
let mut hit_nodes = HashMap::<(Entity, PointerId), Vec<(Entity, Vec2)>>::default();
// prepare an iterator that contains all the nodes that have the cursor in their rect,
// from the top node to the bottom one. this will also reset the interaction to `None`
@ -167,23 +169,28 @@ pub fn ui_picking(
hit_nodes
.entry((camera_entity, *pointer_id))
.or_default()
.push(*node_entity);
.push((*node_entity, relative_cursor_position));
}
}
}
for ((camera, pointer), hovered_nodes) in hit_nodes.iter() {
for ((camera, pointer), hovered) in hit_nodes.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 picks = Vec::new();
let mut depth = 0.0;
for node in node_query.iter_many(hovered_nodes) {
for (hovered_node, position) in hovered {
let node = node_query.get(*hovered_node).unwrap();
let Some(camera_entity) = node.target_camera.camera() else {
continue;
};
picks.push((node.entity, HitData::new(camera_entity, depth, None, None)));
picks.push((
node.entity,
HitData::new(camera_entity, depth, Some(position.extend(0.0)), None),
));
if let Some(pickable) = node.pickable {
// If an entity has a `Pickable` component, we will use that as the source of truth.