# Objective Fixes a part of #14274. Bevy has an incredibly inconsistent naming convention for its system sets, both internally and across the ecosystem. <img alt="System sets in Bevy" src="https://github.com/user-attachments/assets/d16e2027-793f-4ba4-9cc9-e780b14a5a1b" width="450" /> *Names of public system set types in Bevy* Most Bevy types use a naming of `FooSystem` or just `Foo`, but there are also a few `FooSystems` and `FooSet` types. In ecosystem crates on the other hand, `FooSet` is perhaps the most commonly used name in general. Conventions being so wildly inconsistent can make it harder for users to pick names for their own types, to search for system sets on docs.rs, or to even discern which types *are* system sets. To reign in the inconsistency a bit and help unify the ecosystem, it would be good to establish a common recommended naming convention for system sets in Bevy itself, similar to how plugins are commonly suffixed with `Plugin` (ex: `TimePlugin`). By adopting a consistent naming convention in first-party Bevy, we can softly nudge ecosystem crates to follow suit (for types where it makes sense to do so). Choosing a naming convention is also relevant now, as the [`bevy_cli` recently adopted lints](https://github.com/TheBevyFlock/bevy_cli/pull/345) to enforce naming for plugins and system sets, and the recommended naming used for system sets is still a bit open. ## Which Name To Use? Now the contentious part: what naming convention should we actually adopt? This was discussed on the Bevy Discord at the end of last year, starting [here](<https://discord.com/channels/691052431525675048/692572690833473578/1310659954683936789>). `FooSet` and `FooSystems` were the clear favorites, with `FooSet` very narrowly winning an unofficial poll. However, it seems to me like the consensus was broadly moving towards `FooSystems` at the end and after the poll, with Cart ([source](https://discord.com/channels/691052431525675048/692572690833473578/1311140204974706708)) and later Alice ([source](https://discord.com/channels/691052431525675048/692572690833473578/1311092530732859533)) and also me being in favor of it. Let's do a quick pros and cons list! Of course these are just what I thought of, so take it with a grain of salt. `FooSet`: - Pro: Nice and short! - Pro: Used by many ecosystem crates. - Pro: The `Set` suffix comes directly from the trait name `SystemSet`. - Pro: Pairs nicely with existing APIs like `in_set` and `configure_sets`. - Con: `Set` by itself doesn't actually indicate that it's related to systems *at all*, apart from the implemented trait. A set of what? - Con: Is `FooSet` a set of `Foo`s or a system set related to `Foo`? Ex: `ContactSet`, `MeshSet`, `EnemySet`... `FooSystems`: - Pro: Very clearly indicates that the type represents a collection of systems. The actual core concept, system(s), is in the name. - Pro: Parallels nicely with `FooPlugins` for plugin groups. - Pro: Low risk of conflicts with other names or misunderstandings about what the type is. - Pro: In most cases, reads *very* nicely and clearly. Ex: `PhysicsSystems` and `AnimationSystems` as opposed to `PhysicsSet` and `AnimationSet`. - Pro: Easy to search for on docs.rs. - Con: Usually results in longer names. - Con: Not yet as widely used. Really the big problem with `FooSet` is that it doesn't actually describe what it is. It describes what *kind of thing* it is (a set of something), but not *what it is a set of*, unless you know the type or check its docs or implemented traits. `FooSystems` on the other hand is much more self-descriptive in this regard, at the cost of being a bit longer to type. Ultimately, in some ways it comes down to preference and how you think of system sets. Personally, I was originally in favor of `FooSet`, but have been increasingly on the side of `FooSystems`, especially after seeing what the new names would actually look like in Avian and now Bevy. I prefer it because it usually reads better, is much more clearly related to groups of systems than `FooSet`, and overall *feels* more correct and natural to me in the long term. For these reasons, and because Alice and Cart also seemed to share a preference for it when it was previously being discussed, I propose that we adopt a `FooSystems` naming convention where applicable. ## Solution Rename Bevy's system set types to use a consistent `FooSet` naming where applicable. - `AccessibilitySystem` → `AccessibilitySystems` - `GizmoRenderSystem` → `GizmoRenderSystems` - `PickSet` → `PickingSystems` - `RunFixedMainLoopSystem` → `RunFixedMainLoopSystems` - `TransformSystem` → `TransformSystems` - `RemoteSet` → `RemoteSystems` - `RenderSet` → `RenderSystems` - `SpriteSystem` → `SpriteSystems` - `StateTransitionSteps` → `StateTransitionSystems` - `RenderUiSystem` → `RenderUiSystems` - `UiSystem` → `UiSystems` - `Animation` → `AnimationSystems` - `AssetEvents` → `AssetEventSystems` - `TrackAssets` → `AssetTrackingSystems` - `UpdateGizmoMeshes` → `GizmoMeshSystems` - `InputSystem` → `InputSystems` - `InputFocusSet` → `InputFocusSystems` - `ExtractMaterialsSet` → `MaterialExtractionSystems` - `ExtractMeshesSet` → `MeshExtractionSystems` - `RumbleSystem` → `RumbleSystems` - `CameraUpdateSystem` → `CameraUpdateSystems` - `ExtractAssetsSet` → `AssetExtractionSystems` - `Update2dText` → `Text2dUpdateSystems` - `TimeSystem` → `TimeSystems` - `AudioPlaySet` → `AudioPlaybackSystems` - `SendEvents` → `EventSenderSystems` - `EventUpdates` → `EventUpdateSystems` A lot of the names got slightly longer, but they are also a lot more consistent, and in my opinion the majority of them read much better. For a few of the names I took the liberty of rewording things a bit; definitely open to any further naming improvements. There are still also cases where the `FooSystems` naming doesn't really make sense, and those I left alone. This primarily includes system sets like `Interned<dyn SystemSet>`, `EnterSchedules<S>`, `ExitSchedules<S>`, or `TransitionSchedules<S>`, where the type has some special purpose and semantics. ## Todo - [x] Should I keep all the old names as deprecated type aliases? I can do this, but to avoid wasting work I'd prefer to first reach consensus on whether these renames are even desired. - [x] Migration guide - [x] Release notes
265 lines
9.8 KiB
Rust
265 lines
9.8 KiB
Rust
//! 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 [`Pickable`] component
|
|
//! to override how an entity responds to picking focus. Nodes without the [`Pickable`] 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.
|
|
//! - 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. This backend does not provide a `normal`.
|
|
|
|
#![deny(missing_docs)]
|
|
|
|
use crate::{focus::pick_rounded_rect, prelude::*, UiStack};
|
|
use bevy_app::prelude::*;
|
|
use bevy_ecs::{prelude::*, query::QueryData};
|
|
use bevy_math::{Rect, Vec2};
|
|
use bevy_platform::collections::HashMap;
|
|
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
|
use bevy_render::prelude::*;
|
|
use bevy_transform::prelude::*;
|
|
use bevy_window::PrimaryWindow;
|
|
|
|
use bevy_picking::backend::prelude::*;
|
|
|
|
/// An optional component that marks cameras that should be used in the [`UiPickingPlugin`].
|
|
///
|
|
/// Only needed if [`UiPickingSettings::require_markers`] is set to `true`, and ignored
|
|
/// otherwise.
|
|
#[derive(Debug, Clone, Default, Component, Reflect)]
|
|
#[reflect(Debug, Default, Component)]
|
|
pub struct UiPickingCamera;
|
|
|
|
/// Runtime settings for the [`UiPickingPlugin`].
|
|
#[derive(Resource, Reflect)]
|
|
#[reflect(Resource, Default)]
|
|
pub struct UiPickingSettings {
|
|
/// When set to `true` UI picking will only consider cameras marked with
|
|
/// [`UiPickingCamera`] and entities marked with [`Pickable`]. `false` by default.
|
|
///
|
|
/// This setting is provided to give you fine-grained control over which cameras and entities
|
|
/// should be used by the UI picking backend at runtime.
|
|
pub require_markers: bool,
|
|
}
|
|
|
|
#[expect(
|
|
clippy::allow_attributes,
|
|
reason = "clippy::derivable_impls is not always linted"
|
|
)]
|
|
#[allow(
|
|
clippy::derivable_impls,
|
|
reason = "Known false positive with clippy: <https://github.com/rust-lang/rust-clippy/issues/13160>"
|
|
)]
|
|
impl Default for UiPickingSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
require_markers: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A plugin that adds picking support for UI nodes.
|
|
///
|
|
/// This is included by default in [`UiPlugin`](crate::UiPlugin).
|
|
#[derive(Clone)]
|
|
pub struct UiPickingPlugin;
|
|
impl Plugin for UiPickingPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.init_resource::<UiPickingSettings>()
|
|
.register_type::<(UiPickingCamera, UiPickingSettings)>()
|
|
.add_systems(PreUpdate, ui_picking.in_set(PickingSystems::Backend));
|
|
}
|
|
}
|
|
|
|
/// Main query from bevy's `ui_focus_system`
|
|
#[derive(QueryData)]
|
|
#[query_data(mutable)]
|
|
pub struct NodeQuery {
|
|
entity: Entity,
|
|
node: &'static ComputedNode,
|
|
global_transform: &'static GlobalTransform,
|
|
pickable: Option<&'static Pickable>,
|
|
calculated_clip: Option<&'static CalculatedClip>,
|
|
inherited_visibility: Option<&'static InheritedVisibility>,
|
|
target_camera: &'static ComputedNodeTarget,
|
|
}
|
|
|
|
/// 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<UiPickingCamera>)>,
|
|
primary_window: Query<Entity, With<PrimaryWindow>>,
|
|
settings: Res<UiPickingSettings>,
|
|
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>>::default();
|
|
|
|
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()
|
|
.filter(|(_, _, cam_can_pick)| !settings.require_markers || *cam_can_pick)
|
|
.map(|(entity, camera, _)| {
|
|
(
|
|
entity,
|
|
camera.target.normalize(primary_window.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, 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`
|
|
// 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;
|
|
};
|
|
|
|
if settings.require_markers && node.pickable.is_none() {
|
|
continue;
|
|
}
|
|
|
|
// Nodes that are not rendered should not be interactable
|
|
if node
|
|
.inherited_visibility
|
|
.map(|inherited_visibility| inherited_visibility.get())
|
|
!= Some(true)
|
|
{
|
|
continue;
|
|
}
|
|
let Some(camera_entity) = node.target_camera.camera() else {
|
|
continue;
|
|
};
|
|
|
|
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)
|
|
&& 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, relative_cursor_position));
|
|
}
|
|
}
|
|
}
|
|
|
|
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 (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, 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.
|
|
if pickable.should_block_lower {
|
|
break;
|
|
}
|
|
} else {
|
|
// If the `Pickable` 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.write(PointerHits::new(*pointer, picks, order));
|
|
}
|
|
}
|