diff --git a/Cargo.toml b/Cargo.toml index 42f7ccf41a..04990d8aed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } @@ -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" diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 2f077379ed..62f9bc2a10 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -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" } diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index 87304ac959..cc6098b02f 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -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 diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index 2a5944f40a..0e6b1780aa 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -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}, diff --git a/crates/bevy_ui/src/ghost_hierarchy.rs b/crates/bevy_ui/src/experimental/ghost_hierarchy.rs similarity index 81% rename from crates/bevy_ui/src/ghost_hierarchy.rs rename to crates/bevy_ui/src/experimental/ghost_hierarchy.rs index ad5a889480..067952e9f1 100644 --- a/crates/bevy_ui/src/ghost_hierarchy.rs +++ b/crates/bevy_ui/src/experimental/ghost_hierarchy.rs @@ -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; @@ -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. /// @@ -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, @@ -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); @@ -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(); diff --git a/crates/bevy_ui/src/experimental/mod.rs b/crates/bevy_ui/src/experimental/mod.rs new file mode 100644 index 0000000000..a8f2740c30 --- /dev/null +++ b/crates/bevy_ui/src/experimental/mod.rs @@ -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::*; diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index b81e3fe09c..7f191fcf1b 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -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}, diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 9ac4087e75..1da41b7ab5 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -23,9 +23,11 @@ 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; @@ -33,7 +35,6 @@ mod ui_node; pub use focus::*; pub use geometry::*; -pub use ghost_hierarchy::*; pub use layout::*; pub use measurement::*; pub use render::*; diff --git a/crates/bevy_ui/src/stack.rs b/crates/bevy_ui/src/stack.rs index a8d535fa2f..a89add3d6f 100644 --- a/crates/bevy_ui/src/stack.rs +++ b/crates/bevy_ui/src/stack.rs @@ -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). /// diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index 22647e2502..e2d878d39e 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -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::{ diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 527ef205f0..5fb465fd33 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -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| diff --git a/examples/ui/ghost_nodes.rs b/examples/ui/ghost_nodes.rs index 99ff0956bc..0460c4c30a 100644 --- a/examples/ui/ghost_nodes.rs +++ b/examples/ui/ghost_nodes.rs @@ -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() @@ -22,14 +30,16 @@ fn setup(mut commands: Commands, asset_server: Res) { 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 @@ -48,7 +58,7 @@ fn setup(mut commands: Commands, asset_server: Res) { .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