Custom marker component
In bevy it's common to have marker components. That is, components with no data that mark the entity as having some user-defined purpose. There are at least two ways to do this.
Before looking at either approach we should set some goals.
Goals:
- Add an exit button to the interface.
- Add a button to despawn the interface.
- On despawning add another button to respawn the interface.
Here is our code that we've built so far, adding in a MainInterface
marker component.
use bevy::prelude::*;
use bevy_cobweb_ui::prelude::*;
//-------------------------------------------------------------------------------------------------------------------
#[derive(Component)]
struct MainInterface;
fn build_ui(mut c: Commands, mut s: ResMut<SceneLoader>) {
c.spawn(Camera2d);
c.ui_root()
.load_scene_and_edit(("main.cob", "main_scene"), &mut s, |loaded_scene| {
loaded_scene
.get("cell::text")
.update_text("My runtime text");
for i in (0..=10).into_iter() {
loaded_scene.load_scene_and_edit(("main.cob", "number_text"), |loaded_scene| {
loaded_scene.edit("cell::text", |loaded_scene| {
loaded_scene.update_text(i.to_string());
loaded_scene.on_pressed(move|/* We write arbitary bevy parameters here*/|{
println!("You clicked {}", i);
});
});
});
}
});
}
//-------------------------------------------------------------------------------------------------------------------
fn main() {
App::new()
.add_plugins(bevy::DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
window_theme: Some(bevy::window::WindowTheme::Dark),
..default()
}),
..default()
}))
.add_plugins(CobwebUiPlugin)
.load("main.cob")
.add_systems(OnEnter(LoadState::Done), build_ui)
.run();
}
And here is the cob file we've built, adding in some small button scenes:
#![allow(unused)] fn main() { scenes "main_scene" AbsoluteNode{left:40% flex_direction:Column} "cell" Animated<BackgroundColor>{ idle:#FF0000 hover:Hsla{ hue:120 saturation:1.0 lightness:0.50 alpha:1.0 } press:Hsla{ hue:240 saturation:1.0 lightness:0.50 alpha:1.0 } } NodeShadow{color:#FF0000 spread_radius:10px blur_radius:5px} "text" TextLine{text:"Hello, World!, I am writing using cobweb "} "number_text" "cell" "text" TextLine{text:"placeholder"} TextLineColor(Hsla{hue:45 saturation:1.0 lightness:0.5 alpha:1.0}) Interactive "exit_button" TextLine{text:"Exit"} Interactive "despawn_button" TextLine{text:"Despawn"} Interactive "respawn_button" TextLine{text:"Respawn"} Interactive }
The exit and despawn buttons could just as easily be added as children of main_scene
.
Rust approach
Let's look at the first way of doing this using what is likely to be a more familiar approach to you.
#[derive(Component)]
struct MainInterface;
fn build_ui(mut c: Commands, mut s: ResMut<SceneLoader>) {
c.spawn(Camera2d);
c.ui_root()
.load_scene_and_edit(("main.cob", "main_scene"), &mut s, |loaded_scene| {
loaded_scene.insert(MainInterface); // <-- add the marker component
loaded_scene
.get("cell::text")
.update_text("My runtime text");
for i in (0..=10).into_iter() {
loaded_scene.load_scene_and_edit(("main.cob", "number_text"), |loaded_scene| {
loaded_scene.edit("cell::text", |loaded_scene| {
loaded_scene.update_text(i.to_string());
loaded_scene.on_pressed(move|/* We write arbitary bevy parameters here*/|{
println!("You clicked {}", i);
});
});
});
}
});
}
Now let's add the despawning button. We can use on_pressed
along with a normal bevy query.
use bevy::prelude::*;
use bevy_cobweb_ui::prelude::*;
//-------------------------------------------------------------------------------------------------------------------
#[derive(Component)]
struct MainInterface;
fn build_ui(mut c: Commands, mut s: ResMut<SceneLoader>) {
c.spawn(Camera2d);
c.ui_root()
.load_scene_and_edit(("main.cob", "main_scene"), &mut s, |loaded_scene| {
loaded_scene.insert(MainInterface);
loaded_scene
.get("cell::text")
.update_text("My runtime text");
for i in (0..=10).into_iter() {
loaded_scene.load_scene_and_edit(("main.cob", "number_text"), |loaded_scene| {
loaded_scene.edit("cell::text", |loaded_scene| {
loaded_scene.update_text(i.to_string());
loaded_scene.on_pressed(move|/* We write arbitary bevy parameters here*/|{
println!("You clicked {}",i);
});
});
});
}
// NEW: despawn button
loaded_scene.load_scene_and_edit(("main.cob", "despawn_button"), |loaded_scene| {
// Despawn the main interface on press.
// Notice this looks like a normal bevy query.
loaded_scene.on_pressed(
|interface_query: Query<Entity, With<MainInterface>>,
mut commands: Commands| {
// Cobweb callbacks can use `?` if you return `OK` or `DONE`.
// The .result() converts Options to Results.
commands
.get_entity(interface_query.get_single()?)
.result()?
.despawn_recursive();
OK
},
);
});
});
}
Now let's add the exit button, which will not by any more difficult:
use bevy::prelude::*;
use bevy_cobweb_ui::prelude::*;
//-------------------------------------------------------------------------------------------------------------------
#[derive(Component)]
struct MainInterface;
fn build_ui(mut c: Commands, mut s: ResMut<SceneLoader>) {
c.spawn(Camera2d);
c.ui_root()
.load_scene_and_edit(("main.cob", "main_scene"), &mut s, |loaded_scene| {
loaded_scene.insert(MainInterface);
loaded_scene
.get("cell::text")
.update_text("My runtime text");
for i in (0..=10).into_iter() {
loaded_scene.load_scene_and_edit(("main.cob", "number_text"), |loaded_scene| {
loaded_scene.edit("cell::text", |loaded_scene| {
loaded_scene.update_text(i.to_string());
loaded_scene.on_pressed(move|/* We write arbitary bevy parameters here*/|{
println!("You clicked {}", i);
});
});
});
}
loaded_scene.load_scene_and_edit(("main.cob", "despawn_button"), |loaded_scene| {
loaded_scene.on_pressed(
|interface_query: Query<Entity, With<MainInterface>>,
mut commands: Commands| {
commands
.get_entity(interface_query.get_single()?)
.result()?
.despawn_recursive();
OK
},
);
});
// NEW: exit button
loaded_scene.load_scene_and_edit(("main.cob", "exit_button"), |loaded_scene| {
loaded_scene.on_pressed(
|mut commands: Commands, focused_windows: Query<Entity, With<Window>>| {
let window = focused_windows.get_single()?;
commands.get_entity(window).result()?.despawn();
OK
},
);
});
});
}
Spawn new interface
Let's add a system for making respawn buttons. The respawn button will spawn the main interface.
fn spawn_respawn_button(mut c: Commands, mut s: ResMut<SceneLoader>) {
c.ui_root()
.load_scene_and_edit(("main.cob", "respawn_button"), &mut s, |loaded_scene| {
//TODO respawning main interface
});
}
Now call it on despawn
loaded_scene.load_scene_and_edit(("main.cob", "despawn_button"), |loaded_scene| {
loaded_scene.on_pressed(
|interface_query: Query<Entity, With<MainInterface>>,
mut commands: Commands| {
commands
.get_entity(interface_query.get_single()?)
.result()?
.despawn_recursive();
// NEW: spawn the respawn button
commands.run_system_cached(spawn_respawn_button);
},
);
});
run_system_cached
is a convenient way to call a function arbitrarily when you don't need to supply data. Consider observers if you do. You can also use syscall
from bevy_cobweb
.
We can now despawn the main interface. Another approach could have been writing run_system_cached
as an observer, and sending an event to trigger it in our "despawn_button"
callback.
All there is to do now is fill in the respawn logic. There is not much to discuss so will see the updated code below.
use bevy::prelude::*;
use bevy_cobweb_ui::prelude::*;
//-------------------------------------------------------------------------------------------------------------------
#[derive(Component)]
struct MainInterface;
fn build_ui(mut c: Commands) {
c.spawn(Camera2d);
c.run_system_cached(spawn_main_interface);
}
fn spawn_main_interface(mut c: Commands, mut s: ResMut<SceneLoader>) {
c.ui_root()
.load_scene_and_edit(("main.cob", "main_scene"), &mut s, |loaded_scene| {
loaded_scene.insert(MainInterface);
loaded_scene
.get("cell::text")
.update_text("My runtime text");
for i in (0..=10).into_iter() {
loaded_scene.load_scene_and_edit(("main.cob", "number_text"), |loaded_scene| {
loaded_scene.edit("cell::text", |loaded_scene| {
loaded_scene.update_text(i.to_string());
loaded_scene.on_pressed(move|/* We write arbitary bevy parameters here*/|{
println!("You clicked {}",i);
});
});
});
}
loaded_scene.load_scene_and_edit(("main.cob", "despawn_button"), |loaded_scene| {
loaded_scene.on_pressed(
|interface_query: Query<Entity, With<MainInterface>>,
mut commands: Commands| {
commands
.get_entity(interface_query.get_single()?)
.result()?
.despawn_recursive();
commands.run_system_cached(spawn_other_interface);
OK
},
);
});
loaded_scene.load_scene_and_edit(("main.cob", "exit_button"), |loaded_scene| {
loaded_scene.on_pressed(
|mut commands: Commands, focused_windows: Query<Entity, With<Window>>| {
let window = focused_windows.get_single()?;
commands.get_entity(window).result()?.despawn();
OK
},
);
});
});
}
fn spawn_respawn_button(mut c: Commands, mut s: ResMut<SceneLoader>) {
c.ui_root()
.load_scene_and_edit(("main.cob", "respawn_button"), &mut s, |loaded_scene| {
let entity = loaded_scene.id();
loaded_scene.on_pressed(move |mut commands: Commands| {
commands.get_entity(entity).result()?.despawn_recursive();
commands.run_system_cached(spawn_main_interface);
OK
});
});
}
//-------------------------------------------------------------------------------------------------------------------
fn main() {
App::new()
.add_plugins(bevy::DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
window_theme: Some(bevy::window::WindowTheme::Dark),
..default()
}),
..default()
}))
.add_plugins(CobwebUiPlugin)
.load("main.cob")
.add_systems(OnEnter(LoadState::Done), build_ui)
.run();
}
To despawn the current scene we can get the entity using .id()
.
Moving MainInterface to a cob file
Now let's try out another way to add marker components via cob files.
We need to add some derives:
#[derive(Component, Default, PartialEq, Reflect)]
struct MainInterface;
Now let's remove the .insert(MainInterface)
and register the component type.
use bevy::prelude::*;
use bevy_cobweb_ui::prelude::*;
//-------------------------------------------------------------------------------------------------------------------
#[derive(Component, Default, PartialEq, Reflect)]
struct MainInterface;
fn build_ui(mut c: Commands) {
c.spawn(Camera2d);
c.run_system_cached(spawn_main_interface);
}
fn spawn_main_interface(mut c: Commands, mut s: ResMut<SceneLoader>) {
c.ui_root()
.load_scene_and_edit(("main.cob", "main_scene"), &mut s, |loaded_scene| {
// <-- We no longer have insert here
loaded_scene
.get("cell::text")
.update_text("My runtime text");
// ...
});
}
fn spawn_respawn_button(mut c: Commands, mut s: ResMut<SceneLoader>) {
// ...
}
//-------------------------------------------------------------------------------------------------------------------
fn main() {
App::new()
.add_plugins(bevy::DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
window_theme: Some(bevy::window::WindowTheme::Dark),
..default()
}),
..default()
}))
.add_plugins(CobwebUiPlugin)
.register_component_type::<MainInterface>() // <-- This allows cob to load this type
.load("main.cob")
.add_systems(OnEnter(LoadState::Done), build_ui)
.run();
}
Let's now add MainInterface
to our cob file.
#scenes
"main_scene"
MainInterface // <-- NEW
AbsoluteNode{left:40%,flex_direction:Column}
"cell"
Animated<BackgroundColor>{
idle:#FF0000
hover:Hsla{ hue:120 saturation:1.0 lightness:0.50 alpha:1.0 }
press:Hsla{ hue:240 saturation:1.0 lightness:0.50 alpha:1.0 }
}
NodeShadow{color:#FF0000 spread_radius:10px blur_radius:5px}
"text"
TextLine{text:"Hello, World!, I am writing using cobweb "}
"number_text"
"cell"
"text"
TextLine{text:"placeholder"}
TextLineColor(Hsla{hue:45 saturation:1.0 lightness:0.5 alpha:1.0})
Interactive
"exit_button"
TextLine{text:"Exit"}
Interactive
"despawn_button"
TextLine{text:"Despawn"}
Interactive
"respawn_button"
TextLine{text:"Respawn"}
Interactive
We are now done.