Skip to content

Commit f86ee32

Browse files
villoralice-i-cecileUkoeHB
authored
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](#15341 (comment)) ## 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 <[email protected]> Co-authored-by: UkoeHB <[email protected]>
1 parent 3df281b commit f86ee32

File tree

10 files changed

+482
-135
lines changed

10 files changed

+482
-135
lines changed

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2972,6 +2972,17 @@ description = "Demonstrates text wrapping"
29722972
category = "UI (User Interface)"
29732973
wasm = true
29742974

2975+
[[example]]
2976+
name = "ghost_nodes"
2977+
path = "examples/ui/ghost_nodes.rs"
2978+
doc-scrape-examples = true
2979+
2980+
[package.metadata.example.ghost_nodes]
2981+
name = "Ghost Nodes"
2982+
description = "Demonstrates the use of Ghost Nodes to skip entities in the UI layout hierarchy"
2983+
category = "UI (User Interface)"
2984+
wasm = true
2985+
29752986
[[example]]
29762987
name = "grid"
29772988
path = "examples/ui/grid.rs"

crates/bevy_ui/src/accessibility.rs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
prelude::{Button, Label},
3-
Node, UiImage,
3+
Node, UiChildren, UiImage,
44
};
55
use bevy_a11y::{
66
accesskit::{NodeBuilder, Rect, Role},
@@ -14,15 +14,14 @@ use bevy_ecs::{
1414
system::{Commands, Query},
1515
world::Ref,
1616
};
17-
use bevy_hierarchy::Children;
1817
use bevy_render::{camera::CameraUpdateSystem, prelude::Camera};
1918
use bevy_text::Text;
2019
use bevy_transform::prelude::GlobalTransform;
2120

22-
fn calc_name(texts: &Query<&Text>, children: &Children) -> Option<Box<str>> {
21+
fn calc_name(texts: &Query<&Text>, children: impl Iterator<Item = Entity>) -> Option<Box<str>> {
2322
let mut name = None;
2423
for child in children {
25-
if let Ok(text) = texts.get(*child) {
24+
if let Ok(text) = texts.get(child) {
2625
let values = text
2726
.sections
2827
.iter()
@@ -59,11 +58,12 @@ fn calc_bounds(
5958

6059
fn button_changed(
6160
mut commands: Commands,
62-
mut query: Query<(Entity, &Children, Option<&mut AccessibilityNode>), Changed<Button>>,
61+
mut query: Query<(Entity, Option<&mut AccessibilityNode>), Changed<Button>>,
62+
ui_children: UiChildren,
6363
texts: Query<&Text>,
6464
) {
65-
for (entity, children, accessible) in &mut query {
66-
let name = calc_name(&texts, children);
65+
for (entity, accessible) in &mut query {
66+
let name = calc_name(&texts, ui_children.iter_ui_children(entity));
6767
if let Some(mut accessible) = accessible {
6868
accessible.set_role(Role::Button);
6969
if let Some(name) = name {
@@ -85,14 +85,12 @@ fn button_changed(
8585

8686
fn image_changed(
8787
mut commands: Commands,
88-
mut query: Query<
89-
(Entity, &Children, Option<&mut AccessibilityNode>),
90-
(Changed<UiImage>, Without<Button>),
91-
>,
88+
mut query: Query<(Entity, Option<&mut AccessibilityNode>), (Changed<UiImage>, Without<Button>)>,
89+
ui_children: UiChildren,
9290
texts: Query<&Text>,
9391
) {
94-
for (entity, children, accessible) in &mut query {
95-
let name = calc_name(&texts, children);
92+
for (entity, accessible) in &mut query {
93+
let name = calc_name(&texts, ui_children.iter_ui_children(entity));
9694
if let Some(mut accessible) = accessible {
9795
accessible.set_role(Role::Image);
9896
if let Some(name) = name {
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
//! This module contains [`GhostNode`] and utilities to flatten the UI hierarchy, traversing past ghost nodes.
2+
3+
use bevy_ecs::{prelude::*, system::SystemParam};
4+
use bevy_hierarchy::{Children, HierarchyQueryExt, Parent};
5+
use bevy_reflect::prelude::*;
6+
use bevy_render::view::Visibility;
7+
use bevy_transform::prelude::Transform;
8+
use smallvec::SmallVec;
9+
10+
use crate::Node;
11+
12+
/// Marker component for entities that should be ignored within UI hierarchies.
13+
///
14+
/// The UI systems will traverse past these and treat their first non-ghost descendants as direct children of their first non-ghost ancestor.
15+
///
16+
/// Any components necessary for transform and visibility propagation will be added automatically.
17+
#[derive(Component, Default, Debug, Copy, Clone, Reflect)]
18+
#[reflect(Component, Debug)]
19+
#[require(Visibility, Transform)]
20+
pub struct GhostNode;
21+
22+
/// System param that allows iteration of all UI root nodes.
23+
///
24+
/// A UI root node is either a [`Node`] without a [`Parent`], or with only [`GhostNode`] ancestors.
25+
#[derive(SystemParam)]
26+
pub struct UiRootNodes<'w, 's> {
27+
root_node_query: Query<'w, 's, Entity, (With<Node>, Without<Parent>)>,
28+
root_ghost_node_query: Query<'w, 's, Entity, (With<GhostNode>, Without<Parent>)>,
29+
all_nodes_query: Query<'w, 's, Entity, With<Node>>,
30+
ui_children: UiChildren<'w, 's>,
31+
}
32+
33+
impl<'w, 's> UiRootNodes<'w, 's> {
34+
pub fn iter(&'s self) -> impl Iterator<Item = Entity> + 's {
35+
self.root_node_query
36+
.iter()
37+
.chain(self.root_ghost_node_query.iter().flat_map(|root_ghost| {
38+
self.all_nodes_query
39+
.iter_many(self.ui_children.iter_ui_children(root_ghost))
40+
}))
41+
}
42+
}
43+
44+
/// System param that gives access to UI children utilities, skipping over [`GhostNode`].
45+
#[derive(SystemParam)]
46+
pub struct UiChildren<'w, 's> {
47+
ui_children_query: Query<'w, 's, (Option<&'static Children>, Option<&'static GhostNode>)>,
48+
changed_children_query: Query<'w, 's, Entity, Changed<Children>>,
49+
children_query: Query<'w, 's, &'static Children>,
50+
ghost_nodes_query: Query<'w, 's, Entity, With<GhostNode>>,
51+
parents_query: Query<'w, 's, &'static Parent>,
52+
}
53+
54+
impl<'w, 's> UiChildren<'w, 's> {
55+
/// Iterates the children of `entity`, skipping over [`GhostNode`].
56+
///
57+
/// Traverses the hierarchy depth-first to ensure child order.
58+
///
59+
/// # Performance
60+
///
61+
/// This iterator allocates if the `entity` node has more than 8 children (including ghost nodes).
62+
pub fn iter_ui_children(&'s self, entity: Entity) -> UiChildrenIter<'w, 's> {
63+
UiChildrenIter {
64+
stack: self
65+
.ui_children_query
66+
.get(entity)
67+
.map_or(SmallVec::new(), |(children, _)| {
68+
children.into_iter().flatten().rev().copied().collect()
69+
}),
70+
query: &self.ui_children_query,
71+
}
72+
}
73+
74+
/// Returns the UI parent of the provided entity, skipping over [`GhostNode`].
75+
pub fn get_parent(&'s self, entity: Entity) -> Option<Entity> {
76+
self.parents_query
77+
.iter_ancestors(entity)
78+
.find(|entity| !self.ghost_nodes_query.contains(*entity))
79+
}
80+
81+
/// Iterates the [`GhostNode`]s between this entity and its UI children.
82+
pub fn iter_ghost_nodes(&'s self, entity: Entity) -> Box<dyn Iterator<Item = Entity> + 's> {
83+
Box::new(
84+
self.children_query
85+
.get(entity)
86+
.into_iter()
87+
.flat_map(|children| {
88+
self.ghost_nodes_query
89+
.iter_many(children)
90+
.flat_map(|entity| {
91+
core::iter::once(entity).chain(self.iter_ghost_nodes(entity))
92+
})
93+
}),
94+
)
95+
}
96+
97+
/// 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.
98+
pub fn is_changed(&'s self, entity: Entity) -> bool {
99+
self.changed_children_query.contains(entity)
100+
|| self
101+
.iter_ghost_nodes(entity)
102+
.any(|entity| self.changed_children_query.contains(entity))
103+
}
104+
}
105+
106+
pub struct UiChildrenIter<'w, 's> {
107+
stack: SmallVec<[Entity; 8]>,
108+
query: &'s Query<'w, 's, (Option<&'static Children>, Option<&'static GhostNode>)>,
109+
}
110+
111+
impl<'w, 's> Iterator for UiChildrenIter<'w, 's> {
112+
type Item = Entity;
113+
fn next(&mut self) -> Option<Self::Item> {
114+
loop {
115+
let entity = self.stack.pop()?;
116+
let (children, ghost_node) = self.query.get(entity).ok()?;
117+
if ghost_node.is_none() {
118+
return Some(entity);
119+
}
120+
if let Some(children) = children {
121+
self.stack.extend(children.iter().rev().copied());
122+
}
123+
}
124+
}
125+
}
126+
127+
#[cfg(test)]
128+
mod tests {
129+
use bevy_ecs::{
130+
prelude::Component,
131+
system::{Query, SystemState},
132+
world::World,
133+
};
134+
use bevy_hierarchy::{BuildChildren, ChildBuild};
135+
136+
use super::{GhostNode, UiChildren, UiRootNodes};
137+
use crate::prelude::NodeBundle;
138+
139+
#[derive(Component, PartialEq, Debug)]
140+
struct A(usize);
141+
142+
#[test]
143+
fn iterate_ui_root_nodes() {
144+
let world = &mut World::new();
145+
146+
// Normal root
147+
world
148+
.spawn((A(1), NodeBundle::default()))
149+
.with_children(|parent| {
150+
parent.spawn((A(2), NodeBundle::default()));
151+
parent
152+
.spawn((A(3), GhostNode))
153+
.with_child((A(4), NodeBundle::default()));
154+
});
155+
156+
// Ghost root
157+
world.spawn((A(5), GhostNode)).with_children(|parent| {
158+
parent.spawn((A(6), NodeBundle::default()));
159+
parent
160+
.spawn((A(7), GhostNode))
161+
.with_child((A(8), NodeBundle::default()))
162+
.with_child(A(9));
163+
});
164+
165+
let mut system_state = SystemState::<(UiRootNodes, Query<&A>)>::new(world);
166+
let (ui_root_nodes, a_query) = system_state.get(world);
167+
168+
let result: Vec<_> = a_query.iter_many(ui_root_nodes.iter()).collect();
169+
170+
assert_eq!([&A(1), &A(6), &A(8)], result.as_slice());
171+
}
172+
173+
#[test]
174+
fn iterate_ui_children() {
175+
let world = &mut World::new();
176+
177+
let n1 = world.spawn((A(1), NodeBundle::default())).id();
178+
let n2 = world.spawn((A(2), GhostNode)).id();
179+
let n3 = world.spawn((A(3), GhostNode)).id();
180+
let n4 = world.spawn((A(4), NodeBundle::default())).id();
181+
let n5 = world.spawn((A(5), NodeBundle::default())).id();
182+
183+
let n6 = world.spawn((A(6), GhostNode)).id();
184+
let n7 = world.spawn((A(7), GhostNode)).id();
185+
let n8 = world.spawn((A(8), NodeBundle::default())).id();
186+
let n9 = world.spawn((A(9), GhostNode)).id();
187+
let n10 = world.spawn((A(10), NodeBundle::default())).id();
188+
189+
world.entity_mut(n1).add_children(&[n2, n3, n4, n6]);
190+
world.entity_mut(n2).add_children(&[n5]);
191+
192+
world.entity_mut(n6).add_children(&[n7, n9]);
193+
world.entity_mut(n7).add_children(&[n8]);
194+
world.entity_mut(n9).add_children(&[n10]);
195+
196+
let mut system_state = SystemState::<(UiChildren, Query<&A>)>::new(world);
197+
let (ui_children, a_query) = system_state.get(world);
198+
199+
let result: Vec<_> = a_query
200+
.iter_many(ui_children.iter_ui_children(n1))
201+
.collect();
202+
203+
assert_eq!([&A(5), &A(4), &A(8), &A(10)], result.as_slice());
204+
}
205+
}

0 commit comments

Comments
 (0)