Filter UI traversal to only Node and GhostNode (#15746)

# Objective

With the warning removed in
https://github.com/bevyengine/bevy/pull/15736, the rules for the UI tree
changes.
We no longer need to traverse non `Node`/`GhostNode` entities.

## Solution

- Added a filter `Or<(With<Node>, With<GhostNode>)>` to the child
traversal query so we don't unnecessarily traverse nodes that are not
part of the UI tree (like text nodes).
- Also moved the warning for NoUI->UI entities so it is actually
triggered (see comments)

## Testing

- Ran unit tests (still passing)
- Ran the ghost_nodes and ui examples, still works and looks fine 👍 
- Tested the warning by spawning a Node under an empty entity.

---

---------

Co-authored-by: UkoeHB <37489173+UkoeHB@users.noreply.github.com>
This commit is contained in:
Viktor Gustavsson 2024-10-13 19:25:15 +02:00 committed by GitHub
parent 0e30b68b20
commit 5989a845f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 44 additions and 18 deletions

View file

@ -44,7 +44,12 @@ impl<'w, 's> UiRootNodes<'w, 's> {
/// 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>)>,
ui_children_query: Query<
'w,
's,
(Option<&'static Children>, Has<GhostNode>),
Or<(With<Node>, With<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>>,
@ -101,11 +106,21 @@ impl<'w, 's> UiChildren<'w, 's> {
.iter_ghost_nodes(entity)
.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)
}
}
pub struct UiChildrenIter<'w, 's> {
stack: SmallVec<[Entity; 8]>,
query: &'s Query<'w, 's, (Option<&'static Children>, Option<&'static GhostNode>)>,
query: &'s Query<
'w,
's,
(Option<&'static Children>, Has<GhostNode>),
Or<(With<Node>, With<GhostNode>)>,
>,
}
impl<'w, 's> Iterator for UiChildrenIter<'w, 's> {
@ -113,12 +128,13 @@ impl<'w, 's> Iterator for UiChildrenIter<'w, 's> {
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());
if let Ok((children, has_ghost_node)) = self.query.get(entity) {
if !has_ghost_node {
return Some(entity);
}
if let Some(children) = children {
self.stack.extend(children.iter().rev().copied());
}
}
}
}
@ -186,10 +202,12 @@ mod tests {
let n9 = world.spawn((A(9), GhostNode)).id();
let n10 = world.spawn((A(10), NodeBundle::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, n9]);
world.entity_mut(n6).add_children(&[n7, no_ui, n9]);
world.entity_mut(n7).add_children(&[n8]);
world.entity_mut(n9).add_children(&[n10]);

View file

@ -11,7 +11,7 @@ use bevy_ecs::{
system::{Commands, Local, Query, Res, ResMut, SystemParam},
world::Ref,
};
use bevy_hierarchy::Children;
use bevy_hierarchy::{Children, Parent};
use bevy_math::{UVec2, Vec2};
use bevy_render::camera::{Camera, NormalizedRenderTarget};
use bevy_sprite::BorderRect;
@ -114,7 +114,7 @@ pub fn ui_layout_system(
),
With<Node>,
>,
node_query: Query<Entity, With<Node>>,
node_query: Query<(Entity, Option<Ref<Parent>>), With<Node>>,
ui_children: UiChildren,
mut removed_components: UiLayoutSystemRemovedComponentParam,
mut node_transform_query: Query<(
@ -249,7 +249,19 @@ pub fn ui_layout_system(
ui_surface.try_remove_children(entity);
}
node_query.iter().for_each(|entity| {
node_query.iter().for_each(|(entity, maybe_parent)| {
if let Some(parent) = maybe_parent {
// 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()) {
warn!(
"Styled child ({entity}) 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."
);
}
}
if ui_children.is_changed(entity) {
ui_surface.update_children(entity, ui_children.iter_ui_children(entity));
}
@ -261,7 +273,7 @@ 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
node_query.iter().for_each(|entity| {
node_query.iter().for_each(|(entity, _)| {
if ui_children.is_changed(entity) {
ui_surface.update_children(entity, ui_children.iter_ui_children(entity));
}

View file

@ -7,7 +7,7 @@ use bevy_ecs::{
prelude::Resource,
};
use bevy_math::UVec2;
use bevy_utils::{default, tracing::warn};
use bevy_utils::default;
use crate::{
layout::convert, LayoutContext, LayoutError, Measure, MeasureArgs, NodeMeasure, Style,
@ -289,10 +289,6 @@ impl UiSurface {
.layout(*taffy_node)
.map_err(LayoutError::TaffyError)
} else {
warn!(
"Styled child ({entity}) 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."
);
Err(LayoutError::InvalidHierarchy)
}
}