Skip to content
Merged
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,9 @@ reflect_functions = ["bevy_internal/reflect_functions"]
# Enable winit custom cursor support
custom_cursor = ["bevy_internal/custom_cursor"]

# Experimental support for nodes that are ignored for UI layouting
ghost_nodes = ["bevy_internal/ghost_nodes"]

[dependencies]
bevy_internal = { path = "crates/bevy_internal", version = "0.15.0-dev", default-features = false }

Expand Down Expand Up @@ -3081,6 +3084,7 @@ wasm = true
name = "ghost_nodes"
path = "examples/ui/ghost_nodes.rs"
doc-scrape-examples = true
required-features = ["ghost_nodes"]

[package.metadata.example.ghost_nodes]
name = "Ghost Nodes"
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,9 @@ reflect_functions = [
# Enable winit custom cursor support
custom_cursor = ["bevy_winit/custom_cursor"]

# Experimental support for nodes that are ignored for UI layouting
ghost_nodes = ["bevy_ui/ghost_nodes"]

[dependencies]
# bevy
bevy_a11y = { path = "../bevy_a11y", version = "0.15.0-dev" }
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ default = ["bevy_ui_picking_backend"]
serialize = ["serde", "smallvec/serde", "bevy_math/serialize"]
bevy_ui_picking_backend = ["bevy_picking"]

# Experimental features
ghost_nodes = []

[lints]
workspace = true

Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_ui/src/accessibility.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::{
experimental::UiChildren,
prelude::{Button, Label},
widget::TextUiReader,
Node, UiChildren, UiImage,
Node, UiImage,
};
use bevy_a11y::{
accesskit::{NodeBuilder, Rect, Role},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use bevy_hierarchy::{Children, HierarchyQueryExt, Parent};
use bevy_reflect::prelude::*;
use bevy_render::view::Visibility;
use bevy_transform::prelude::Transform;
use core::marker::PhantomData;
use smallvec::SmallVec;

use crate::Node;
Expand All @@ -14,10 +15,30 @@ use crate::Node;
/// 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)]
///
/// Instances of this type cannot be constructed unless the `ghost_nodes` feature is enabled.
#[derive(Component, Debug, Copy, Clone, Reflect)]
#[cfg_attr(feature = "ghost_nodes", derive(Default))]
#[reflect(Component, Debug)]
#[require(Visibility, Transform)]
pub struct GhostNode;
pub struct GhostNode {
// This is a workaround to ensure that GhostNode is only constructable when the appropriate feature flag is enabled
#[reflect(ignore)]
unconstructable: PhantomData<()>, // Spooky!
}

#[cfg(feature = "ghost_nodes")]
impl GhostNode {
/// Creates a new ghost node.
///
/// This method is only available when the `ghost_node` feature is enabled,
/// and will eventually be deprecated then removed in favor of simply using `GhostNode` as no meaningful data is stored.
pub const fn new() -> Self {
GhostNode {
unconstructable: PhantomData,
}
}
}

/// System param that allows iteration of all UI root nodes.
///
Expand Down Expand Up @@ -140,7 +161,7 @@ impl<'w, 's> Iterator for UiChildrenIter<'w, 's> {
}
}

#[cfg(test)]
#[cfg(all(test, feature = "ghost_nodes"))]
mod tests {
use bevy_ecs::{
prelude::Component,
Expand All @@ -165,18 +186,20 @@ mod tests {
.with_children(|parent| {
parent.spawn((A(2), NodeBundle::default()));
parent
.spawn((A(3), GhostNode))
.spawn((A(3), GhostNode::new()))
.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));
});
world
.spawn((A(5), GhostNode::new()))
.with_children(|parent| {
parent.spawn((A(6), NodeBundle::default()));
parent
.spawn((A(7), GhostNode::new()))
.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);
Expand All @@ -191,15 +214,15 @@ mod tests {
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 n2 = world.spawn((A(2), GhostNode::new())).id();
let n3 = world.spawn((A(3), GhostNode::new())).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 n6 = world.spawn((A(6), GhostNode::new())).id();
let n7 = world.spawn((A(7), GhostNode::new())).id();
let n8 = world.spawn((A(8), NodeBundle::default())).id();
let n9 = world.spawn((A(9), GhostNode)).id();
let n9 = world.spawn((A(9), GhostNode::new())).id();
let n10 = world.spawn((A(10), NodeBundle::default())).id();

let no_ui = world.spawn_empty().id();
Expand Down
17 changes: 17 additions & 0 deletions crates/bevy_ui/src/experimental/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//! Experimental features are not yet stable and may change or be removed in the future.
//!
//! These features are not recommended for production use, but are available to ease experimentation
//! within Bevy's ecosystem. Please let us know how you are using these features and what you would
//! like to see improved!
//!
//! These may be feature-flagged: check the `Cargo.toml` for `bevy_ui` to see what options
//! are available.
//!
//! # Warning
//!
//! Be careful when using these features, especially in concert with third-party crates,
//! as they may not be fully supported, functional or stable.

mod ghost_hierarchy;

pub use ghost_hierarchy::*;
3 changes: 2 additions & 1 deletion crates/bevy_ui/src/layout/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
experimental::{UiChildren, UiRootNodes},
BorderRadius, ContentSize, DefaultUiCamera, Display, Node, Outline, OverflowAxis,
ScrollPosition, Style, TargetCamera, UiChildren, UiRootNodes, UiScale,
ScrollPosition, Style, TargetCamera, UiScale,
};
use bevy_ecs::{
change_detection::{DetectChanges, DetectChangesMut},
Expand Down
5 changes: 3 additions & 2 deletions crates/bevy_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,18 @@ pub mod picking_backend;
use bevy_derive::{Deref, DerefMut};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
mod accessibility;
// This module is not re-exported, but is instead made public.
// This is intended to discourage accidental use of the experimental API.
pub mod experimental;
mod focus;
mod geometry;
mod ghost_hierarchy;
mod layout;
mod render;
mod stack;
mod ui_node;

pub use focus::*;
pub use geometry::*;
pub use ghost_hierarchy::*;
pub use layout::*;
pub use measurement::*;
pub use render::*;
Expand Down
5 changes: 4 additions & 1 deletion crates/bevy_ui/src/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
use bevy_ecs::prelude::*;
use bevy_utils::HashSet;

use crate::{GlobalZIndex, Node, UiChildren, UiRootNodes, ZIndex};
use crate::{
experimental::{UiChildren, UiRootNodes},
GlobalZIndex, Node, ZIndex,
};

/// The current UI stack, which contains all UI nodes ordered by their depth (back-to-front).
///
Expand Down
5 changes: 4 additions & 1 deletion crates/bevy_ui/src/update.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
//! This module contains systems that update the UI when something changes

use crate::{CalculatedClip, Display, OverflowAxis, Style, TargetCamera, UiChildren, UiRootNodes};
use crate::{
experimental::{UiChildren, UiRootNodes},
CalculatedClip, Display, OverflowAxis, Style, TargetCamera,
};

use super::Node;
use bevy_ecs::{
Expand Down
1 change: 1 addition & 0 deletions docs/cargo_features.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ The default feature set enables most of the expected features of a game engine,
|ff|Farbfeld image format support|
|file_watcher|Enables watching the filesystem for Bevy Asset hot-reloading|
|flac|FLAC audio format support|
|ghost_nodes|Experimental support for nodes that are ignored for UI layouting|
|gif|GIF image format support|
|glam_assert|Enable assertions to check the validity of parameters passed to glam|
|ico|ICO image format support|
Expand Down
30 changes: 20 additions & 10 deletions examples/ui/ghost_nodes.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
//! 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.
//!
//! # Warning
//!
//! This is an experimental feature, and should be used with caution,
//! especially in concert with 3rd party plugins or systems that may not be aware of ghost nodes.
//!
//! To add [`GhostNode`] components to entities, you must enable the `ghost_nodes` feature flag,
//! as they are otherwise unconstructable even though the type is defined.

use bevy::{prelude::*, ui::GhostNode, winit::WinitSettings};
use bevy::{prelude::*, ui::experimental::GhostNode, winit::WinitSettings};

fn main() {
App::new()
Expand All @@ -22,14 +30,16 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2d);

// 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(),
));
});
commands
.spawn(GhostNode::new())
.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
Expand All @@ -48,7 +58,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
.spawn((NodeBundle::default(), Counter(0)))
.with_children(|layout_parent| {
layout_parent
.spawn((GhostNode, Counter(0)))
.spawn((GhostNode::new(), 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
Expand Down