bevy/crates/bevy_ui/src/picking_backend.rs

221 lines
8.1 KiB
Rust
Raw Normal View History

//! A picking backend for UI nodes.
//!
//! # Usage
//!
//! This backend does not require markers on cameras or entities to function. It will look for any
//! pointers using the same render target as the UI camera, and run hit tests on the UI node tree.
//!
//! ## Important Note
//!
//! This backend completely ignores [`FocusPolicy`](crate::FocusPolicy). The design of `bevy_ui`'s
//! focus systems and the picking plugin are not compatible. Instead, use the optional [`PickingBehavior`] component
//! to override how an entity responds to picking focus. Nodes without the [`PickingBehavior`] component
//! will still trigger events and block items below it from being hovered.
//!
//! ## Implementation Notes
//!
//! - `bevy_ui` can only render to the primary window
//! - `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.
#![allow(clippy::type_complexity)]
#![allow(clippy::too_many_arguments)]
#![deny(missing_docs)]
use crate::{focus::pick_rounded_rect, prelude::*, UiStack};
use bevy_app::prelude::*;
use bevy_ecs::{prelude::*, query::QueryData};
use precomputed border values (#15163) # Objective Fixes #15142 ## Solution * Moved all the UI border geometry calculations that were scattered through the UI extraction functions into `ui_layout_system`. * Added a `border: BorderRect` field to `Node` to store the border size computed by `ui_layout_system`. * Use the border values returned from Taffy rather than calculate them ourselves during extraction. * Removed the `logical_rect` and `physical_rect` methods from `Node` the descriptions and namings are deceptive, it's better to create the rects manually instead. * Added a method `outline_radius` to `Node` that calculates the border radius of outlines. * For border values `ExtractedUiNode` takes `BorderRect` and `ResolvedBorderRadius` now instead of raw `[f32; 4]` values and converts them in `prepare_uinodes`. * Removed some unnecessary scaling and clamping of border values (#15142). * Added a `BorderRect::ZERO` constant. * Added an `outlined_node_size` method to `Node`. ## Testing Added some non-uniform borders to the border example. Everything seems to be in order: <img width="626" alt="nub" src="https://github.com/user-attachments/assets/258ed8b5-1a9e-4ac5-99c2-6bf25c0ef31c"> ## Migration Guide The `logical_rect` and `physical_rect` methods have been removed from `Node`. Use `Rect::from_center_size` with the translation and node size instead. The types of the fields border and border_radius of `ExtractedUiNode` have been changed to `BorderRect` and `ResolvedBorderRadius` respectively. --------- Co-authored-by: UkoeHB <37489173+UkoeHB@users.noreply.github.com> Co-authored-by: akimakinai <105044389+akimakinai@users.noreply.github.com>
2024-09-26 23:10:35 +00:00
use bevy_math::{Rect, Vec2};
use bevy_render::prelude::*;
use bevy_transform::prelude::*;
use bevy_utils::hashbrown::HashMap;
use bevy_window::PrimaryWindow;
use bevy_picking::backend::prelude::*;
/// A plugin that adds picking support for UI nodes.
#[derive(Clone)]
pub struct UiPickingPlugin;
impl Plugin for UiPickingPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PreUpdate, ui_picking.in_set(PickSet::Backend));
}
}
/// Main query from bevy's `ui_focus_system`
#[derive(QueryData)]
#[query_data(mutable)]
pub struct NodeQuery {
entity: Entity,
Merge Style properties into Node. Use ComputedNode for computed properties. (#15975) # Objective Continue improving the user experience of our UI Node API in the direction specified by [Bevy's Next Generation Scene / UI System](https://github.com/bevyengine/bevy/discussions/14437) ## Solution As specified in the document above, merge `Style` fields into `Node`, and move "computed Node fields" into `ComputedNode` (I chose this name over something like `ComputedNodeLayout` because it currently contains more than just layout info. If we want to break this up / rename these concepts, lets do that in a separate PR). `Style` has been removed. This accomplishes a number of goals: ## Ergonomics wins Specifying both `Node` and `Style` is now no longer required for non-default styles Before: ```rust commands.spawn(( Node::default(), Style { width: Val::Px(100.), ..default() }, )); ``` After: ```rust commands.spawn(Node { width: Val::Px(100.), ..default() }); ``` ## Conceptual clarity `Style` was never a comprehensive "style sheet". It only defined "core" style properties that all `Nodes` shared. Any "styled property" that couldn't fit that mold had to be in a separate component. A "real" style system would style properties _across_ components (`Node`, `Button`, etc). We have plans to build a true style system (see the doc linked above). By moving the `Style` fields to `Node`, we fully embrace `Node` as the driving concept and remove the "style system" confusion. ## Next Steps * Consider identifying and splitting out "style properties that aren't core to Node". This should not happen for Bevy 0.15. --- ## Migration Guide Move any fields set on `Style` into `Node` and replace all `Style` component usage with `Node`. Before: ```rust commands.spawn(( Node::default(), Style { width: Val::Px(100.), ..default() }, )); ``` After: ```rust commands.spawn(Node { width: Val::Px(100.), ..default() }); ``` For any usage of the "computed node properties" that used to live on `Node`, use `ComputedNode` instead: Before: ```rust fn system(nodes: Query<&Node>) { for node in &nodes { let computed_size = node.size(); } } ``` After: ```rust fn system(computed_nodes: Query<&ComputedNode>) { for computed_node in &computed_nodes { let computed_size = computed_node.size(); } } ```
2024-10-18 22:25:33 +00:00
node: &'static ComputedNode,
global_transform: &'static GlobalTransform,
picking_behavior: Option<&'static PickingBehavior>,
calculated_clip: Option<&'static CalculatedClip>,
view_visibility: Option<&'static ViewVisibility>,
target_camera: Option<&'static TargetCamera>,
}
/// 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)>,
camera_query: Query<(Entity, &Camera, Has<IsDefaultUiCamera>)>,
default_ui_camera: DefaultUiCamera,
primary_window: Query<Entity, With<PrimaryWindow>>,
ui_stack: Res<UiStack>,
node_query: Query<NodeQuery>,
mut output: EventWriter<PointerHits>,
) {
// For each camera, the pointer and its position
let mut pointer_pos_by_camera = HashMap::<Entity, HashMap<PointerId, Vec2>>::new();
for (pointer_id, pointer_location) in
pointers.iter().filter_map(|(pointer, pointer_location)| {
Some(*pointer).zip(pointer_location.location().cloned())
})
{
// This pointer is associated with a render target, which could be used by multiple
// cameras. We want to ensure we return all cameras with a matching target.
for camera in camera_query
.iter()
.map(|(entity, camera, _)| {
(
entity,
camera.target.normalize(primary_window.get_single().ok()),
)
})
.filter_map(|(entity, target)| Some(entity).zip(target))
.filter(|(_entity, target)| target == &pointer_location.target)
.map(|(cam_entity, _target)| cam_entity)
{
let Ok((_, camera_data, _)) = camera_query.get(camera) else {
continue;
};
let mut pointer_pos =
pointer_location.position * camera_data.target_scaling_factor().unwrap_or(1.);
if let Some(viewport) = camera_data.physical_viewport_rect() {
pointer_pos -= viewport.min.as_vec2();
}
pointer_pos_by_camera
.entry(camera)
.or_default()
.insert(pointer_id, pointer_pos);
}
}
// The list of node entities hovered for each (camera, pointer) combo
let mut hit_nodes = HashMap::<(Entity, PointerId), Vec<Entity>>::new();
// 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`
// for all nodes encountered that are no longer hovered.
for node_entity in ui_stack
.uinodes
.iter()
// reverse the iterator to traverse the tree from closest nodes to furthest
.rev()
{
let Ok(node) = node_query.get(*node_entity) else {
continue;
};
// Nodes that are not rendered should not be interactable
if node
.view_visibility
.map(|view_visibility| view_visibility.get())
!= Some(true)
{
continue;
}
let Some(camera_entity) = node
.target_camera
.map(TargetCamera::entity)
.or(default_ui_camera.get())
else {
continue;
};
use precomputed border values (#15163) # Objective Fixes #15142 ## Solution * Moved all the UI border geometry calculations that were scattered through the UI extraction functions into `ui_layout_system`. * Added a `border: BorderRect` field to `Node` to store the border size computed by `ui_layout_system`. * Use the border values returned from Taffy rather than calculate them ourselves during extraction. * Removed the `logical_rect` and `physical_rect` methods from `Node` the descriptions and namings are deceptive, it's better to create the rects manually instead. * Added a method `outline_radius` to `Node` that calculates the border radius of outlines. * For border values `ExtractedUiNode` takes `BorderRect` and `ResolvedBorderRadius` now instead of raw `[f32; 4]` values and converts them in `prepare_uinodes`. * Removed some unnecessary scaling and clamping of border values (#15142). * Added a `BorderRect::ZERO` constant. * Added an `outlined_node_size` method to `Node`. ## Testing Added some non-uniform borders to the border example. Everything seems to be in order: <img width="626" alt="nub" src="https://github.com/user-attachments/assets/258ed8b5-1a9e-4ac5-99c2-6bf25c0ef31c"> ## Migration Guide The `logical_rect` and `physical_rect` methods have been removed from `Node`. Use `Rect::from_center_size` with the translation and node size instead. The types of the fields border and border_radius of `ExtractedUiNode` have been changed to `BorderRect` and `ResolvedBorderRadius` respectively. --------- Co-authored-by: UkoeHB <37489173+UkoeHB@users.noreply.github.com> Co-authored-by: akimakinai <105044389+akimakinai@users.noreply.github.com>
2024-09-26 23:10:35 +00:00
let node_rect = Rect::from_center_size(
node.global_transform.translation().truncate(),
node.node.size(),
);
// Nodes with Display::None have a (0., 0.) logical rect and can be ignored
if node_rect.size() == Vec2::ZERO {
continue;
}
// Intersect with the calculated clip rect to find the bounds of the visible region of the node
let visible_rect = node
.calculated_clip
.map(|clip| node_rect.intersect(clip.clip))
.unwrap_or(node_rect);
let pointers_on_this_cam = pointer_pos_by_camera.get(&camera_entity);
// The mouse position relative to the node
// (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner
// Coordinates are relative to the entire node, not just the visible region.
for (pointer_id, cursor_position) in pointers_on_this_cam.iter().flat_map(|h| h.iter()) {
let relative_cursor_position = (*cursor_position - node_rect.min) / node_rect.size();
if visible_rect
.normalize(node_rect)
.contains(relative_cursor_position)
Ignore clicks on uinodes outside of rounded corners (#14957) # Objective Fixes #14941 ## Solution 1. Add a `resolved_border_radius` field to `Node` to hold the resolved border radius values. 2. Remove the border radius calculations from the UI's extraction functions. 4. Compute the border radius during UI relayouts in `ui_layout_system` and store them in `Node`. 5. New `pick_rounded_rect` function based on the border radius SDF from `ui.wgsl`. 6. Use `pick_rounded_rect` in `focus` and `picking_backend` to check if the pointer is hovering UI nodes with rounded corners. --- ## Showcase ``` cargo run --example button ``` https://github.com/user-attachments/assets/ea951a64-17ef-455e-b5c9-a2e6f6360648 ## Testing Modified button example with buttons with different corner radius: ``` use bevy::{color::palettes::basic::*, prelude::*, winit::WinitSettings}; fn main() { App::new() .add_plugins(DefaultPlugins) // Only run the app when there is user input. This will significantly reduce CPU/GPU use. .insert_resource(WinitSettings::desktop_app()) .add_systems(Startup, setup) .add_systems(Update, button_system) .run(); } const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15); const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); fn button_system( mut interaction_query: Query< ( &Interaction, &mut BackgroundColor, &mut BorderColor, &Children, ), (Changed<Interaction>, With<Button>), >, mut text_query: Query<&mut Text>, ) { for (interaction, mut color, mut border_color, children) in &mut interaction_query { let mut text = text_query.get_mut(children[0]).unwrap(); match *interaction { Interaction::Pressed => { text.sections[0].value = "Press".to_string(); *color = PRESSED_BUTTON.into(); border_color.0 = RED.into(); } Interaction::Hovered => { text.sections[0].value = "Hover".to_string(); *color = HOVERED_BUTTON.into(); border_color.0 = Color::WHITE; } Interaction::None => { text.sections[0].value = "Button".to_string(); *color = NORMAL_BUTTON.into(); border_color.0 = Color::BLACK; } } } } fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { // ui camera commands.spawn(Camera2dBundle::default()); commands .spawn(NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), align_items: AlignItems::Center, justify_content: JustifyContent::Center, row_gap: Val::Px(10.), ..default() }, ..default() }) .with_children(|parent| { for border_radius in [ BorderRadius { top_left: Val::ZERO, ..BorderRadius::MAX }, BorderRadius { top_right: Val::ZERO, ..BorderRadius::MAX }, BorderRadius { bottom_right: Val::ZERO, ..BorderRadius::MAX }, BorderRadius { bottom_left: Val::ZERO, ..BorderRadius::MAX }, ] { parent .spawn(ButtonBundle { style: Style { width: Val::Px(150.0), height: Val::Px(65.0), border: UiRect::all(Val::Px(5.0)), // horizontally center child text justify_content: JustifyContent::Center, // vertically center child text align_items: AlignItems::Center, ..default() }, border_color: BorderColor(Color::BLACK), border_radius, background_color: NORMAL_BUTTON.into(), ..default() }) .with_child(TextBundle::from_section( "Button", TextStyle { font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 40.0, color: Color::srgb(0.9, 0.9, 0.9), }, )); } }); } ``` --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Matty <weatherleymatthew@gmail.com>
2024-09-03 12:38:59 +00:00
&& pick_rounded_rect(
*cursor_position - node_rect.center(),
node_rect.size(),
node.node.border_radius,
)
{
hit_nodes
.entry((camera_entity, *pointer_id))
.or_default()
.push(*node_entity);
}
}
}
for ((camera, pointer), hovered_nodes) 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) {
let Some(camera_entity) = node
.target_camera
.map(TargetCamera::entity)
.or(default_ui_camera.get())
else {
continue;
};
picks.push((node.entity, HitData::new(camera_entity, depth, None, None)));
if let Some(picking_behavior) = node.picking_behavior {
// If an entity has a `PickingBehavior` component, we will use that as the source of truth.
if picking_behavior.should_block_lower {
break;
}
} else {
// If the PickingBehavior component doesn't exist, default behavior is to block.
break;
}
depth += 0.00001; // keep depth near 0 for precision
}
let order = camera_query
.get(*camera)
.map(|(_, cam, _)| cam.order)
.unwrap_or_default() as f32
+ 0.5; // bevy ui can run on any camera, it's a special case
output.send(PointerHits::new(*pointer, picks, order));
}
}