mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
Add UI GhostNode
(#15341)
# Objective - Fixes #14826 - For context, see #15238 ## Solution Add a `GhostNode` component to `bevy_ui` and update all the relevant systems to use it to traverse for UI children. - [x] `ghost_hierarchy` module - [x] Add `GhostNode` - [x] Add `UiRootNodes` system param for iterating (ghost-aware) UI root nodes - [x] Add `UiChildren` system param for iterating (ghost-aware) UI children - [x] Update `layout::ui_layout_system` - [x] Use ghost-aware root nodes for camera updates - [x] Update and remove children in taffy - [x] Initial spawn - [x] Detect changes on nested UI children - [x] Use ghost-aware children traversal in `update_uinode_geometry_recursive` - [x] Update the rest of the UI systems to use the ghost hierarchy - [x] `stack::ui_stack_system` - [x] `update::` - [x] `update_clipping_system` - [x] `update_target_camera_system` - [x] `accessibility::calc_name` ## Testing - [x] Added a new example `ghost_nodes` that can be used as a testbed. - [x] Added unit tests for _some_ of the traversal utilities in `ghost_hierarchy` - [x] Ensure this fulfills the needs for currently known use cases - [x] Reactivity libraries (test with `bevy_reactor`) - [ ] Text spans (mentioned by koe [on discord](https://discord.com/channels/691052431525675048/1285371432460881991/1285377442998915246)) --- ## Performance [See comment below](https://github.com/bevyengine/bevy/pull/15341#issuecomment-2385456820) ## Migration guide Any code that previously relied on `Parent`/`Children` to iterate UI children may now want to use `bevy_ui::UiChildren` to ensure ghost nodes are skipped, and their first descendant Nodes included. UI root nodes may now be children of ghost nodes, which means `Without<Parent>` might not query all root nodes. Use `bevy_ui::UiRootNodes` where needed to iterate root nodes instead. ## Potential future work - Benchmarking/optimizations of hierarchies containing lots of ghost nodes - Further exploration of UI hierarchies and markers for root nodes/leaf nodes to create better ergonomics for things like `UiLayer` (world-space ui) --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: UkoeHB <37489173+UkoeHB@users.noreply.github.com>
This commit is contained in:
parent
3df281ba7b
commit
f86ee32576
10 changed files with 488 additions and 141 deletions
11
Cargo.toml
11
Cargo.toml
|
@ -2972,6 +2972,17 @@ description = "Demonstrates text wrapping"
|
|||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "ghost_nodes"
|
||||
path = "examples/ui/ghost_nodes.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.ghost_nodes]
|
||||
name = "Ghost Nodes"
|
||||
description = "Demonstrates the use of Ghost Nodes to skip entities in the UI layout hierarchy"
|
||||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "grid"
|
||||
path = "examples/ui/grid.rs"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
prelude::{Button, Label},
|
||||
Node, UiImage,
|
||||
Node, UiChildren, UiImage,
|
||||
};
|
||||
use bevy_a11y::{
|
||||
accesskit::{NodeBuilder, Rect, Role},
|
||||
|
@ -14,15 +14,14 @@ use bevy_ecs::{
|
|||
system::{Commands, Query},
|
||||
world::Ref,
|
||||
};
|
||||
use bevy_hierarchy::Children;
|
||||
use bevy_render::{camera::CameraUpdateSystem, prelude::Camera};
|
||||
use bevy_text::Text;
|
||||
use bevy_transform::prelude::GlobalTransform;
|
||||
|
||||
fn calc_name(texts: &Query<&Text>, children: &Children) -> Option<Box<str>> {
|
||||
fn calc_name(texts: &Query<&Text>, children: impl Iterator<Item = Entity>) -> Option<Box<str>> {
|
||||
let mut name = None;
|
||||
for child in children {
|
||||
if let Ok(text) = texts.get(*child) {
|
||||
if let Ok(text) = texts.get(child) {
|
||||
let values = text
|
||||
.sections
|
||||
.iter()
|
||||
|
@ -59,11 +58,12 @@ fn calc_bounds(
|
|||
|
||||
fn button_changed(
|
||||
mut commands: Commands,
|
||||
mut query: Query<(Entity, &Children, Option<&mut AccessibilityNode>), Changed<Button>>,
|
||||
mut query: Query<(Entity, Option<&mut AccessibilityNode>), Changed<Button>>,
|
||||
ui_children: UiChildren,
|
||||
texts: Query<&Text>,
|
||||
) {
|
||||
for (entity, children, accessible) in &mut query {
|
||||
let name = calc_name(&texts, children);
|
||||
for (entity, accessible) in &mut query {
|
||||
let name = calc_name(&texts, ui_children.iter_ui_children(entity));
|
||||
if let Some(mut accessible) = accessible {
|
||||
accessible.set_role(Role::Button);
|
||||
if let Some(name) = name {
|
||||
|
@ -85,14 +85,12 @@ fn button_changed(
|
|||
|
||||
fn image_changed(
|
||||
mut commands: Commands,
|
||||
mut query: Query<
|
||||
(Entity, &Children, Option<&mut AccessibilityNode>),
|
||||
(Changed<UiImage>, Without<Button>),
|
||||
>,
|
||||
mut query: Query<(Entity, Option<&mut AccessibilityNode>), (Changed<UiImage>, Without<Button>)>,
|
||||
ui_children: UiChildren,
|
||||
texts: Query<&Text>,
|
||||
) {
|
||||
for (entity, children, accessible) in &mut query {
|
||||
let name = calc_name(&texts, children);
|
||||
for (entity, accessible) in &mut query {
|
||||
let name = calc_name(&texts, ui_children.iter_ui_children(entity));
|
||||
if let Some(mut accessible) = accessible {
|
||||
accessible.set_role(Role::Image);
|
||||
if let Some(name) = name {
|
||||
|
|
205
crates/bevy_ui/src/ghost_hierarchy.rs
Normal file
205
crates/bevy_ui/src/ghost_hierarchy.rs
Normal file
|
@ -0,0 +1,205 @@
|
|||
//! This module contains [`GhostNode`] and utilities to flatten the UI hierarchy, traversing past ghost nodes.
|
||||
|
||||
use bevy_ecs::{prelude::*, system::SystemParam};
|
||||
use bevy_hierarchy::{Children, HierarchyQueryExt, Parent};
|
||||
use bevy_reflect::prelude::*;
|
||||
use bevy_render::view::Visibility;
|
||||
use bevy_transform::prelude::Transform;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::Node;
|
||||
|
||||
/// Marker component for entities that should be ignored within UI hierarchies.
|
||||
///
|
||||
/// The UI systems will traverse past these and treat their first non-ghost descendants as direct children of their first non-ghost ancestor.
|
||||
///
|
||||
/// Any components necessary for transform and visibility propagation will be added automatically.
|
||||
#[derive(Component, Default, Debug, Copy, Clone, Reflect)]
|
||||
#[reflect(Component, Debug)]
|
||||
#[require(Visibility, Transform)]
|
||||
pub struct GhostNode;
|
||||
|
||||
/// System param that allows iteration of all UI root nodes.
|
||||
///
|
||||
/// A UI root node is either a [`Node`] without a [`Parent`], or with only [`GhostNode`] ancestors.
|
||||
#[derive(SystemParam)]
|
||||
pub struct UiRootNodes<'w, 's> {
|
||||
root_node_query: Query<'w, 's, Entity, (With<Node>, Without<Parent>)>,
|
||||
root_ghost_node_query: Query<'w, 's, Entity, (With<GhostNode>, Without<Parent>)>,
|
||||
all_nodes_query: Query<'w, 's, Entity, With<Node>>,
|
||||
ui_children: UiChildren<'w, 's>,
|
||||
}
|
||||
|
||||
impl<'w, 's> UiRootNodes<'w, 's> {
|
||||
pub fn iter(&'s self) -> impl Iterator<Item = Entity> + 's {
|
||||
self.root_node_query
|
||||
.iter()
|
||||
.chain(self.root_ghost_node_query.iter().flat_map(|root_ghost| {
|
||||
self.all_nodes_query
|
||||
.iter_many(self.ui_children.iter_ui_children(root_ghost))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// System param that gives access to UI children utilities, skipping over [`GhostNode`].
|
||||
#[derive(SystemParam)]
|
||||
pub struct UiChildren<'w, 's> {
|
||||
ui_children_query: Query<'w, 's, (Option<&'static Children>, Option<&'static GhostNode>)>,
|
||||
changed_children_query: Query<'w, 's, Entity, Changed<Children>>,
|
||||
children_query: Query<'w, 's, &'static Children>,
|
||||
ghost_nodes_query: Query<'w, 's, Entity, With<GhostNode>>,
|
||||
parents_query: Query<'w, 's, &'static Parent>,
|
||||
}
|
||||
|
||||
impl<'w, 's> UiChildren<'w, 's> {
|
||||
/// Iterates the children of `entity`, skipping over [`GhostNode`].
|
||||
///
|
||||
/// Traverses the hierarchy depth-first to ensure child order.
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// This iterator allocates if the `entity` node has more than 8 children (including ghost nodes).
|
||||
pub fn iter_ui_children(&'s self, entity: Entity) -> UiChildrenIter<'w, 's> {
|
||||
UiChildrenIter {
|
||||
stack: self
|
||||
.ui_children_query
|
||||
.get(entity)
|
||||
.map_or(SmallVec::new(), |(children, _)| {
|
||||
children.into_iter().flatten().rev().copied().collect()
|
||||
}),
|
||||
query: &self.ui_children_query,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the UI parent of the provided entity, skipping over [`GhostNode`].
|
||||
pub fn get_parent(&'s self, entity: Entity) -> Option<Entity> {
|
||||
self.parents_query
|
||||
.iter_ancestors(entity)
|
||||
.find(|entity| !self.ghost_nodes_query.contains(*entity))
|
||||
}
|
||||
|
||||
/// Iterates the [`GhostNode`]s between this entity and its UI children.
|
||||
pub fn iter_ghost_nodes(&'s self, entity: Entity) -> Box<dyn Iterator<Item = Entity> + 's> {
|
||||
Box::new(
|
||||
self.children_query
|
||||
.get(entity)
|
||||
.into_iter()
|
||||
.flat_map(|children| {
|
||||
self.ghost_nodes_query
|
||||
.iter_many(children)
|
||||
.flat_map(|entity| {
|
||||
core::iter::once(entity).chain(self.iter_ghost_nodes(entity))
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Given an entity in the UI hierarchy, check if its set of children has changed, e.g if children has been added/removed or if the order has changed.
|
||||
pub fn is_changed(&'s self, entity: Entity) -> bool {
|
||||
self.changed_children_query.contains(entity)
|
||||
|| self
|
||||
.iter_ghost_nodes(entity)
|
||||
.any(|entity| self.changed_children_query.contains(entity))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UiChildrenIter<'w, 's> {
|
||||
stack: SmallVec<[Entity; 8]>,
|
||||
query: &'s Query<'w, 's, (Option<&'static Children>, Option<&'static GhostNode>)>,
|
||||
}
|
||||
|
||||
impl<'w, 's> Iterator for UiChildrenIter<'w, 's> {
|
||||
type Item = Entity;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
let entity = self.stack.pop()?;
|
||||
let (children, ghost_node) = self.query.get(entity).ok()?;
|
||||
if ghost_node.is_none() {
|
||||
return Some(entity);
|
||||
}
|
||||
if let Some(children) = children {
|
||||
self.stack.extend(children.iter().rev().copied());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bevy_ecs::{
|
||||
prelude::Component,
|
||||
system::{Query, SystemState},
|
||||
world::World,
|
||||
};
|
||||
use bevy_hierarchy::{BuildChildren, ChildBuild};
|
||||
|
||||
use super::{GhostNode, UiChildren, UiRootNodes};
|
||||
use crate::prelude::NodeBundle;
|
||||
|
||||
#[derive(Component, PartialEq, Debug)]
|
||||
struct A(usize);
|
||||
|
||||
#[test]
|
||||
fn iterate_ui_root_nodes() {
|
||||
let world = &mut World::new();
|
||||
|
||||
// Normal root
|
||||
world
|
||||
.spawn((A(1), NodeBundle::default()))
|
||||
.with_children(|parent| {
|
||||
parent.spawn((A(2), NodeBundle::default()));
|
||||
parent
|
||||
.spawn((A(3), GhostNode))
|
||||
.with_child((A(4), NodeBundle::default()));
|
||||
});
|
||||
|
||||
// Ghost root
|
||||
world.spawn((A(5), GhostNode)).with_children(|parent| {
|
||||
parent.spawn((A(6), NodeBundle::default()));
|
||||
parent
|
||||
.spawn((A(7), GhostNode))
|
||||
.with_child((A(8), NodeBundle::default()))
|
||||
.with_child(A(9));
|
||||
});
|
||||
|
||||
let mut system_state = SystemState::<(UiRootNodes, Query<&A>)>::new(world);
|
||||
let (ui_root_nodes, a_query) = system_state.get(world);
|
||||
|
||||
let result: Vec<_> = a_query.iter_many(ui_root_nodes.iter()).collect();
|
||||
|
||||
assert_eq!([&A(1), &A(6), &A(8)], result.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iterate_ui_children() {
|
||||
let world = &mut World::new();
|
||||
|
||||
let n1 = world.spawn((A(1), NodeBundle::default())).id();
|
||||
let n2 = world.spawn((A(2), GhostNode)).id();
|
||||
let n3 = world.spawn((A(3), GhostNode)).id();
|
||||
let n4 = world.spawn((A(4), NodeBundle::default())).id();
|
||||
let n5 = world.spawn((A(5), NodeBundle::default())).id();
|
||||
|
||||
let n6 = world.spawn((A(6), GhostNode)).id();
|
||||
let n7 = world.spawn((A(7), GhostNode)).id();
|
||||
let n8 = world.spawn((A(8), NodeBundle::default())).id();
|
||||
let n9 = world.spawn((A(9), GhostNode)).id();
|
||||
let n10 = world.spawn((A(10), NodeBundle::default())).id();
|
||||
|
||||
world.entity_mut(n1).add_children(&[n2, n3, n4, n6]);
|
||||
world.entity_mut(n2).add_children(&[n5]);
|
||||
|
||||
world.entity_mut(n6).add_children(&[n7, n9]);
|
||||
world.entity_mut(n7).add_children(&[n8]);
|
||||
world.entity_mut(n9).add_children(&[n10]);
|
||||
|
||||
let mut system_state = SystemState::<(UiChildren, Query<&A>)>::new(world);
|
||||
let (ui_children, a_query) = system_state.get(world);
|
||||
|
||||
let result: Vec<_> = a_query
|
||||
.iter_many(ui_children.iter_ui_children(n1))
|
||||
.collect();
|
||||
|
||||
assert_eq!([&A(5), &A(4), &A(8), &A(10)], result.as_slice());
|
||||
}
|
||||
}
|
|
@ -1,17 +1,17 @@
|
|||
use crate::{
|
||||
BorderRadius, ContentSize, DefaultUiCamera, Display, Node, Outline, OverflowAxis,
|
||||
ScrollPosition, Style, TargetCamera, UiScale,
|
||||
ScrollPosition, Style, TargetCamera, UiChildren, UiRootNodes, UiScale,
|
||||
};
|
||||
use bevy_ecs::{
|
||||
change_detection::{DetectChanges, DetectChangesMut},
|
||||
entity::{Entity, EntityHashMap, EntityHashSet},
|
||||
event::EventReader,
|
||||
query::{With, Without},
|
||||
query::With,
|
||||
removal_detection::RemovedComponents,
|
||||
system::{Commands, Local, Query, Res, ResMut, SystemParam},
|
||||
world::Ref,
|
||||
};
|
||||
use bevy_hierarchy::{Children, Parent};
|
||||
use bevy_hierarchy::Children;
|
||||
use bevy_math::{UVec2, Vec2};
|
||||
use bevy_render::camera::{Camera, NormalizedRenderTarget};
|
||||
use bevy_sprite::BorderRect;
|
||||
|
@ -104,7 +104,7 @@ pub fn ui_layout_system(
|
|||
mut scale_factor_events: EventReader<WindowScaleFactorChanged>,
|
||||
mut resize_events: EventReader<bevy_window::WindowResized>,
|
||||
mut ui_surface: ResMut<UiSurface>,
|
||||
root_node_query: Query<(Entity, Option<&TargetCamera>), (With<Node>, Without<Parent>)>,
|
||||
root_nodes: UiRootNodes,
|
||||
mut style_query: Query<
|
||||
(
|
||||
Entity,
|
||||
|
@ -114,8 +114,8 @@ pub fn ui_layout_system(
|
|||
),
|
||||
With<Node>,
|
||||
>,
|
||||
children_query: Query<(Entity, Ref<Children>), With<Node>>,
|
||||
just_children_query: Query<&Children>,
|
||||
node_query: Query<Entity, With<Node>>,
|
||||
ui_children: UiChildren,
|
||||
mut removed_components: UiLayoutSystemRemovedComponentParam,
|
||||
mut node_transform_query: Query<(
|
||||
&mut Node,
|
||||
|
@ -162,35 +162,39 @@ pub fn ui_layout_system(
|
|||
|
||||
// Precalculate the layout info for each camera, so we have fast access to it for each node
|
||||
camera_layout_info.clear();
|
||||
root_node_query.iter().for_each(|(entity,target_camera)|{
|
||||
match camera_with_default(target_camera) {
|
||||
Some(camera_entity) => {
|
||||
let Ok((_, camera)) = cameras.get(camera_entity) else {
|
||||
warn!(
|
||||
"TargetCamera (of root UI node {entity:?}) is pointing to a camera {:?} which doesn't exist",
|
||||
camera_entity
|
||||
);
|
||||
return;
|
||||
};
|
||||
let layout_info = camera_layout_info
|
||||
.entry(camera_entity)
|
||||
.or_insert_with(|| calculate_camera_layout_info(camera));
|
||||
layout_info.root_nodes.push(entity);
|
||||
}
|
||||
None => {
|
||||
if cameras.is_empty() {
|
||||
warn!("No camera found to render UI to. To fix this, add at least one camera to the scene.");
|
||||
} else {
|
||||
warn!(
|
||||
"Multiple cameras found, causing UI target ambiguity. \
|
||||
To fix this, add an explicit `TargetCamera` component to the root UI node {:?}",
|
||||
entity
|
||||
);
|
||||
|
||||
style_query
|
||||
.iter_many(root_nodes.iter())
|
||||
.for_each(|(entity, _, _, target_camera)| {
|
||||
match camera_with_default(target_camera) {
|
||||
Some(camera_entity) => {
|
||||
let Ok((_, camera)) = cameras.get(camera_entity) else {
|
||||
warn!(
|
||||
"TargetCamera (of root UI node {entity:?}) is pointing to a camera {:?} which doesn't exist",
|
||||
camera_entity
|
||||
);
|
||||
return;
|
||||
};
|
||||
let layout_info = camera_layout_info
|
||||
.entry(camera_entity)
|
||||
.or_insert_with(|| calculate_camera_layout_info(camera));
|
||||
layout_info.root_nodes.push(entity);
|
||||
}
|
||||
None => {
|
||||
if cameras.is_empty() {
|
||||
warn!("No camera found to render UI to. To fix this, add at least one camera to the scene.");
|
||||
} else {
|
||||
warn!(
|
||||
"Multiple cameras found, causing UI target ambiguity. \
|
||||
To fix this, add an explicit `TargetCamera` component to the root UI node {:?}",
|
||||
entity
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// When a `ContentSize` component is removed from an entity, we need to remove the measure from the corresponding taffy node.
|
||||
for entity in removed_components.removed_content_sizes.read() {
|
||||
|
@ -244,9 +248,10 @@ pub fn ui_layout_system(
|
|||
for entity in removed_components.removed_children.read() {
|
||||
ui_surface.try_remove_children(entity);
|
||||
}
|
||||
children_query.iter().for_each(|(entity, children)| {
|
||||
if children.is_changed() {
|
||||
ui_surface.update_children(entity, &children);
|
||||
|
||||
node_query.iter().for_each(|entity| {
|
||||
if ui_children.is_changed(entity) {
|
||||
ui_surface.update_children(entity, ui_children.iter_ui_children(entity));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -256,9 +261,9 @@ pub fn ui_layout_system(
|
|||
ui_surface.remove_entities(removed_components.removed_nodes.read());
|
||||
|
||||
// Re-sync changed children: avoid layout glitches caused by removed nodes that are still set as a child of another node
|
||||
children_query.iter().for_each(|(entity, children)| {
|
||||
if children.is_changed() {
|
||||
ui_surface.update_children(entity, &children);
|
||||
node_query.iter().for_each(|entity| {
|
||||
if ui_children.is_changed(entity) {
|
||||
ui_surface.update_children(entity, ui_children.iter_ui_children(entity));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -281,7 +286,7 @@ pub fn ui_layout_system(
|
|||
&ui_surface,
|
||||
None,
|
||||
&mut node_transform_query,
|
||||
&just_children_query,
|
||||
&ui_children,
|
||||
inverse_target_scale_factor,
|
||||
Vec2::ZERO,
|
||||
Vec2::ZERO,
|
||||
|
@ -307,7 +312,7 @@ pub fn ui_layout_system(
|
|||
Option<&Outline>,
|
||||
Option<&ScrollPosition>,
|
||||
)>,
|
||||
children_query: &Query<&Children>,
|
||||
ui_children: &UiChildren,
|
||||
inverse_target_scale_factor: f32,
|
||||
parent_size: Vec2,
|
||||
parent_scroll_position: Vec2,
|
||||
|
@ -415,21 +420,19 @@ pub fn ui_layout_system(
|
|||
.insert(ScrollPosition::from(&clamped_scroll_position));
|
||||
}
|
||||
|
||||
if let Ok(children) = children_query.get(entity) {
|
||||
for &child_uinode in children {
|
||||
update_uinode_geometry_recursive(
|
||||
commands,
|
||||
child_uinode,
|
||||
ui_surface,
|
||||
Some(viewport_size),
|
||||
node_transform_query,
|
||||
children_query,
|
||||
inverse_target_scale_factor,
|
||||
rounded_size,
|
||||
clamped_scroll_position,
|
||||
absolute_location,
|
||||
);
|
||||
}
|
||||
for child_uinode in ui_children.iter_ui_children(entity) {
|
||||
update_uinode_geometry_recursive(
|
||||
commands,
|
||||
child_uinode,
|
||||
ui_surface,
|
||||
Some(viewport_size),
|
||||
node_transform_query,
|
||||
ui_children,
|
||||
inverse_target_scale_factor,
|
||||
rounded_size,
|
||||
clamped_scroll_position,
|
||||
absolute_location,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ use bevy_ecs::{
|
|||
entity::{Entity, EntityHashMap},
|
||||
prelude::Resource,
|
||||
};
|
||||
use bevy_hierarchy::Children;
|
||||
use bevy_math::UVec2;
|
||||
use bevy_utils::{default, tracing::warn};
|
||||
|
||||
|
@ -28,6 +27,7 @@ pub struct UiSurface {
|
|||
pub(super) camera_entity_to_taffy: EntityHashMap<EntityHashMap<taffy::NodeId>>,
|
||||
pub(super) camera_roots: EntityHashMap<Vec<RootNodePair>>,
|
||||
pub(super) taffy: TaffyTree<NodeMeasure>,
|
||||
taffy_children_scratch: Vec<taffy::NodeId>,
|
||||
}
|
||||
|
||||
fn _assert_send_sync_ui_surface_impl_safe() {
|
||||
|
@ -55,6 +55,7 @@ impl Default for UiSurface {
|
|||
camera_entity_to_taffy: Default::default(),
|
||||
camera_roots: Default::default(),
|
||||
taffy,
|
||||
taffy_children_scratch: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -114,22 +115,24 @@ impl UiSurface {
|
|||
}
|
||||
|
||||
/// Update the children of the taffy node corresponding to the given [`Entity`].
|
||||
pub fn update_children(&mut self, entity: Entity, children: &Children) {
|
||||
let mut taffy_children = Vec::with_capacity(children.len());
|
||||
pub fn update_children(&mut self, entity: Entity, children: impl Iterator<Item = Entity>) {
|
||||
self.taffy_children_scratch.clear();
|
||||
|
||||
for child in children {
|
||||
if let Some(taffy_node) = self.entity_to_taffy.get(child) {
|
||||
taffy_children.push(*taffy_node);
|
||||
if let Some(taffy_node) = self.entity_to_taffy.get(&child) {
|
||||
self.taffy_children_scratch.push(*taffy_node);
|
||||
} else {
|
||||
warn!(
|
||||
"Unstyled child `{child}` in a UI entity hierarchy. You are using an entity \
|
||||
without UI components as a child of an entity with UI components, results may be unexpected."
|
||||
without UI components as a child of an entity with UI components, results may be unexpected. \
|
||||
If this is intentional, consider adding a GhostNode component to this entity."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let taffy_node = self.entity_to_taffy.get(&entity).unwrap();
|
||||
self.taffy
|
||||
.set_children(*taffy_node, &taffy_children)
|
||||
.set_children(*taffy_node, &self.taffy_children_scratch)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
@ -147,7 +150,7 @@ without UI components as a child of an entity with UI components, results may be
|
|||
}
|
||||
}
|
||||
|
||||
/// Set the ui node entities without a [`bevy_hierarchy::Parent`] as children to the root node in the taffy layout.
|
||||
/// Sets the ui root node entities as children to the root node in the taffy layout.
|
||||
pub fn set_camera_children(
|
||||
&mut self,
|
||||
camera_id: Entity,
|
||||
|
|
|
@ -26,6 +26,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
|||
mod accessibility;
|
||||
mod focus;
|
||||
mod geometry;
|
||||
mod ghost_hierarchy;
|
||||
mod layout;
|
||||
mod render;
|
||||
mod stack;
|
||||
|
@ -33,6 +34,7 @@ mod ui_node;
|
|||
|
||||
pub use focus::*;
|
||||
pub use geometry::*;
|
||||
pub use ghost_hierarchy::*;
|
||||
pub use layout::*;
|
||||
pub use measurement::*;
|
||||
pub use render::*;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
//! This module contains the systems that update the stored UI nodes stack
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_hierarchy::prelude::*;
|
||||
use bevy_utils::HashSet;
|
||||
|
||||
use crate::{GlobalZIndex, Node, ZIndex};
|
||||
use crate::{GlobalZIndex, Node, UiChildren, UiRootNodes, ZIndex};
|
||||
|
||||
/// The current UI stack, which contains all UI nodes ordered by their depth (back-to-front).
|
||||
///
|
||||
|
@ -39,31 +39,19 @@ impl ChildBufferCache {
|
|||
pub fn ui_stack_system(
|
||||
mut cache: Local<ChildBufferCache>,
|
||||
mut root_nodes: Local<Vec<(Entity, (i32, i32))>>,
|
||||
mut visited_root_nodes: Local<HashSet<Entity>>,
|
||||
mut ui_stack: ResMut<UiStack>,
|
||||
root_node_query: Query<
|
||||
(Entity, Option<&GlobalZIndex>, Option<&ZIndex>),
|
||||
(With<Node>, Without<Parent>),
|
||||
>,
|
||||
zindex_global_node_query: Query<
|
||||
(Entity, &GlobalZIndex, Option<&ZIndex>),
|
||||
(With<Node>, With<Parent>),
|
||||
>,
|
||||
children_query: Query<&Children>,
|
||||
ui_root_nodes: UiRootNodes,
|
||||
root_node_query: Query<(Entity, Option<&GlobalZIndex>, Option<&ZIndex>)>,
|
||||
zindex_global_node_query: Query<(Entity, &GlobalZIndex, Option<&ZIndex>), With<Node>>,
|
||||
ui_children: UiChildren,
|
||||
zindex_query: Query<Option<&ZIndex>, (With<Node>, Without<GlobalZIndex>)>,
|
||||
mut update_query: Query<&mut Node>,
|
||||
) {
|
||||
ui_stack.uinodes.clear();
|
||||
for (id, global_zindex, maybe_zindex) in zindex_global_node_query.iter() {
|
||||
root_nodes.push((
|
||||
id,
|
||||
(
|
||||
global_zindex.0,
|
||||
maybe_zindex.map(|zindex| zindex.0).unwrap_or(0),
|
||||
),
|
||||
));
|
||||
}
|
||||
visited_root_nodes.clear();
|
||||
|
||||
for (id, maybe_global_zindex, maybe_zindex) in root_node_query.iter() {
|
||||
for (id, maybe_global_zindex, maybe_zindex) in root_node_query.iter_many(ui_root_nodes.iter()) {
|
||||
root_nodes.push((
|
||||
id,
|
||||
(
|
||||
|
@ -71,6 +59,21 @@ pub fn ui_stack_system(
|
|||
maybe_zindex.map(|zindex| zindex.0).unwrap_or(0),
|
||||
),
|
||||
));
|
||||
visited_root_nodes.insert_unique_unchecked(id);
|
||||
}
|
||||
|
||||
for (id, global_zindex, maybe_zindex) in zindex_global_node_query.iter() {
|
||||
if visited_root_nodes.contains(&id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
root_nodes.push((
|
||||
id,
|
||||
(
|
||||
global_zindex.0,
|
||||
maybe_zindex.map(|zindex| zindex.0).unwrap_or(0),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
root_nodes.sort_by_key(|(_, z)| *z);
|
||||
|
@ -79,7 +82,7 @@ pub fn ui_stack_system(
|
|||
update_uistack_recursive(
|
||||
&mut cache,
|
||||
root_entity,
|
||||
&children_query,
|
||||
&ui_children,
|
||||
&zindex_query,
|
||||
&mut ui_stack.uinodes,
|
||||
);
|
||||
|
@ -95,26 +98,28 @@ pub fn ui_stack_system(
|
|||
fn update_uistack_recursive(
|
||||
cache: &mut ChildBufferCache,
|
||||
node_entity: Entity,
|
||||
children_query: &Query<&Children>,
|
||||
ui_children: &UiChildren,
|
||||
zindex_query: &Query<Option<&ZIndex>, (With<Node>, Without<GlobalZIndex>)>,
|
||||
ui_stack: &mut Vec<Entity>,
|
||||
) {
|
||||
ui_stack.push(node_entity);
|
||||
|
||||
if let Ok(children) = children_query.get(node_entity) {
|
||||
let mut child_buffer = cache.pop();
|
||||
child_buffer.extend(children.iter().filter_map(|child_entity| {
|
||||
zindex_query
|
||||
.get(*child_entity)
|
||||
.ok()
|
||||
.map(|zindex| (*child_entity, zindex.map(|zindex| zindex.0).unwrap_or(0)))
|
||||
}));
|
||||
child_buffer.sort_by_key(|k| k.1);
|
||||
for (child_entity, _) in child_buffer.drain(..) {
|
||||
update_uistack_recursive(cache, child_entity, children_query, zindex_query, ui_stack);
|
||||
}
|
||||
cache.push(child_buffer);
|
||||
let mut child_buffer = cache.pop();
|
||||
child_buffer.extend(
|
||||
ui_children
|
||||
.iter_ui_children(node_entity)
|
||||
.filter_map(|child_entity| {
|
||||
zindex_query
|
||||
.get(child_entity)
|
||||
.ok()
|
||||
.map(|zindex| (child_entity, zindex.map(|zindex| zindex.0).unwrap_or(0)))
|
||||
}),
|
||||
);
|
||||
child_buffer.sort_by_key(|k| k.1);
|
||||
for (child_entity, _) in child_buffer.drain(..) {
|
||||
update_uistack_recursive(cache, child_entity, ui_children, zindex_query, ui_stack);
|
||||
}
|
||||
cache.push(child_buffer);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
//! This module contains systems that update the UI when something changes
|
||||
|
||||
use crate::{CalculatedClip, Display, OverflowAxis, Style, TargetCamera};
|
||||
use crate::{CalculatedClip, Display, OverflowAxis, Style, TargetCamera, UiChildren, UiRootNodes};
|
||||
|
||||
use super::Node;
|
||||
use bevy_ecs::{
|
||||
entity::Entity,
|
||||
query::{Changed, With, Without},
|
||||
query::{Changed, With},
|
||||
system::{Commands, Query},
|
||||
};
|
||||
use bevy_hierarchy::{Children, Parent};
|
||||
use bevy_math::Rect;
|
||||
use bevy_transform::components::GlobalTransform;
|
||||
use bevy_utils::HashSet;
|
||||
|
@ -16,14 +15,14 @@ use bevy_utils::HashSet;
|
|||
/// Updates clipping for all nodes
|
||||
pub fn update_clipping_system(
|
||||
mut commands: Commands,
|
||||
root_node_query: Query<Entity, (With<Node>, Without<Parent>)>,
|
||||
root_nodes: UiRootNodes,
|
||||
mut node_query: Query<(&Node, &GlobalTransform, &Style, Option<&mut CalculatedClip>)>,
|
||||
children_query: Query<&Children>,
|
||||
ui_children: UiChildren,
|
||||
) {
|
||||
for root_node in &root_node_query {
|
||||
for root_node in root_nodes.iter() {
|
||||
update_clipping(
|
||||
&mut commands,
|
||||
&children_query,
|
||||
&ui_children,
|
||||
&mut node_query,
|
||||
root_node,
|
||||
None,
|
||||
|
@ -33,7 +32,7 @@ pub fn update_clipping_system(
|
|||
|
||||
fn update_clipping(
|
||||
commands: &mut Commands,
|
||||
children_query: &Query<&Children>,
|
||||
ui_children: &UiChildren,
|
||||
node_query: &mut Query<(&Node, &GlobalTransform, &Style, Option<&mut CalculatedClip>)>,
|
||||
entity: Entity,
|
||||
mut maybe_inherited_clip: Option<Rect>,
|
||||
|
@ -93,10 +92,8 @@ fn update_clipping(
|
|||
Some(maybe_inherited_clip.map_or(node_rect, |c| c.intersect(node_rect)))
|
||||
};
|
||||
|
||||
if let Ok(children) = children_query.get(entity) {
|
||||
for &child in children {
|
||||
update_clipping(commands, children_query, node_query, child, children_clip);
|
||||
}
|
||||
for child in ui_children.iter_ui_children(entity) {
|
||||
update_clipping(commands, ui_children, node_query, child, children_clip);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,11 +101,11 @@ pub fn update_target_camera_system(
|
|||
mut commands: Commands,
|
||||
changed_root_nodes_query: Query<
|
||||
(Entity, Option<&TargetCamera>),
|
||||
(With<Node>, Without<Parent>, Changed<TargetCamera>),
|
||||
(With<Node>, Changed<TargetCamera>),
|
||||
>,
|
||||
changed_children_query: Query<(Entity, Option<&TargetCamera>), (With<Node>, Changed<Children>)>,
|
||||
children_query: Query<&Children, With<Node>>,
|
||||
node_query: Query<Option<&TargetCamera>, With<Node>>,
|
||||
node_query: Query<(Entity, Option<&TargetCamera>), With<Node>>,
|
||||
ui_root_nodes: UiRootNodes,
|
||||
ui_children: UiChildren,
|
||||
) {
|
||||
// Track updated entities to prevent redundant updates, as `Commands` changes are deferred,
|
||||
// and updates done for changed_children_query can overlap with itself or with root_node_query
|
||||
|
@ -116,12 +113,12 @@ pub fn update_target_camera_system(
|
|||
|
||||
// Assuming that TargetCamera is manually set on the root node only,
|
||||
// update root nodes first, since it implies the biggest change
|
||||
for (root_node, target_camera) in &changed_root_nodes_query {
|
||||
for (root_node, target_camera) in changed_root_nodes_query.iter_many(ui_root_nodes.iter()) {
|
||||
update_children_target_camera(
|
||||
root_node,
|
||||
target_camera,
|
||||
&node_query,
|
||||
&children_query,
|
||||
&ui_children,
|
||||
&mut commands,
|
||||
&mut updated_entities,
|
||||
);
|
||||
|
@ -130,12 +127,16 @@ pub fn update_target_camera_system(
|
|||
// If the root node TargetCamera was changed, then every child is updated
|
||||
// by this point, and iteration will be skipped.
|
||||
// Otherwise, update changed children
|
||||
for (parent, target_camera) in &changed_children_query {
|
||||
for (parent, target_camera) in &node_query {
|
||||
if !ui_children.is_changed(parent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
update_children_target_camera(
|
||||
parent,
|
||||
target_camera,
|
||||
&node_query,
|
||||
&children_query,
|
||||
&ui_children,
|
||||
&mut commands,
|
||||
&mut updated_entities,
|
||||
);
|
||||
|
@ -145,19 +146,15 @@ pub fn update_target_camera_system(
|
|||
fn update_children_target_camera(
|
||||
entity: Entity,
|
||||
camera_to_set: Option<&TargetCamera>,
|
||||
node_query: &Query<Option<&TargetCamera>, With<Node>>,
|
||||
children_query: &Query<&Children, With<Node>>,
|
||||
node_query: &Query<(Entity, Option<&TargetCamera>), With<Node>>,
|
||||
ui_children: &UiChildren,
|
||||
commands: &mut Commands,
|
||||
updated_entities: &mut HashSet<Entity>,
|
||||
) {
|
||||
let Ok(children) = children_query.get(entity) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for &child in children {
|
||||
for child in ui_children.iter_ui_children(entity) {
|
||||
// Skip if the child has already been updated or update is not needed
|
||||
if updated_entities.contains(&child)
|
||||
|| camera_to_set == node_query.get(child).ok().flatten()
|
||||
|| camera_to_set == node_query.get(child).ok().and_then(|(_, camera)| camera)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
@ -176,7 +173,7 @@ fn update_children_target_camera(
|
|||
child,
|
||||
camera_to_set,
|
||||
node_query,
|
||||
children_query,
|
||||
ui_children,
|
||||
commands,
|
||||
updated_entities,
|
||||
);
|
||||
|
|
|
@ -496,6 +496,7 @@ Example | Description
|
|||
[Display and Visibility](../examples/ui/display_and_visibility.rs) | Demonstrates how Display and Visibility work in the UI.
|
||||
[Flex Layout](../examples/ui/flex_layout.rs) | Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text
|
||||
[Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally)
|
||||
[Ghost Nodes](../examples/ui/ghost_nodes.rs) | Demonstrates the use of Ghost Nodes to skip entities in the UI layout hierarchy
|
||||
[Overflow](../examples/ui/overflow.rs) | Simple example demonstrating overflow behavior
|
||||
[Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior
|
||||
[Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component
|
||||
|
|
122
examples/ui/ghost_nodes.rs
Normal file
122
examples/ui/ghost_nodes.rs
Normal file
|
@ -0,0 +1,122 @@
|
|||
//! This example demonstrates the use of Ghost Nodes.
|
||||
//!
|
||||
//! UI layout will ignore ghost nodes, and treat their children as if they were direct descendants of the first non-ghost ancestor.
|
||||
|
||||
use bevy::{prelude::*, ui::GhostNode, winit::WinitSettings};
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.insert_resource(WinitSettings::desktop_app())
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(Update, button_system)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct Counter(i32);
|
||||
|
||||
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
let font_handle = asset_server.load("fonts/FiraSans-Bold.ttf");
|
||||
|
||||
commands.spawn(Camera2dBundle::default());
|
||||
|
||||
// Ghost UI root
|
||||
commands.spawn(GhostNode).with_children(|ghost_root| {
|
||||
ghost_root
|
||||
.spawn(NodeBundle::default())
|
||||
.with_child(create_label(
|
||||
"This text node is rendered under a ghost root",
|
||||
font_handle.clone(),
|
||||
));
|
||||
});
|
||||
|
||||
// Normal UI root
|
||||
commands
|
||||
.spawn(NodeBundle {
|
||||
style: Style {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
align_items: AlignItems::Center,
|
||||
justify_content: JustifyContent::Center,
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
parent
|
||||
.spawn((NodeBundle::default(), Counter(0)))
|
||||
.with_children(|layout_parent| {
|
||||
layout_parent
|
||||
.spawn((GhostNode, Counter(0)))
|
||||
.with_children(|ghost_parent| {
|
||||
// Ghost children using a separate counter state
|
||||
// These buttons are being treated as children of layout_parent in the context of UI
|
||||
ghost_parent
|
||||
.spawn(create_button())
|
||||
.with_child(create_label("0", font_handle.clone()));
|
||||
ghost_parent
|
||||
.spawn(create_button())
|
||||
.with_child(create_label("0", font_handle.clone()));
|
||||
});
|
||||
|
||||
// A normal child using the layout parent counter
|
||||
layout_parent
|
||||
.spawn(create_button())
|
||||
.with_child(create_label("0", font_handle.clone()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn create_button() -> ButtonBundle {
|
||||
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: BorderRadius::MAX,
|
||||
background_color: Color::srgb(0.15, 0.15, 0.15).into(),
|
||||
..default()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_label(text: &str, font: Handle<Font>) -> TextBundle {
|
||||
TextBundle::from_section(
|
||||
text,
|
||||
TextStyle {
|
||||
font,
|
||||
font_size: 33.0,
|
||||
color: Color::srgb(0.9, 0.9, 0.9),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn button_system(
|
||||
mut interaction_query: Query<(&Interaction, &Parent), (Changed<Interaction>, With<Button>)>,
|
||||
labels_query: Query<(&Children, &Parent), With<Button>>,
|
||||
mut text_query: Query<&mut Text>,
|
||||
mut counter_query: Query<&mut Counter>,
|
||||
) {
|
||||
// Update parent counter on click
|
||||
for (interaction, parent) in &mut interaction_query {
|
||||
if matches!(interaction, Interaction::Pressed) {
|
||||
let mut counter = counter_query.get_mut(parent.get()).unwrap();
|
||||
counter.0 += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Update button labels to match their parent counter
|
||||
for (children, parent) in &labels_query {
|
||||
let counter = counter_query.get(parent.get()).unwrap();
|
||||
let mut text = text_query.get_mut(children[0]).unwrap();
|
||||
|
||||
text.sections[0].value = counter.0.to_string();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue