Introduction

Cobweb is a UI and asset-management framework for the bevy game engine.

Cobweb Features

  • Custom scene format called COB
  • Localization framework (text, fonts, images, audio)
  • Font family API
  • Built-in UI widgets and color palettes
  • Asset management tools
  • And many quality of life features.

This book is intended to give you a starting point for making your own UI.

Structure of the book

We will start off with the most basic example possible. From there the book will split off into recipes that you can browse as you need them for your own projects.

The final section will cover some common errors.

Getting started

Commands

let's make an empty project to test it out:

cargo new cobweb_test
cd cobweb_test

This book won't be making any distinction between bevy_cobweb and bevy_cobweb_ui. bevy_cobweb is a reactivity library that bevy_cobweb_ui uses for convenience methods like .on_pressed.

cargo add bevy_cobweb
cargo add bevy_cobweb_ui -F hot_reload

We are definitely adding hot reloading, but you can remove it for your release version.

Syntax Highlighting

You can optionally install syntax highlighting for the cob files we will be using.

Instructions here.

Rust Code

Set your main.rs to be as below.

use bevy::prelude::*;
use bevy_cobweb_ui::prelude::*;

//-------------------------------------------------------------------------------------------------------------------

fn build_ui(mut c: Commands, mut s: ResMut<SceneLoader>)
{
    c.spawn(Camera2d);
    c.ui_root().load_scene(("main.cob", "main_scene"), &mut s);
}

//-------------------------------------------------------------------------------------------------------------------

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();
}

This will add systems to handle loading of your files and other plumbing: .add_plugins(CobwebUiPlugin)

This tells cobweb to load this cob file: .load("main.cob")

When all the cob files are loaded this will call our UI setup system: .add_systems(OnEnter(LoadState::Done), build_ui)

This makes a new UI hierarchy with its own root node: c.ui_root().load_scene(("main.cob", "main_scene"), &mut s);

COB code

Create a new folder called assets.

Create a new file called main.cob.

Add in the following:

#![allow(unused)]
fn main() {
scenes
"main_scene"
    TextLine{ text: "Hello, World!" }
}

Now let's run the program.

We have our first cobweb UI program. Next chapter we can start making changes without recompiling.

Cob files

Cob allows you to separate your UI from your code, in addition to enabling hot reloading of changes. Cob is whitespace-sensitive.

Sections

Cob files are made up of sections. For now, we will explore one type of section.

Scenes

Scenes are declared following the #scenes keyword. Scenes are given a name. In our example, we use "main_scene"... extremely creative. We use scene root nodes to load scenes Cobweb. We will need to recompile to add new scenes to our app.

Extending our example.

First, rerun the program if you have closed it. We want to see the hot reloading in action.

Let's change our cob file to be something like this:

#![allow(unused)]
fn main() {
scenes
"main_scene"
    TextLine{ text: "Hello, World!, I am writing using cobweb " }
}

Loadables

Loadables are Rust types that can be added to the scene nodes. For now, we will only explore one type of loadable:

Types that implement the instruction trait.

TextLine is an example of a loadable.

