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:
Viktor Gustavsson 2024-10-02 02:24:28 +02:00 committed by GitHub
parent 3df281ba7b
commit f86ee32576
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 488 additions and 141 deletions

View file

@ -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"

View file

@ -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 {

View 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());
}
}

View file

@ -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,
);
}
}
}

View file

@ -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,

View file

@ -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::*;

View file

@ -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)]

View file

@ -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,
);

View file

@ -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
View 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();
}
}