ghost_nodes/
ghost_nodes.rs

1//! This example demonstrates the use of Ghost Nodes.
2//!
3//! UI layout will ignore ghost nodes, and treat their children as if they were direct descendants of the first non-ghost ancestor.
4//!
5//! # Warning
6//!
7//! This is an experimental feature, and should be used with caution,
8//! especially in concert with 3rd party plugins or systems that may not be aware of ghost nodes.
9//!
10//! In order to use [`GhostNode`]s you must enable the `ghost_nodes` feature flag.
11
12use bevy::{prelude::*, ui::experimental::GhostNode};
13
14fn main() {
15    App::new()
16        .add_plugins(DefaultPlugins)
17        .add_systems(Startup, setup)
18        .add_systems(Update, button_system)
19        .run();
20}
21
22#[derive(Component)]
23struct Counter(i32);
24
25fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
26    let font_handle = asset_server.load("fonts/FiraSans-Bold.ttf");
27
28    commands.spawn(Camera2d);
29
30    // Ghost UI root
31    commands.spawn(GhostNode).with_children(|ghost_root| {
32        ghost_root.spawn(Node::default()).with_child(create_label(
33            "This text node is rendered under a ghost root",
34            font_handle.clone(),
35        ));
36    });
37
38    // Normal UI root
39    commands
40        .spawn(Node {
41            width: percent(100),
42            height: percent(100),
43            align_items: AlignItems::Center,
44            justify_content: JustifyContent::Center,
45            ..default()
46        })
47        .with_children(|parent| {
48            parent
49                .spawn((Node::default(), Counter(0)))
50                .with_children(|layout_parent| {
51                    layout_parent
52                        .spawn((GhostNode, Counter(0)))
53                        .with_children(|ghost_parent| {
54                            // Ghost children using a separate counter state
55                            // These buttons are being treated as children of layout_parent in the context of UI
56                            ghost_parent
57                                .spawn(create_button())
58                                .with_child(create_label("0", font_handle.clone()));
59                            ghost_parent
60                                .spawn(create_button())
61                                .with_child(create_label("0", font_handle.clone()));
62                        });
63
64                    // A normal child using the layout parent counter
65                    layout_parent
66                        .spawn(create_button())
67                        .with_child(create_label("0", font_handle.clone()));
68                });
69        });
70}
71
72fn create_button() -> impl Bundle {
73    (
74        Button,
75        Node {
76            width: px(150),
77            height: px(65),
78            border: UiRect::all(px(5)),
79            // horizontally center child text
80            justify_content: JustifyContent::Center,
81            // vertically center child text
82            align_items: AlignItems::Center,
83            ..default()
84        },
85        BorderColor::all(Color::BLACK),
86        BorderRadius::MAX,
87        BackgroundColor(Color::srgb(0.15, 0.15, 0.15)),
88    )
89}
90
91fn create_label(text: &str, font: Handle<Font>) -> (Text, TextFont, TextColor) {
92    (
93        Text::new(text),
94        TextFont {
95            font,
96            font_size: 33.0,
97            ..default()
98        },
99        TextColor(Color::srgb(0.9, 0.9, 0.9)),
100    )
101}
102
103fn button_system(
104    mut interaction_query: Query<(&Interaction, &ChildOf), (Changed<Interaction>, With<Button>)>,
105    labels_query: Query<(&Children, &ChildOf), With<Button>>,
106    mut text_query: Query<&mut Text>,
107    mut counter_query: Query<&mut Counter>,
108) {
109    // Update parent counter on click
110    for (interaction, child_of) in &mut interaction_query {
111        if matches!(interaction, Interaction::Pressed) {
112            let mut counter = counter_query.get_mut(child_of.parent()).unwrap();
113            counter.0 += 1;
114        }
115    }
116
117    // Update button labels to match their parent counter
118    for (children, child_of) in &labels_query {
119        let counter = counter_query.get(child_of.parent()).unwrap();
120        let mut text = text_query.get_mut(children[0]).unwrap();
121
122        **text = counter.0.to_string();
123    }
124}