From 76744bf58c9cbbd4eea21d9a16bb0721752f61a2 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 16 Oct 2024 18:20:48 -0400 Subject: [PATCH] Mark ghost nodes as experimental and partially feature flag them (#15961) # Objective As discussed in #15341, ghost nodes are a contentious and experimental feature. In the interest of enabling ecosystem experimentation, we've decided to keep them in Bevy 0.15. That said, we don't use them internally, and don't expect third-party crates to support them. If the experimentation returns a negative result (they aren't very useful, an alternative design is preferred etc) they will be removed. We should clearly communicate this status to users, and make sure that users don't use ghost nodes in their projects without a very clear understanding of what they're getting themselves into. ## Solution To make life easy for users (and Bevy), `GhostNode` and all associated helpers remain public and are always available. However, actually constructing these requires enabling a feature flag that's clearly marked as experimental. To do so, I've added a meaningless private field. When the feature flag is enabled, our constructs (`new` and `default`) can be used. I've added a `new` constructor, which should be preferred over `Default::default` as that can be readily deprecated, allowing us to prompt users to swap over to the much nicer `GhostNode` syntax once this is a unit struct again. Full credit: this was mostly @cart's design: I'm just implementing it! ## Testing I've run the ghost_nodes example and it fails to compile without the feature flag. With the feature flag, it works fine :) --------- Co-authored-by: Zachary Harrold --- Cargo.toml | 4 ++ crates/bevy_internal/Cargo.toml | 3 + crates/bevy_ui/Cargo.toml | 3 + crates/bevy_ui/src/accessibility.rs | 3 +- .../src/{ => experimental}/ghost_hierarchy.rs | 55 +++++++++++++------ crates/bevy_ui/src/experimental/mod.rs | 17 ++++++ crates/bevy_ui/src/layout/mod.rs | 3 +- crates/bevy_ui/src/lib.rs | 5 +- crates/bevy_ui/src/stack.rs | 5 +- crates/bevy_ui/src/update.rs | 5 +- docs/cargo_features.md | 1 + examples/ui/ghost_nodes.rs | 30 ++++++---- 12 files changed, 102 insertions(+), 32 deletions(-) rename crates/bevy_ui/src/{ => experimental}/ghost_hierarchy.rs (81%) create mode 100644 crates/bevy_ui/src/experimental/mod.rs 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