From 180b2c3f4ca20fd9ee4a14ada43ef651c0ea7d7a Mon Sep 17 00:00:00 2001 From: Viktor Gustavsson Date: Thu, 19 Sep 2024 23:53:41 +0200 Subject: [PATCH 01/25] Add the GhostNode component --- crates/bevy_ui/src/ghost_hierarchy.rs | 22 ++++++++++++++++++++++ crates/bevy_ui/src/lib.rs | 2 ++ 2 files changed, 24 insertions(+) create mode 100644 crates/bevy_ui/src/ghost_hierarchy.rs diff --git a/crates/bevy_ui/src/ghost_hierarchy.rs b/crates/bevy_ui/src/ghost_hierarchy.rs new file mode 100644 index 0000000000000..d1b009ca3af82 --- /dev/null +++ b/crates/bevy_ui/src/ghost_hierarchy.rs @@ -0,0 +1,22 @@ +//! This module contains [`GhostNode`] and utilities to flatten the UI hierarchy, traversing past ghost nodes. + +use bevy_ecs::prelude::*; +use bevy_reflect::prelude::*; +use bevy_render::view::{InheritedVisibility, ViewVisibility, Visibility}; +use bevy_transform::prelude::{GlobalTransform, Transform}; + +/// Marker component for entities that should be ignored by within UI hierarchies. +/// +/// The UI systems will traverse past these and consider 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)] +#[reflect(Component, Debug)] +#[require( + Visibility, + InheritedVisibility, + ViewVisibility, + Transform, + GlobalTransform +)] +pub struct GhostNode; diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index e1f46c405ba46..3add1a0c75142 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -27,6 +27,7 @@ use bevy_reflect::Reflect; mod accessibility; mod focus; mod geometry; +mod ghost_hierarchy; mod layout; mod render; mod stack; @@ -34,6 +35,7 @@ mod ui_node; pub use focus::*; pub use geometry::*; +pub use ghost_hierarchy::*; pub use layout::*; pub use measurement::*; pub use render::*; From 1a5b5ef904aa0930f492f3bdaac20ab1c507d369 Mon Sep 17 00:00:00 2001 From: Viktor Gustavsson Date: Thu, 19 Sep 2024 23:54:55 +0200 Subject: [PATCH 02/25] Add Ghost Nodes example --- Cargo.toml | 11 ++++ examples/ui/ghost_nodes.rs | 120 +++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 examples/ui/ghost_nodes.rs diff --git a/Cargo.toml b/Cargo.toml index 1c95c166e0681..2022d42e85985 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2889,6 +2889,17 @@ description = "Demonstrates text wrapping" category = "UI (User Interface)" wasm = true +[[example]] +name = "ghost_nodes" +path = "examples/ui/ghost_nodes.rs" +doc-scrape-examples = true + +[package.metadata.example.ghost_nodes] +name = "Ghost Nodes" +description = "Demonstrates the use of Ghost Nodes" +category = "UI (User Interface)" +wasm = true + [[example]] name = "grid" path = "examples/ui/grid.rs" diff --git a/examples/ui/ghost_nodes.rs b/examples/ui/ghost_nodes.rs new file mode 100644 index 0000000000000..f777054e6ca86 --- /dev/null +++ b/examples/ui/ghost_nodes.rs @@ -0,0 +1,120 @@ +//! This example demonstrates the use of Ghost Nodes. + +use bevy::{prelude::*, ui::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) { + let font_handle = asset_server.load("fonts/FiraSans-Bold.ttf"); + + commands.spawn(Camera2dBundle::default()); + + // 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(), + )); + }); + + // 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, Counter(0))) + .with_children(|ghost_parent| { + // Ghost children using a separate counter state + // These buttons are being treated 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) -> TextBundle { + TextBundle::from_section( + text, + TextStyle { + font, + font_size: 33.0, + color: Color::srgb(0.9, 0.9, 0.9), + }, + ) +} + +fn button_system( + mut interaction_query: Query<(&Interaction, &Parent), (Changed, With