mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 12:13:25 +00:00
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:
parent
f4d9c52c0d
commit
76744bf58c
12 changed files with 102 additions and 32 deletions
|
@ -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"
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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();
|
17
crates/bevy_ui/src/experimental/mod.rs
Normal file
17
crates/bevy_ui/src/experimental/mod.rs
Normal 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::*;
|
|
@ -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},
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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).
|
||||
///
|
||||
|
|
|
@ -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::{
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue