Skip to content
6 changes: 3 additions & 3 deletions crates/bevy_ui/src/accessibility.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
experimental::UiChildren,
navigation::UiChildren,
prelude::{Button, Label},
widget::{ImageNode, TextUiReader},
ComputedNode,
Expand Down Expand Up @@ -69,7 +69,7 @@ fn button_changed(
mut text_reader: TextUiReader,
) {
for (entity, accessible) in &mut query {
let label = calc_label(&mut text_reader, ui_children.iter_ui_children(entity));
let label = calc_label(&mut text_reader, ui_children.iter_actual_children(entity));
if let Some(mut accessible) = accessible {
accessible.set_role(Role::Button);
if let Some(name) = label {
Expand Down Expand Up @@ -99,7 +99,7 @@ fn image_changed(
mut text_reader: TextUiReader,
) {
for (entity, accessible) in &mut query {
let label = calc_label(&mut text_reader, ui_children.iter_ui_children(entity));
let label = calc_label(&mut text_reader, ui_children.iter_actual_children(entity));
if let Some(mut accessible) = accessible {
accessible.set_role(Role::Image);
if let Some(label) = label {
Expand Down
252 changes: 62 additions & 190 deletions crates/bevy_ui/src/experimental/ghost_hierarchy.rs
Original file line number Diff line number Diff line change
@@ -1,110 +1,91 @@
//! This module contains [`GhostNode`] and utilities to flatten the UI hierarchy, traversing past ghost nodes.
//! This module contains utilities to flatten a hierarchy using ghost nodes, traversing past ghost nodes automatically.

#[cfg(feature = "ghost_nodes")]
use crate::ui_node::ComputedNodeTarget;
use crate::Node;
use bevy_ecs::{prelude::*, system::SystemParam};
#[cfg(feature = "ghost_nodes")]
use bevy_reflect::prelude::*;
#[cfg(feature = "ghost_nodes")]
use bevy_render::view::Visibility;
#[cfg(feature = "ghost_nodes")]
use bevy_transform::prelude::Transform;
#[cfg(feature = "ghost_nodes")]
use smallvec::SmallVec;
/// 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.
#[cfg(feature = "ghost_nodes")]
#[derive(Component, Debug, Copy, Clone, Reflect)]
#[cfg_attr(feature = "ghost_nodes", derive(Default))]
#[reflect(Component, Debug)]
#[require(Visibility, Transform, ComputedNodeTarget)]
pub struct GhostNode;

#[cfg(feature = "ghost_nodes")]
/// System param that allows iteration of all UI root nodes.
/// Allows a hierarchy of entities that all have a component implementing `GhostNode` to be flattened
/// to just a tree of only the entities with an `Actual` component.
pub trait GhostNode {
type Actual: Component;
}

/// System param that allows iteration of all actual root nodes.
///
/// A UI root node is either a [`Node`] without a [`ChildOf`], or with only [`GhostNode`] ancestors.
/// An root node is either an `Actual` node without a [`ChildOf`], or with only ghost ancestor nodes.
#[derive(SystemParam)]
pub struct UiRootNodes<'w, 's> {
root_node_query: Query<'w, 's, Entity, (With<Node>, Without<ChildOf>)>,
root_ghost_node_query: Query<'w, 's, Entity, (With<GhostNode>, Without<ChildOf>)>,
all_nodes_query: Query<'w, 's, Entity, With<Node>>,
ui_children: UiChildren<'w, 's>,
pub struct GhostRootNodes<'w, 's, G>
where
G: GhostNode + Component,
<G as GhostNode>::Actual: Component,
{
root_node_query: Query<'w, 's, Entity, (With<<G as GhostNode>::Actual>, Without<ChildOf>)>,
root_ghost_node_query:
Query<'w, 's, Entity, (With<G>, Without<<G as GhostNode>::Actual>, Without<ChildOf>)>,
all_nodes_query: Query<'w, 's, Entity, With<<G as GhostNode>::Actual>>,
ui_children: GhostChildren<'w, 's, G>,
}

#[cfg(not(feature = "ghost_nodes"))]
pub type UiRootNodes<'w, 's> = Query<'w, 's, Entity, (With<Node>, Without<ChildOf>)>;

#[cfg(feature = "ghost_nodes")]
impl<'w, 's> UiRootNodes<'w, 's> {
impl<'w, 's, G> GhostRootNodes<'w, 's, G>
where
G: GhostNode + Component,
{
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))
.iter_many(self.ui_children.iter_actual_children(root_ghost))
}))
}
}

#[cfg(feature = "ghost_nodes")]
/// System param that gives access to UI children utilities, skipping over [`GhostNode`].
/// System param that gives access to UI children utilities, skipping over non-actual nodes.
#[derive(SystemParam)]
pub struct UiChildren<'w, 's> {
ui_children_query: Query<
'w,
's,
(Option<&'static Children>, Has<GhostNode>),
Or<(With<Node>, With<GhostNode>)>,
>,
pub struct GhostChildren<'w, 's, G>
where
G: GhostNode + Component,
<G as GhostNode>::Actual: Component,
{
actual_children_query:
Query<'w, 's, (Option<&'static Children>, Has<<G as GhostNode>::Actual>), With<G>>,
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 ChildOf>,
}

#[cfg(not(feature = "ghost_nodes"))]
/// System param that gives access to UI children utilities.
#[derive(SystemParam)]
pub struct UiChildren<'w, 's> {
ui_children_query: Query<'w, 's, Option<&'static Children>, With<Node>>,
changed_children_query: Query<'w, 's, Entity, Changed<Children>>,
ghost_nodes_query: Query<'w, 's, Entity, (With<G>, Without<<G as GhostNode>::Actual>)>,
parents_query: Query<'w, 's, &'static ChildOf>,
}

#[cfg(feature = "ghost_nodes")]
impl<'w, 's> UiChildren<'w, 's> {
/// Iterates the children of `entity`, skipping over [`GhostNode`].
impl<'w, 's, G> GhostChildren<'w, 's, G>
where
G: GhostNode + Component,
<G as GhostNode>::Actual: Component,
{
/// Iterates the children of `entity`, skipping over non-actual nodes.
///
/// 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 {
/// This iterator allocates if the `entity` node has more than 8 children (including ghosts).
pub fn iter_actual_children(&'s self, entity: Entity) -> GhostChildrenIter<'w, 's, G> {
GhostChildrenIter {
stack: self
.ui_children_query
.actual_children_query
.get(entity)
.map_or(SmallVec::new(), |(children, _)| {
children.into_iter().flatten().rev().copied().collect()
}),
query: &self.ui_children_query,
query: &self.actual_children_query,
}
}

/// Returns the UI parent of the provided entity, skipping over [`GhostNode`].
/// Returns the UI parent of the provided entity, skipping over non-actual nodes.
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.
/// Iterates the ghosts between this entity and its actual children.
pub fn iter_ghost_nodes(&'s self, entity: Entity) -> Box<dyn Iterator<Item = Entity> + 's> {
Box::new(
self.children_query
Expand All @@ -128,61 +109,32 @@ impl<'w, 's> UiChildren<'w, 's> {
.any(|entity| self.changed_children_query.contains(entity))
}

/// Returns `true` if the given entity is either a [`Node`] or a [`GhostNode`].
pub fn is_ui_node(&'s self, entity: Entity) -> bool {
self.ui_children_query.contains(entity)
/// Returns `true` if the given entity is an actual node.
pub fn is_actual(&'s self, entity: Entity) -> bool {
self.actual_children_query.contains(entity)
}
}

#[cfg(not(feature = "ghost_nodes"))]
impl<'w, 's> UiChildren<'w, 's> {
/// Iterates the children of `entity`.
pub fn iter_ui_children(&'s self, entity: Entity) -> impl Iterator<Item = Entity> + 's {
self.ui_children_query
.get(entity)
.ok()
.flatten()
.map(|children| children.as_ref())
.unwrap_or(&[])
.iter()
.copied()
}

/// Returns the UI parent of the provided entity.
pub fn get_parent(&'s self, entity: Entity) -> Option<Entity> {
self.parents_query.get(entity).ok().map(|parent| parent.0)
}

/// 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)
}

/// Returns `true` if the given entity is either a [`Node`] or a [`GhostNode`].
pub fn is_ui_node(&'s self, entity: Entity) -> bool {
self.ui_children_query.contains(entity)
}
}

#[cfg(feature = "ghost_nodes")]
pub struct UiChildrenIter<'w, 's> {
pub struct GhostChildrenIter<'w, 's, G>
where
G: GhostNode + Component,
<G as GhostNode>::Actual: Component,
{
stack: SmallVec<[Entity; 8]>,
query: &'s Query<
'w,
's,
(Option<&'static Children>, Has<GhostNode>),
Or<(With<Node>, With<GhostNode>)>,
>,
query: &'s Query<'w, 's, (Option<&'static Children>, Has<G::Actual>), With<G>>,
}

#[cfg(feature = "ghost_nodes")]
impl<'w, 's> Iterator for UiChildrenIter<'w, 's> {
impl<'w, 's, G> Iterator for GhostChildrenIter<'w, 's, G>
where
G: GhostNode + Component,
<G as GhostNode>::Actual: Component,
{
type Item = Entity;
fn next(&mut self) -> Option<Self::Item> {
loop {
let entity = self.stack.pop()?;
if let Ok((children, has_ghost_node)) = self.query.get(entity) {
if !has_ghost_node {
if let Ok((children, has_node)) = self.query.get(entity) {
if has_node {
return Some(entity);
}
if let Some(children) = children {
Expand All @@ -192,83 +144,3 @@ impl<'w, 's> Iterator for UiChildrenIter<'w, 's> {
}
}
}

#[cfg(all(test, feature = "ghost_nodes"))]
mod tests {
use bevy_ecs::{
prelude::Component,
system::{Query, SystemState},
world::World,
};

use super::{GhostNode, Node, UiChildren, UiRootNodes};

#[derive(Component, PartialEq, Debug)]
struct A(usize);

#[test]
fn iterate_ui_root_nodes() {
let world = &mut World::new();

// Normal root
world
.spawn((A(1), Node::default()))
.with_children(|parent| {
parent.spawn((A(2), Node::default()));
parent
.spawn((A(3), GhostNode))
.with_child((A(4), Node::default()));
});

// Ghost root
world.spawn((A(5), GhostNode)).with_children(|parent| {
parent.spawn((A(6), Node::default()));
parent
.spawn((A(7), GhostNode))
.with_child((A(8), Node::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), Node::default())).id();
let n2 = world.spawn((A(2), GhostNode)).id();
let n3 = world.spawn((A(3), GhostNode)).id();
let n4 = world.spawn((A(4), Node::default())).id();
let n5 = world.spawn((A(5), Node::default())).id();

let n6 = world.spawn((A(6), GhostNode)).id();
let n7 = world.spawn((A(7), GhostNode)).id();
let n8 = world.spawn((A(8), Node::default())).id();
let n9 = world.spawn((A(9), GhostNode)).id();
let n10 = world.spawn((A(10), Node::default())).id();

let no_ui = world.spawn_empty().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, no_ui, 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());
}
}
10 changes: 5 additions & 5 deletions crates/bevy_ui/src/layout/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
experimental::{UiChildren, UiRootNodes},
navigation::{UiChildren, UiRootNodes},
BorderRadius, ComputedNode, ComputedNodeTarget, ContentSize, Display, LayoutConfig, Node,
Outline, OverflowAxis, ScrollPosition, Val,
};
Expand Down Expand Up @@ -132,7 +132,7 @@ pub fn ui_layout_system(
// Note: This does not cover the case where a parent's Node component was removed.
// Users are responsible for fixing hierarchies if they do that (it is not recommended).
// Detecting it here would be a permanent perf burden on the hot path.
if parent.is_changed() && !ui_children.is_ui_node(parent.get()) {
if parent.is_changed() && !ui_children.is_actual(parent.get()) {
warn!(
"Node ({entity}) is in a non-UI entity hierarchy. You are using an entity \
with UI components as a child of an entity without UI components, your UI layout may be broken."
Expand All @@ -141,7 +141,7 @@ with UI components as a child of an entity without UI components, your UI layout
}

if ui_children.is_changed(entity) {
ui_surface.update_children(entity, ui_children.iter_ui_children(entity));
ui_surface.update_children(entity, ui_children.iter_actual_children(entity));
}
});

Expand All @@ -155,7 +155,7 @@ with UI components as a child of an entity without UI components, your UI layout
// Re-sync changed children: avoid layout glitches caused by removed nodes that are still set as a child of another node
computed_node_query.iter().for_each(|(entity, _)| {
if ui_children.is_changed(entity) {
ui_surface.update_children(entity, ui_children.iter_ui_children(entity));
ui_surface.update_children(entity, ui_children.iter_actual_children(entity));
}
});

Expand Down Expand Up @@ -324,7 +324,7 @@ with UI components as a child of an entity without UI components, your UI layout
let physical_scroll_position =
(clamped_scroll_position / inverse_target_scale_factor).round();

for child_uinode in ui_children.iter_ui_children(entity) {
for child_uinode in ui_children.iter_actual_children(entity) {
update_uinode_geometry_recursive(
commands,
child_uinode,
Expand Down
Loading