Loadables should not have space between their name and the opening {.

let's add another called AbsoluteNode:

#![allow(unused)]
fn main() {
scenes
"main_scene"
    AbsoluteNode{left:40% top:30vh}
    TextLine{ text: "Hello, World!, I am writing using cobweb" }

}

We have now moved the text around. You can also experiment with other units such as 40px 40vw.

Separate fields are not comma separated.

let's try another loadable BackgroundColor:

#![allow(unused)]
fn main() {
scenes
"main_scene"
    AbsoluteNode{left:40%}
    BackgroundColor(#FFFF00)
    TextLine{ text: "Hello, World!, I am writing using cobweb " }

}

We can see this does not contrast well with the text without recompiling!

Hex values convert to Srgba colours.

Animations

Let's add hovering effects. We will start off by changing the background colours based on user hovering.

Hovering and pressing
#![allow(unused)]
fn main() {
scenes
"main_scene"
    AbsoluteNode{left:40%}
    TextLine{ text: "Hello, World!, I am writing using cobweb " }
    
    Animated<BackgroundColor>{
        idle:#FF0000 // You can also input colours in other formats
        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 }
    }
}

Animated is a loadable that works with implementers of the AnimatableAttribute trait. One example implementer is BackgroundColor.

Next

Next we will look at how we know what loadables we can use based on the documentation, and what fields are available in each loadable.

Loadables and their fields

So far you have just been given the types to modify. This page aims to explain the different data types in cob files. When you read the documentation of each loadable hopefully you will be able to easily implement it.

Rerun your program if you have closed it.

Node shadow

let's start with adding a NodeShadow to our example. The documentation can be found here.

At the time of writing it looked like this:

#![allow(unused)]
fn main() {
pub struct NodeShadow {
    pub color: Color,
    pub x_offset: Val,
    pub y_offset: Val,
    pub spread_radius: Val,
    pub blur_radius: Val,
}
}

Let's start with Val before color as it is slightly simpler.

Val

Val variants can be written with special units (px, %, vw, vh, vmin, vmax) and the keyword auto. For example, 10px is equivalent to Px(10).

Colour

In our previous examples we have loaded colour using both RGB and HSLA.

Hex

Hex colours are a special data type in cob and can just be written as #FF00FF with an implied alpha of FF. You can also add an explicit alpha by adding in extra digits #FF00FFEE.

NewType collapsing

Loadable newtypes and newtype enum variants use newtype collapsing to simplify what you write. Newtypes are collapsed by discarding 'outer layers'.

An example we have used for HSLA. This is written in rust:

#![allow(unused)]
fn main() {
Color::Hsla(Hsla {
    hue: 240.0,
    saturation:1.0,
    lightness: 0.5,
    alpha:1.0,
})
}

And this is written in cob:

Hsla{ hue:240 saturation:1.0 lightness:0.50 alpha:1.0 }

The rule for enums inside loadables is A::B(C{ .. }) -> B{ .. }. Newtype collapsing can occur for loadables like A(vec![]) -> A[].

Defaults

Not all fields in loadables need to be filled out. Every field left blank we will be defaulted. Only fields annotated with #[reflect(default)] can be skipped.

Adding NodeShadow

With the above information we have enough to create our node shadow:

#![allow(unused)]
fn main() {
scenes
"main_scene"
    AbsoluteNode{left:40%}
    TextLine{ text: "Hello, World!, I am writing using cobweb " }
    NodeShadow{color:#FF0000 spread_radius:10px blur_radius:5px} // <-- our new node shadow
    Animated<BackgroundColor>{
        idle:#FF0000 // You can also input colours in other formats
        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 }
    }
    
}

Done

Floats

Floats are written similar to how they are written in rust.

  • Scientific notation: 1.2e3 or 1.2E3.
  • Integer-to-float conversion: 1 can be written instead of 1.0.
  • Keywords inf/-inf/nan: infinity, negative infinity, NaN.

Let's go ahead with an example using size in TextLine:

#![allow(unused)]
fn main() {
scenes
"main_scene"
    AbsoluteNode{left:40%}
    TextLine{ text: "Hello, World!, I am writing using cobweb " size:150 } // <-- add the size here
    NodeShadow{color:#FF0000 spread_radius:10px blur_radius:5px}
    Animated<BackgroundColor>{
        idle:#FF0000 // You can input colours in other formats
        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 }
    }

}

Strings

Strings are handled similar to how rust string literals are handled.

  • Enclosed by double quotes (e.g. "Hello, World!").
  • Escape sequences: standard ASCII escape sequences are supported (\n, \t, \r, \f, \", \\), in addition to Unicode code points (\u{..1-6 digit hex..}).
  • Multi-line strings: a string segment that ends in \ followed by a newline will be concatenated with the next non-space character on the next line.
  • Can contain raw Unicode characters.

Using rust to update text

We will be using rust to modify the contents of a text node at runtime (no hot reloading).

Modify the cob file

Let's setup the cob file as below.

#![allow(unused)]
fn main() {
"scene"
    AbsoluteNode{left:40%}
    "cell"
        Animated<BackgroundColor>{
            idle:#FF0000 // You can input colours in other formats
            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 "} // <-- will be overwritten

}

We split position logic into a child node called cell which holds most of the positioning and styling logic. cell has a child called text. Text is a minimal node responsible for just text stuff.

Why we split text from styling

This is more of an html/CSS pattern then anything particular with cobweb but it is worth mentioning here.

It just turns out to be easier to position nodes than it is to position text.

Rust

Updating text at runtime

Let's change the rust code to be as below.

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");
        });
}

