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 <zac@harrold.com.au>
This commit is contained in:
Alice Cecile 2024-10-16 18:20:48 -04:00 committed by GitHub
parent f4d9c52c0d
commit 76744bf58c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 102 additions and 32 deletions

View file

@ -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"

View file

@ -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" }

View file

@ -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

View file

@ -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},

View file

@ -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();

View file

@ -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::*;

View file

@ -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},

View file

@ -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::*;

View file

@ -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).
///

View file

@ -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::{

View file

@ -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|

View file

@ -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<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
@ -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