
# 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>
134 lines
4.5 KiB
Rust
134 lines
4.5 KiB
Rust
//! 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::experimental::GhostNode, winit::WinitSettings};
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
.insert_resource(WinitSettings::desktop_app())
|
|
.add_systems(Startup, setup)
|
|
.add_systems(Update, button_system)
|
|
.run();
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct Counter(i32);
|
|
|
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|
let font_handle = asset_server.load("fonts/FiraSans-Bold.ttf");
|
|
|
|
commands.spawn(Camera2d);
|
|
|
|
// Ghost UI root
|
|
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
|
|
.spawn(NodeBundle {
|
|
style: Style {
|
|
width: Val::Percent(100.0),
|
|
height: Val::Percent(100.0),
|
|
align_items: AlignItems::Center,
|
|
justify_content: JustifyContent::Center,
|
|
..default()
|
|
},
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
parent
|
|
.spawn((NodeBundle::default(), Counter(0)))
|
|
.with_children(|layout_parent| {
|
|
layout_parent
|
|
.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
|
|
ghost_parent
|
|
.spawn(create_button())
|
|
.with_child(create_label("0", font_handle.clone()));
|
|
ghost_parent
|
|
.spawn(create_button())
|
|
.with_child(create_label("0", font_handle.clone()));
|
|
});
|
|
|
|
// A normal child using the layout parent counter
|
|
layout_parent
|
|
.spawn(create_button())
|
|
.with_child(create_label("0", font_handle.clone()));
|
|
});
|
|
});
|
|
}
|
|
|
|
fn create_button() -> ButtonBundle {
|
|
ButtonBundle {
|
|
style: Style {
|
|
width: Val::Px(150.0),
|
|
height: Val::Px(65.0),
|
|
border: UiRect::all(Val::Px(5.0)),
|
|
// horizontally center child text
|
|
justify_content: JustifyContent::Center,
|
|
// vertically center child text
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
},
|
|
border_color: BorderColor(Color::BLACK),
|
|
border_radius: BorderRadius::MAX,
|
|
background_color: Color::srgb(0.15, 0.15, 0.15).into(),
|
|
..default()
|
|
}
|
|
}
|
|
|
|
fn create_label(text: &str, font: Handle<Font>) -> (Text, TextFont, TextColor) {
|
|
(
|
|
Text::new(text),
|
|
TextFont {
|
|
font,
|
|
font_size: 33.0,
|
|
..default()
|
|
},
|
|
TextColor(Color::srgb(0.9, 0.9, 0.9)),
|
|
)
|
|
}
|
|
|
|
fn button_system(
|
|
mut interaction_query: Query<(&Interaction, &Parent), (Changed<Interaction>, With<Button>)>,
|
|
labels_query: Query<(&Children, &Parent), With<Button>>,
|
|
mut text_query: Query<&mut Text>,
|
|
mut counter_query: Query<&mut Counter>,
|
|
) {
|
|
// Update parent counter on click
|
|
for (interaction, parent) in &mut interaction_query {
|
|
if matches!(interaction, Interaction::Pressed) {
|
|
let mut counter = counter_query.get_mut(parent.get()).unwrap();
|
|
counter.0 += 1;
|
|
}
|
|
}
|
|
|
|
// Update button labels to match their parent counter
|
|
for (children, parent) in &labels_query {
|
|
let counter = counter_query.get(parent.get()).unwrap();
|
|
let mut text = text_query.get_mut(children[0]).unwrap();
|
|
|
|
**text = counter.0.to_string();
|
|
}
|
|
}
|