We have changed load_scene to be load_scene_and_edit.

When we load "main_scene" in the cob file, we automatically load all the child nodes recursively. The second argument is a closure where we can use loaded_scene similar to commands along with extension methods provided by cobweb.

Inside the closure we call get(cell::text) which is basically a path syntax to go straight to the text node. It also possible to call edit on "cell" then call update_text inside the resulting closure.

Recompile and run the program. You will see your text has changed to reflect the rust code.

Spawning new nodes

Cobweb can also spawn new scenes inside other scenes. Let's start with an example.

Below we have our new scene called number_text.

If the concept of scenes was a bit confusing before, this should clarify it a bit more.

#![allow(unused)]
fn main() {
scenes
"scene"
    AbsoluteNode{left:40% flex_direction:Column}
    "cell"
        Animated<BackgroundColor>{
            idle:#FF0000 // You can input colours in other formats
            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"}
}

Now let's change our rust code to spawn some scenes.

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");

            // Spawning new ui nodes inside our main scene
            for i in (0..=10).into_iter() {
                loaded_scene.load_scene_and_edit(("main.cob", "number_text"), |loaded_scene| {
                    loaded_scene.get("cell::text").update_text(i.to_string());
                });
            }
        });
}

We now have some numbers that appear based on your code. We can still modify the cob files and change styling:

#![allow(unused)]
fn main() {
"number_text"
    "cell"
        "text"
            TextLine{text:"placeholder"}
            TextLineColor(Hsla{hue:45 saturation:1.0 lightness:0.5 alpha:1.0}) // <-- add this
}

Making nodes interactive

Setting our UI to react to the user is essential, and easy. First we need to add the Interactive loadable.

#![allow(unused)]
fn main() {
"number_text"
    "cell"
        "text"
            TextLine{text:"placeholder"}
            TextLineColor(Hsla{hue:45 saturation:1.0 lightness:0.5 alpha:1.0})
            Interactive // Sets up the node for user interaction

}

Now we can use on_pressed:

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 can write arbitrary bevy parameters here*/|{
                            println!("You clicked {}", i);
                        });
                    });
                });
            }
        });
}

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.

Cob file organization

#defs section

Cob files can have constants that let you define common values to be used all over your cob files. This will allow you to make changes in one place to impact multiple nodes.

Let's do an example.

// #defs is another type of section. So far we have only been doing #scenes.
#defs
$text_colour = Hsla{hue:45 saturation:1.0 lightness:0.5 alpha:1.0}

#scenes
"main_scene"
    MainInterface
    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($text_colour) // <-- now uses a constant
            Interactive


"exit_button"
    TextLine{text:"Exit"}
    TextLineColor($text_colour) // <-- now uses a constant
    Interactive
"despawn_button"
    TextLine{text:"Despawn"}
    TextLineColor($text_colour) // <-- now uses a constant
    Interactive
"respawn_button"
    TextLine{text:"Respawn"}
    TextLineColor($text_colour) // <-- now uses a constant
    Interactive

#manifest and #import

You can further extend this over many files using #manifest and #import sections.

#manifest

Manifests let you load many files recursively. This way you only need to write .load("manifest.cob") once in your app.

Make a central manifest file at assets/manifest.cob:

#manifest
"main.cob" as main
"ui/colour_scheme.cob" as cs

