mirror of
https://github.com/bevyengine/bevy
synced 2024-11-25 06:00:20 +00:00
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:
parent
3df281ba7b
commit
f86ee32576
10 changed files with 488 additions and 141 deletions
11
Cargo.toml
11
Cargo.toml
|
@ -2972,6 +2972,17 @@ description = "Demonstrates text wrapping"
|
||||||
category = "UI (User Interface)"
|
category = "UI (User Interface)"
|
||||||
wasm = true
|
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]]
|
[[example]]
|
||||||
name = "grid"
|
name = "grid"
|
||||||
path = "examples/ui/grid.rs"
|
path = "examples/ui/grid.rs"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
prelude::{Button, Label},
|
prelude::{Button, Label},
|
||||||
Node, UiImage,
|
Node, UiChildren, UiImage,
|
||||||
};
|
};
|
||||||
use bevy_a11y::{
|
use bevy_a11y::{
|
||||||
accesskit::{NodeBuilder, Rect, Role},
|
accesskit::{NodeBuilder, Rect, Role},
|
||||||
|
@ -14,15 +14,14 @@ use bevy_ecs::{
|
||||||
system::{Commands, Query},
|
system::{Commands, Query},
|
||||||
world::Ref,
|
world::Ref,
|
||||||
};
|
};
|
||||||
use bevy_hierarchy::Children;
|
|
||||||
use bevy_render::{camera::CameraUpdateSystem, prelude::Camera};
|
use bevy_render::{camera::CameraUpdateSystem, prelude::Camera};
|
||||||
use bevy_text::Text;
|
use bevy_text::Text;
|
||||||
use bevy_transform::prelude::GlobalTransform;
|
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;
|
let mut name = None;
|
||||||
for child in children {
|
for child in children {
|
||||||
if let Ok(text) = texts.get(*child) {
|
if let Ok(text) = texts.get(child) {
|
||||||
let values = text
|
let values = text
|
||||||
.sections
|
.sections
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -59,11 +58,12 @@ fn calc_bounds(
|
||||||
|
|
||||||
fn button_changed(
|
fn button_changed(
|
||||||
mut commands: Commands,
|
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>,
|
texts: Query<&Text>,
|
||||||
) {
|
) {
|
||||||
for (entity, children, accessible) in &mut query {
|
for (entity, accessible) in &mut query {
|
||||||
let name = calc_name(&texts, children);
|
let name = calc_name(&texts, ui_children.iter_ui_children(entity));
|
||||||
if let Some(mut accessible) = accessible {
|
if let Some(mut accessible) = accessible {
|
||||||
accessible.set_role(Role::Button);
|
accessible.set_role(Role::Button);
|
||||||
if let Some(name) = name {
|
if let Some(name) = name {
|
||||||
|
@ -85,14 +85,12 @@ fn button_changed(
|
||||||
|
|
||||||
fn image_changed(
|
fn image_changed(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut query: Query<
|
mut query: Query<(Entity, Option<&mut AccessibilityNode>), (Changed<UiImage>, Without<Button>)>,
|
||||||
(Entity, &Children, Option<&mut AccessibilityNode>),
|
ui_children: UiChildren,
|
||||||
(Changed<UiImage>, Without<Button>),
|
|
||||||
>,
|
|
||||||
texts: Query<&Text>,
|
texts: Query<&Text>,
|
||||||
) {
|
) {
|
||||||
for (entity, children, accessible) in &mut query {
|
for (entity, accessible) in &mut query {
|
||||||
let name = calc_name(&texts, children);
|
let name = calc_name(&texts, ui_children.iter_ui_children(entity));
|
||||||
if let Some(mut accessible) = accessible {
|
if let Some(mut accessible) = accessible {
|
||||||
accessible.set_role(Role::Image);
|
accessible.set_role(Role::Image);
|
||||||
if let Some(name) = name {
|
if let Some(name) = name {
|
||||||
|
|
205
crates/bevy_ui/src/ghost_hierarchy.rs
Normal file
205
crates/bevy_ui/src/ghost_hierarchy.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,17 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
BorderRadius, ContentSize, DefaultUiCamera, Display, Node, Outline, OverflowAxis,
|
BorderRadius, ContentSize, DefaultUiCamera, Display, Node, Outline, OverflowAxis,
|
||||||
ScrollPosition, Style, TargetCamera, UiScale,
|
ScrollPosition, Style, TargetCamera, UiChildren, UiRootNodes, UiScale,
|
||||||
};
|
};
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
change_detection::{DetectChanges, DetectChangesMut},
|
change_detection::{DetectChanges, DetectChangesMut},
|
||||||
entity::{Entity, EntityHashMap, EntityHashSet},
|
entity::{Entity, EntityHashMap, EntityHashSet},
|
||||||
event::EventReader,
|
event::EventReader,
|
||||||
query::{With, Without},
|
query::With,
|
||||||
removal_detection::RemovedComponents,
|
removal_detection::RemovedComponents,
|
||||||
system::{Commands, Local, Query, Res, ResMut, SystemParam},
|
system::{Commands, Local, Query, Res, ResMut, SystemParam},
|
||||||
world::Ref,
|
world::Ref,
|
||||||
};
|
};
|
||||||
use bevy_hierarchy::{Children, Parent};
|
use bevy_hierarchy::Children;
|
||||||
use bevy_math::{UVec2, Vec2};
|
use bevy_math::{UVec2, Vec2};
|
||||||
use bevy_render::camera::{Camera, NormalizedRenderTarget};
|
use bevy_render::camera::{Camera, NormalizedRenderTarget};
|
||||||
use bevy_sprite::BorderRect;
|
use bevy_sprite::BorderRect;
|
||||||
|
@ -104,7 +104,7 @@ pub fn ui_layout_system(
|
||||||
mut scale_factor_events: EventReader<WindowScaleFactorChanged>,
|
mut scale_factor_events: EventReader<WindowScaleFactorChanged>,
|
||||||
mut resize_events: EventReader<bevy_window::WindowResized>,
|
mut resize_events: EventReader<bevy_window::WindowResized>,
|
||||||
mut ui_surface: ResMut<UiSurface>,
|
mut ui_surface: ResMut<UiSurface>,
|
||||||
root_node_query: Query<(Entity, Option<&TargetCamera>), (With<Node>, Without<Parent>)>,
|
root_nodes: UiRootNodes,
|
||||||
mut style_query: Query<
|
mut style_query: Query<
|
||||||
(
|
(
|
||||||
Entity,
|
Entity,
|
||||||
|
@ -114,8 +114,8 @@ pub fn ui_layout_system(
|
||||||
),
|
),
|
||||||
With<Node>,
|
With<Node>,
|
||||||
>,
|
>,
|
||||||
children_query: Query<(Entity, Ref<Children>), With<Node>>,
|
node_query: Query<Entity, With<Node>>,
|
||||||
just_children_query: Query<&Children>,
|
ui_children: UiChildren,
|
||||||
mut removed_components: UiLayoutSystemRemovedComponentParam,
|
mut removed_components: UiLayoutSystemRemovedComponentParam,
|
||||||
mut node_transform_query: Query<(
|
mut node_transform_query: Query<(
|
||||||
&mut Node,
|
&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
|
// Precalculate the layout info for each camera, so we have fast access to it for each node
|
||||||
camera_layout_info.clear();
|
camera_layout_info.clear();
|
||||||
root_node_query.iter().for_each(|(entity,target_camera)|{
|
|
||||||
match camera_with_default(target_camera) {
|
style_query
|
||||||
Some(camera_entity) => {
|
.iter_many(root_nodes.iter())
|
||||||
let Ok((_, camera)) = cameras.get(camera_entity) else {
|
.for_each(|(entity, _, _, target_camera)| {
|
||||||
warn!(
|
match camera_with_default(target_camera) {
|
||||||
"TargetCamera (of root UI node {entity:?}) is pointing to a camera {:?} which doesn't exist",
|
Some(camera_entity) => {
|
||||||
camera_entity
|
let Ok((_, camera)) = cameras.get(camera_entity) else {
|
||||||
);
|
warn!(
|
||||||
return;
|
"TargetCamera (of root UI node {entity:?}) is pointing to a camera {:?} which doesn't exist",
|
||||||
};
|
camera_entity
|
||||||
let layout_info = camera_layout_info
|
);
|
||||||
.entry(camera_entity)
|
return;
|
||||||
.or_insert_with(|| calculate_camera_layout_info(camera));
|
};
|
||||||
layout_info.root_nodes.push(entity);
|
let layout_info = camera_layout_info
|
||||||
}
|
.entry(camera_entity)
|
||||||
None => {
|
.or_insert_with(|| calculate_camera_layout_info(camera));
|
||||||
if cameras.is_empty() {
|
layout_info.root_nodes.push(entity);
|
||||||
warn!("No camera found to render UI to. To fix this, add at least one camera to the scene.");
|
}
|
||||||
} else {
|
None => {
|
||||||
warn!(
|
if cameras.is_empty() {
|
||||||
"Multiple cameras found, causing UI target ambiguity. \
|
warn!("No camera found to render UI to. To fix this, add at least one camera to the scene.");
|
||||||
To fix this, add an explicit `TargetCamera` component to the root UI node {:?}",
|
} else {
|
||||||
entity
|
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.
|
// 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() {
|
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() {
|
for entity in removed_components.removed_children.read() {
|
||||||
ui_surface.try_remove_children(entity);
|
ui_surface.try_remove_children(entity);
|
||||||
}
|
}
|
||||||
children_query.iter().for_each(|(entity, children)| {
|
|
||||||
if children.is_changed() {
|
node_query.iter().for_each(|entity| {
|
||||||
ui_surface.update_children(entity, &children);
|
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());
|
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
|
// 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)| {
|
node_query.iter().for_each(|entity| {
|
||||||
if children.is_changed() {
|
if ui_children.is_changed(entity) {
|
||||||
ui_surface.update_children(entity, &children);
|
ui_surface.update_children(entity, ui_children.iter_ui_children(entity));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -281,7 +286,7 @@ pub fn ui_layout_system(
|
||||||
&ui_surface,
|
&ui_surface,
|
||||||
None,
|
None,
|
||||||
&mut node_transform_query,
|
&mut node_transform_query,
|
||||||
&just_children_query,
|
&ui_children,
|
||||||
inverse_target_scale_factor,
|
inverse_target_scale_factor,
|
||||||
Vec2::ZERO,
|
Vec2::ZERO,
|
||||||
Vec2::ZERO,
|
Vec2::ZERO,
|
||||||
|
@ -307,7 +312,7 @@ pub fn ui_layout_system(
|
||||||
Option<&Outline>,
|
Option<&Outline>,
|
||||||
Option<&ScrollPosition>,
|
Option<&ScrollPosition>,
|
||||||
)>,
|
)>,
|
||||||
children_query: &Query<&Children>,
|
ui_children: &UiChildren,
|
||||||
inverse_target_scale_factor: f32,
|
inverse_target_scale_factor: f32,
|
||||||
parent_size: Vec2,
|
parent_size: Vec2,
|
||||||
parent_scroll_position: Vec2,
|
parent_scroll_position: Vec2,
|
||||||
|
@ -415,21 +420,19 @@ pub fn ui_layout_system(
|
||||||
.insert(ScrollPosition::from(&clamped_scroll_position));
|
.insert(ScrollPosition::from(&clamped_scroll_position));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(children) = children_query.get(entity) {
|
for child_uinode in ui_children.iter_ui_children(entity) {
|
||||||
for &child_uinode in children {
|
update_uinode_geometry_recursive(
|
||||||
update_uinode_geometry_recursive(
|
commands,
|
||||||
commands,
|
child_uinode,
|
||||||
child_uinode,
|
ui_surface,
|
||||||
ui_surface,
|
Some(viewport_size),
|
||||||
Some(viewport_size),
|
node_transform_query,
|
||||||
node_transform_query,
|
ui_children,
|
||||||
children_query,
|
inverse_target_scale_factor,
|
||||||
inverse_target_scale_factor,
|
rounded_size,
|
||||||
rounded_size,
|
clamped_scroll_position,
|
||||||
clamped_scroll_position,
|
absolute_location,
|
||||||
absolute_location,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ use bevy_ecs::{
|
||||||
entity::{Entity, EntityHashMap},
|
entity::{Entity, EntityHashMap},
|
||||||
prelude::Resource,
|
prelude::Resource,
|
||||||
};
|
};
|
||||||
use bevy_hierarchy::Children;
|
|
||||||
use bevy_math::UVec2;
|
use bevy_math::UVec2;
|
||||||
use bevy_utils::{default, tracing::warn};
|
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_entity_to_taffy: EntityHashMap<EntityHashMap<taffy::NodeId>>,
|
||||||
pub(super) camera_roots: EntityHashMap<Vec<RootNodePair>>,
|
pub(super) camera_roots: EntityHashMap<Vec<RootNodePair>>,
|
||||||
pub(super) taffy: TaffyTree<NodeMeasure>,
|
pub(super) taffy: TaffyTree<NodeMeasure>,
|
||||||
|
taffy_children_scratch: Vec<taffy::NodeId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _assert_send_sync_ui_surface_impl_safe() {
|
fn _assert_send_sync_ui_surface_impl_safe() {
|
||||||
|
@ -55,6 +55,7 @@ impl Default for UiSurface {
|
||||||
camera_entity_to_taffy: Default::default(),
|
camera_entity_to_taffy: Default::default(),
|
||||||
camera_roots: Default::default(),
|
camera_roots: Default::default(),
|
||||||
taffy,
|
taffy,
|
||||||
|
taffy_children_scratch: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,22 +115,24 @@ impl UiSurface {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the children of the taffy node corresponding to the given [`Entity`].
|
/// Update the children of the taffy node corresponding to the given [`Entity`].
|
||||||
pub fn update_children(&mut self, entity: Entity, children: &Children) {
|
pub fn update_children(&mut self, entity: Entity, children: impl Iterator<Item = Entity>) {
|
||||||
let mut taffy_children = Vec::with_capacity(children.len());
|
self.taffy_children_scratch.clear();
|
||||||
|
|
||||||
for child in children {
|
for child in children {
|
||||||
if let Some(taffy_node) = self.entity_to_taffy.get(child) {
|
if let Some(taffy_node) = self.entity_to_taffy.get(&child) {
|
||||||
taffy_children.push(*taffy_node);
|
self.taffy_children_scratch.push(*taffy_node);
|
||||||
} else {
|
} else {
|
||||||
warn!(
|
warn!(
|
||||||
"Unstyled child `{child}` in a UI entity hierarchy. You are using an entity \
|
"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();
|
let taffy_node = self.entity_to_taffy.get(&entity).unwrap();
|
||||||
self.taffy
|
self.taffy
|
||||||
.set_children(*taffy_node, &taffy_children)
|
.set_children(*taffy_node, &self.taffy_children_scratch)
|
||||||
.unwrap();
|
.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(
|
pub fn set_camera_children(
|
||||||
&mut self,
|
&mut self,
|
||||||
camera_id: Entity,
|
camera_id: Entity,
|
||||||
|
|
|
@ -26,6 +26,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
||||||
mod accessibility;
|
mod accessibility;
|
||||||
mod focus;
|
mod focus;
|
||||||
mod geometry;
|
mod geometry;
|
||||||
|
mod ghost_hierarchy;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod render;
|
mod render;
|
||||||
mod stack;
|
mod stack;
|
||||||
|
@ -33,6 +34,7 @@ mod ui_node;
|
||||||
|
|
||||||
pub use focus::*;
|
pub use focus::*;
|
||||||
pub use geometry::*;
|
pub use geometry::*;
|
||||||
|
pub use ghost_hierarchy::*;
|
||||||
pub use layout::*;
|
pub use layout::*;
|
||||||
pub use measurement::*;
|
pub use measurement::*;
|
||||||
pub use render::*;
|
pub use render::*;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
//! This module contains the systems that update the stored UI nodes stack
|
//! This module contains the systems that update the stored UI nodes stack
|
||||||
|
|
||||||
use bevy_ecs::prelude::*;
|
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).
|
/// 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(
|
pub fn ui_stack_system(
|
||||||
mut cache: Local<ChildBufferCache>,
|
mut cache: Local<ChildBufferCache>,
|
||||||
mut root_nodes: Local<Vec<(Entity, (i32, i32))>>,
|
mut root_nodes: Local<Vec<(Entity, (i32, i32))>>,
|
||||||
|
mut visited_root_nodes: Local<HashSet<Entity>>,
|
||||||
mut ui_stack: ResMut<UiStack>,
|
mut ui_stack: ResMut<UiStack>,
|
||||||
root_node_query: Query<
|
ui_root_nodes: UiRootNodes,
|
||||||
(Entity, Option<&GlobalZIndex>, Option<&ZIndex>),
|
root_node_query: Query<(Entity, Option<&GlobalZIndex>, Option<&ZIndex>)>,
|
||||||
(With<Node>, Without<Parent>),
|
zindex_global_node_query: Query<(Entity, &GlobalZIndex, Option<&ZIndex>), With<Node>>,
|
||||||
>,
|
ui_children: UiChildren,
|
||||||
zindex_global_node_query: Query<
|
|
||||||
(Entity, &GlobalZIndex, Option<&ZIndex>),
|
|
||||||
(With<Node>, With<Parent>),
|
|
||||||
>,
|
|
||||||
children_query: Query<&Children>,
|
|
||||||
zindex_query: Query<Option<&ZIndex>, (With<Node>, Without<GlobalZIndex>)>,
|
zindex_query: Query<Option<&ZIndex>, (With<Node>, Without<GlobalZIndex>)>,
|
||||||
mut update_query: Query<&mut Node>,
|
mut update_query: Query<&mut Node>,
|
||||||
) {
|
) {
|
||||||
ui_stack.uinodes.clear();
|
ui_stack.uinodes.clear();
|
||||||
for (id, global_zindex, maybe_zindex) in zindex_global_node_query.iter() {
|
visited_root_nodes.clear();
|
||||||
root_nodes.push((
|
|
||||||
id,
|
|
||||||
(
|
|
||||||
global_zindex.0,
|
|
||||||
maybe_zindex.map(|zindex| zindex.0).unwrap_or(0),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
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((
|
root_nodes.push((
|
||||||
id,
|
id,
|
||||||
(
|
(
|
||||||
|
@ -71,6 +59,21 @@ pub fn ui_stack_system(
|
||||||
maybe_zindex.map(|zindex| zindex.0).unwrap_or(0),
|
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);
|
root_nodes.sort_by_key(|(_, z)| *z);
|
||||||
|
@ -79,7 +82,7 @@ pub fn ui_stack_system(
|
||||||
update_uistack_recursive(
|
update_uistack_recursive(
|
||||||
&mut cache,
|
&mut cache,
|
||||||
root_entity,
|
root_entity,
|
||||||
&children_query,
|
&ui_children,
|
||||||
&zindex_query,
|
&zindex_query,
|
||||||
&mut ui_stack.uinodes,
|
&mut ui_stack.uinodes,
|
||||||
);
|
);
|
||||||
|
@ -95,26 +98,28 @@ pub fn ui_stack_system(
|
||||||
fn update_uistack_recursive(
|
fn update_uistack_recursive(
|
||||||
cache: &mut ChildBufferCache,
|
cache: &mut ChildBufferCache,
|
||||||
node_entity: Entity,
|
node_entity: Entity,
|
||||||
children_query: &Query<&Children>,
|
ui_children: &UiChildren,
|
||||||
zindex_query: &Query<Option<&ZIndex>, (With<Node>, Without<GlobalZIndex>)>,
|
zindex_query: &Query<Option<&ZIndex>, (With<Node>, Without<GlobalZIndex>)>,
|
||||||
ui_stack: &mut Vec<Entity>,
|
ui_stack: &mut Vec<Entity>,
|
||||||
) {
|
) {
|
||||||
ui_stack.push(node_entity);
|
ui_stack.push(node_entity);
|
||||||
|
|
||||||
if let Ok(children) = children_query.get(node_entity) {
|
let mut child_buffer = cache.pop();
|
||||||
let mut child_buffer = cache.pop();
|
child_buffer.extend(
|
||||||
child_buffer.extend(children.iter().filter_map(|child_entity| {
|
ui_children
|
||||||
zindex_query
|
.iter_ui_children(node_entity)
|
||||||
.get(*child_entity)
|
.filter_map(|child_entity| {
|
||||||
.ok()
|
zindex_query
|
||||||
.map(|zindex| (*child_entity, zindex.map(|zindex| zindex.0).unwrap_or(0)))
|
.get(child_entity)
|
||||||
}));
|
.ok()
|
||||||
child_buffer.sort_by_key(|k| k.1);
|
.map(|zindex| (child_entity, zindex.map(|zindex| zindex.0).unwrap_or(0)))
|
||||||
for (child_entity, _) in child_buffer.drain(..) {
|
}),
|
||||||
update_uistack_recursive(cache, child_entity, children_query, zindex_query, ui_stack);
|
);
|
||||||
}
|
child_buffer.sort_by_key(|k| k.1);
|
||||||
cache.push(child_buffer);
|
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)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
//! This module contains systems that update the UI when something changes
|
//! 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 super::Node;
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
entity::Entity,
|
entity::Entity,
|
||||||
query::{Changed, With, Without},
|
query::{Changed, With},
|
||||||
system::{Commands, Query},
|
system::{Commands, Query},
|
||||||
};
|
};
|
||||||
use bevy_hierarchy::{Children, Parent};
|
|
||||||
use bevy_math::Rect;
|
use bevy_math::Rect;
|
||||||
use bevy_transform::components::GlobalTransform;
|
use bevy_transform::components::GlobalTransform;
|
||||||
use bevy_utils::HashSet;
|
use bevy_utils::HashSet;
|
||||||
|
@ -16,14 +15,14 @@ use bevy_utils::HashSet;
|
||||||
/// Updates clipping for all nodes
|
/// Updates clipping for all nodes
|
||||||
pub fn update_clipping_system(
|
pub fn update_clipping_system(
|
||||||
mut commands: Commands,
|
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>)>,
|
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(
|
update_clipping(
|
||||||
&mut commands,
|
&mut commands,
|
||||||
&children_query,
|
&ui_children,
|
||||||
&mut node_query,
|
&mut node_query,
|
||||||
root_node,
|
root_node,
|
||||||
None,
|
None,
|
||||||
|
@ -33,7 +32,7 @@ pub fn update_clipping_system(
|
||||||
|
|
||||||
fn update_clipping(
|
fn update_clipping(
|
||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
children_query: &Query<&Children>,
|
ui_children: &UiChildren,
|
||||||
node_query: &mut Query<(&Node, &GlobalTransform, &Style, Option<&mut CalculatedClip>)>,
|
node_query: &mut Query<(&Node, &GlobalTransform, &Style, Option<&mut CalculatedClip>)>,
|
||||||
entity: Entity,
|
entity: Entity,
|
||||||
mut maybe_inherited_clip: Option<Rect>,
|
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)))
|
Some(maybe_inherited_clip.map_or(node_rect, |c| c.intersect(node_rect)))
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(children) = children_query.get(entity) {
|
for child in ui_children.iter_ui_children(entity) {
|
||||||
for &child in children {
|
update_clipping(commands, ui_children, node_query, child, children_clip);
|
||||||
update_clipping(commands, children_query, node_query, child, children_clip);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,11 +101,11 @@ pub fn update_target_camera_system(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
changed_root_nodes_query: Query<
|
changed_root_nodes_query: Query<
|
||||||
(Entity, Option<&TargetCamera>),
|
(Entity, Option<&TargetCamera>),
|
||||||
(With<Node>, Without<Parent>, Changed<TargetCamera>),
|
(With<Node>, Changed<TargetCamera>),
|
||||||
>,
|
>,
|
||||||
changed_children_query: Query<(Entity, Option<&TargetCamera>), (With<Node>, Changed<Children>)>,
|
node_query: Query<(Entity, Option<&TargetCamera>), With<Node>>,
|
||||||
children_query: Query<&Children, With<Node>>,
|
ui_root_nodes: UiRootNodes,
|
||||||
node_query: Query<Option<&TargetCamera>, With<Node>>,
|
ui_children: UiChildren,
|
||||||
) {
|
) {
|
||||||
// Track updated entities to prevent redundant updates, as `Commands` changes are deferred,
|
// 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
|
// 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,
|
// Assuming that TargetCamera is manually set on the root node only,
|
||||||
// update root nodes first, since it implies the biggest change
|
// 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(
|
update_children_target_camera(
|
||||||
root_node,
|
root_node,
|
||||||
target_camera,
|
target_camera,
|
||||||
&node_query,
|
&node_query,
|
||||||
&children_query,
|
&ui_children,
|
||||||
&mut commands,
|
&mut commands,
|
||||||
&mut updated_entities,
|
&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
|
// If the root node TargetCamera was changed, then every child is updated
|
||||||
// by this point, and iteration will be skipped.
|
// by this point, and iteration will be skipped.
|
||||||
// Otherwise, update changed children
|
// 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(
|
update_children_target_camera(
|
||||||
parent,
|
parent,
|
||||||
target_camera,
|
target_camera,
|
||||||
&node_query,
|
&node_query,
|
||||||
&children_query,
|
&ui_children,
|
||||||
&mut commands,
|
&mut commands,
|
||||||
&mut updated_entities,
|
&mut updated_entities,
|
||||||
);
|
);
|
||||||
|
@ -145,19 +146,15 @@ pub fn update_target_camera_system(
|
||||||
fn update_children_target_camera(
|
fn update_children_target_camera(
|
||||||
entity: Entity,
|
entity: Entity,
|
||||||
camera_to_set: Option<&TargetCamera>,
|
camera_to_set: Option<&TargetCamera>,
|
||||||
node_query: &Query<Option<&TargetCamera>, With<Node>>,
|
node_query: &Query<(Entity, Option<&TargetCamera>), With<Node>>,
|
||||||
children_query: &Query<&Children, With<Node>>,
|
ui_children: &UiChildren,
|
||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
updated_entities: &mut HashSet<Entity>,
|
updated_entities: &mut HashSet<Entity>,
|
||||||
) {
|
) {
|
||||||
let Ok(children) = children_query.get(entity) else {
|
for child in ui_children.iter_ui_children(entity) {
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
for &child in children {
|
|
||||||
// Skip if the child has already been updated or update is not needed
|
// Skip if the child has already been updated or update is not needed
|
||||||
if updated_entities.contains(&child)
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -176,7 +173,7 @@ fn update_children_target_camera(
|
||||||
child,
|
child,
|
||||||
camera_to_set,
|
camera_to_set,
|
||||||
node_query,
|
node_query,
|
||||||
children_query,
|
ui_children,
|
||||||
commands,
|
commands,
|
||||||
updated_entities,
|
updated_entities,
|
||||||
);
|
);
|
||||||
|
|
|
@ -496,6 +496,7 @@ Example | Description
|
||||||
[Display and Visibility](../examples/ui/display_and_visibility.rs) | Demonstrates how Display and Visibility work in the UI.
|
[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
|
[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)
|
[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](../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
|
[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
|
[Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component
|
||||||
|
|
122
examples/ui/ghost_nodes.rs
Normal file
122
examples/ui/ghost_nodes.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue