Skip to content

Anonymous ghost nodes #17908

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

Draft
wants to merge 31 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
76c0480
* Added a `transform: Transform` field to `Node`.
ickshonpe Dec 2, 2024
6847c51
Fixed overflow_debug example
ickshonpe Dec 2, 2024
ff39bd4
Use logical units for `Node`'s translation
ickshonpe Dec 2, 2024
96465d1
Merge branch 'main' into node-global-transform
ickshonpe Dec 2, 2024
f6a3dc8
fix for ambiguity detection
ickshonpe Dec 2, 2024
447cea6
Merge branch 'node-global-transform' of https://github.com/ickshonpe/…
ickshonpe Dec 2, 2024
7040ddf
Reimplemt `GhostNode`s using a `Ghost` trait.
ickshonpe Feb 17, 2025
a6889bb
Renamings:
ickshonpe Feb 17, 2025
04bed16
Clean up comments
ickshonpe Feb 17, 2025
15c90e3
Fix comment
ickshonpe Feb 17, 2025
a0fcb8b
Fixed test import
ickshonpe Feb 17, 2025
1611507
Merge branch 'main' into ghostnode-trait
ickshonpe Feb 17, 2025
f293b1d
Fixed `iterate_ui_children` test
ickshonpe Feb 17, 2025
49bc083
Merge branch 'ghostnode-trait' of https://github.com/ickshonpe/bevy i…
ickshonpe Feb 17, 2025
029170e
Fixed test imports
ickshonpe Feb 17, 2025
2cb61df
Renamed `is_actual_node` to `is_actual`
ickshonpe Feb 17, 2025
d7eef43
Moved ghost node tests to the navigation module
ickshonpe Feb 17, 2025
3dc5c2a
Merge branch 'main' into node-global-transform
ickshonpe Feb 17, 2025
67e8315
Fixes for merge
ickshonpe Feb 17, 2025
f38858b
Added `is_visable` flage to `ComputedNode`.
ickshonpe Feb 17, 2025
2de6026
Removed `Visibility` require from `Node`
ickshonpe Feb 17, 2025
9690e70
Added visibility field to `Node`
ickshonpe Feb 17, 2025
ad07e93
propagate visibility in layout updates
ickshonpe Feb 17, 2025
fd8a70f
Fixed extraction
ickshonpe Feb 17, 2025
327f183
Updated picking
ickshonpe Feb 17, 2025
6900469
Fixed `display_and_visibility` example
ickshonpe Feb 17, 2025
04bc1a0
Merge branch 'ghostnode-trait' into remove-ghost-nodes-merge-trait
ickshonpe Feb 17, 2025
f7d0621
Removed `UiNode` and `ghost_nodes` feature gate.
ickshonpe Feb 17, 2025
5a798a3
Fixed test imports
ickshonpe Feb 17, 2025
0f8c8d6
Removed unused? imports
ickshonpe Feb 17, 2025
71a0764
Fixed debug overlay
ickshonpe Feb 17, 2025
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
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
237 changes: 34 additions & 203 deletions crates/bevy_ui/src/experimental/ghost_hierarchy.rs
Original file line number Diff line number Diff line change
@@ -1,110 +1,52 @@
//! 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.
///
/// A UI root node is either a [`Node`] without a [`ChildOf`], or with only [`GhostNode`] ancestors.
/// System param that gives access to UI children utilities, skipping over non-actual 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>,
}

#[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> {
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))
}))
}
}

#[cfg(feature = "ghost_nodes")]
/// 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>, Has<GhostNode>),
Or<(With<Node>, With<GhostNode>)>,
>,
pub struct FlattenChildren<'w, 's, N>
where
N: Component,
{
actual_children_query: Query<'w, 's, (Option<&'static Children>, Has<N>)>,
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, Without<N>>,
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, N> FlattenChildren<'w, 's, N>
where
N: 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) -> FlattenChildrenIter<'w, 's, N> {
FlattenChildrenIter {
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 +70,30 @@ 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)
}
}

#[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)
/// 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(feature = "ghost_nodes")]
pub struct UiChildrenIter<'w, 's> {
pub struct FlattenChildrenIter<'w, 's, N>
where
N: 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<N>)>,
}

#[cfg(feature = "ghost_nodes")]
impl<'w, 's> Iterator for UiChildrenIter<'w, 's> {
impl<'w, 's, N> Iterator for FlattenChildrenIter<'w, 's, N>
where
N: 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 +103,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());
}
}
6 changes: 2 additions & 4 deletions crates/bevy_ui/src/focus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use bevy_input::{mouse::MouseButton, touch::Touches, ButtonInput};
use bevy_math::{Rect, Vec2};
use bevy_platform_support::collections::HashMap;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::InheritedVisibility};
use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera};
use bevy_transform::components::GlobalTransform;
use bevy_window::{PrimaryWindow, Window};

Expand Down Expand Up @@ -138,7 +138,6 @@ pub struct NodeQuery {
relative_cursor_position: Option<&'static mut RelativeCursorPosition>,
focus_policy: Option<&'static FocusPolicy>,
calculated_clip: Option<&'static CalculatedClip>,
inherited_visibility: Option<&'static InheritedVisibility>,
target_camera: &'static ComputedNodeTarget,
}

Expand Down Expand Up @@ -222,9 +221,8 @@ pub fn ui_focus_system(
return None;
};

let inherited_visibility = node.inherited_visibility?;
// Nodes that are not rendered should not be interactable
if !inherited_visibility.get() {
if !node.node.is_visible {
// Reset their interaction to None to avoid strange stuck state
if let Some(mut interaction) = node.interaction {
// We cannot simply set the interaction to None, as that will trigger change detection repeatedly
Expand Down
5 changes: 5 additions & 0 deletions crates/bevy_ui/src/layout/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,9 @@ impl RepeatedGridTrack {

#[cfg(test)]
mod tests {
use bevy_render::view::Visibility;
use bevy_transform::components::Transform;

use super::*;

#[test]
Expand Down Expand Up @@ -522,6 +525,8 @@ mod tests {
],
grid_column: GridPlacement::start(4),
grid_row: GridPlacement::span(3),
transform: Transform::IDENTITY,
visibility: Visibility::Inherited,
};
let viewport_values = LayoutContext::new(1.0, bevy_math::Vec2::new(800., 600.));
let taffy_style = from_node(&node, &viewport_values, false);
Expand Down
Loading
Loading