Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UI GhostNode #15341

Merged
merged 27 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
180b2c3
Add the GhostNode component
villor Sep 19, 2024
1a5b5ef
Add Ghost Nodes example
villor Sep 19, 2024
388bd79
Update UI hierarchy warning to include note about GhostNodes
villor Sep 19, 2024
cdbde25
Introduce UiRootNodes system param
villor Sep 19, 2024
07cfaab
Use UiRootNodes for clipping updates
villor Sep 19, 2024
32713bc
Update TODO
villor Sep 19, 2024
a421bb5
Add UiChildren system param
villor Sep 20, 2024
7d72f98
Add tests for UiRootNodes and UiChildren
villor Sep 20, 2024
a632b90
Clean up UiRootNodes a bit
villor Sep 20, 2024
afe50f9
Use UiChildren in ui_layout_system
villor Sep 20, 2024
27bc607
Add missing example in docs
villor Sep 20, 2024
d83ca58
Simplify UiRootNodes iteration to avoid iterating all UI nodes
villor Sep 21, 2024
33a1af4
Ensure only Nodes are included in UiRootNodes iteration
villor Sep 21, 2024
110c600
Add change detection for UI children under ghost nodes
villor Sep 21, 2024
fbd1404
Use UiChildren in accessibility
villor Sep 21, 2024
875af25
Implement get_parent() and use it in bevy_ui::render
villor Sep 21, 2024
35cb12f
Use UiChildren in bevy_ui::stack
villor Sep 21, 2024
40d26b4
Update doc comment
villor Sep 21, 2024
a2f2bd2
Use UiChildren/UiRootNodes in bevy_ui::update
villor Sep 21, 2024
3165d8f
Merge branch 'main' into ui-ghost-nodes
villor Sep 28, 2024
a7590e0
Fix clippy warning
villor Sep 28, 2024
8ffd8d5
Fix bug where nested ghost nodes would iterate children in reverse
villor Sep 28, 2024
30be253
Simplify required components for GhostNode
villor Sep 29, 2024
7a09a32
Merge branch 'main' into ui-ghost-nodes
villor Oct 1, 2024
a7decfb
Apply suggested example docs
villor Sep 30, 2024
637f301
Apply doc suggestions from @UkoeHB
villor Oct 1, 2024
e85d422
Reduce allocations in ui_surface::update_children by using a scratch vec
villor Oct 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2965,6 +2965,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"
Expand Down
24 changes: 11 additions & 13 deletions crates/bevy_ui/src/accessibility.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
prelude::{Button, Label},
Node, UiImage,
Node, UiChildren, UiImage,
};
use bevy_a11y::{
accesskit::{NodeBuilder, Rect, Role},
Expand All @@ -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()
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
201 changes: 201 additions & 0 deletions crates/bevy_ui/src/ghost_hierarchy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
//! 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 UI children utilities, skipping over [`GhostNode`].
villor marked this conversation as resolved.
Show resolved Hide resolved
#[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 UI children of `entity`, skipping over [`GhostNode`].
///
/// Traverses the hierarchy depth-first to ensure child order.
pub fn iter_ui_children(&'s self, entity: Entity) -> UiChildrenIter<'w, 's> {
villor marked this conversation as resolved.
Show resolved Hide resolved
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());
}
}
Loading