The as main defines a manifest key for the file main.cob.

Now replace the .load("main.cob") in your app with .load("manifest.cob"). You can also simplify .load_scene_and_edit(("main.cob", "main_scene"), ...) to .load_scene_and_edit(("main", "main_scene"), ...), using the main file's manifest key.

#import

Imports let you bring in #defs from other files.

In any file where you want to import defs you can do as below.

#import
cs as colours

Defs from cs are scoped to the import alias colours:

BackgroundColor($colours::background_colour)

Quick notes

This section will mention notes that did not come up in the tutorial but may still be helpful for you.

Misc Cob Syntax

Splat

splat is a shorthand way for multiple fields to be set the same value, you may have already encountered this in bevy with Vec2::splat or Vec3::splat.

To use splat the data type should implement splattable.

For example Splat<Border>(15px)

This should not be defined in a FlexNode or AbsoluteNode but as a seperate line.

Loading Cob files

When you have a cob file that defines scenes, you must register with the app by using this syntax (or add it to a manifest section of another file): .load("main.cob")

Waiting for LoadState::Done

If your UI is to appear at the start of the game then you should wait to ensure cobweb has had a chance to read its scene data. .add_systems(OnEnter(LoadState::Done), build_ui)

If your UI comes up in response to player actions on other UI, then calling it using observers or events should be fine as cob files would have been already read by cobweb.

Other features not covered (at least for now!)

Cobweb has reactive features.

Some examples can be found here.

You can use broadcasts to refresh your ui, update_on(broadcast::<MyArbitaryStruct>(),|/*bevy query*/|{}); Call it using commands.react().broadcast(MyArbitaryStruct).

There are others as well.

Other features

  • Radio button widget: we hope to have an example later. It can also be useful for tabbing.
  • Here's a good starting point for animations, states, interactions.
  • Localization: there is an example here, with documentation here.
  • Commands: cob files include a #commands section. TODO

Cob documentation.

You can find more details about cob files here.

Pulling existing node to edit

For creating nodes from scratch we used commands.ui_builder(UiRoot) (or commands.ui_root()). If we want to modify an existing UI node, substitue UiRoot with the entity you want to edit. If you use commands.get you can end up with this error:

WARN bevy_ui::layout: Node (233769v8) is in a non-UI entity hierarchy. You are using an entity with UI components as a child of an entity without UI components, your UI layout may be broken. at /home/lyndonm/.cargo/registry/src/index.crates.io-6f17d22bba15001f/bevy_ui-0.15.0-rc.3/src/layout/mod.rs:267

Errors

Forgetting to add Interactive to nodes

If your node defined in the cob file has to be interacted with (e.g. with .on_pressed()), make sure you put in Interactive.

Odd lifetime errors, usually about static lifetimes

If you capture data in closures like .on_pressed(), make sure you use move and clone anything you need.

Trying to load non-top-level scenes

Only the top level scenes can be loaded as scenes independently.

Multiple manifests warning.

You may get a warning like below, it means you added a file to multiple manifests in different files.

WARN bevy_cobweb_ui::loading::cache::commands_buffer: reparenting file CobFile("ui/colour_scheme.cob") from Parent(CobFile("ui/panels/outliner.cob")) to Parent(CobFile("ui/moons.cob")) at /home/lyndonm/.cargo/git/checkouts/bevy_cobweb_ui-68d12fe85b5a400c/5b3a3aa/src/loading/cache/commands_buffer.rs:485

Non using ui_builder to load a scene

If you get an error like below when loading a scene inside another scene, then substitute UiRoot with the entity you want to load into: commands.ui_builder(UiRoot)

WARN bevy_ui::layout: Node (233769v8) is in a non-UI entity hierarchy. You are using an entity with UI components as a child of an entity without UI components, your UI layout may be broken.
    at /home/lyndonm/.cargo/registry/src/index.crates.io-6f17d22bba15001f/bevy_ui-0.15.0-rc.3/src/layout/mod.rs:267