Cleanup

COB cleanup

Our cob file from last chapter looks like below.

It serves it's purpose but we can make this more maintainable.

Ideally we would have done it at the start but the choice was made to focus on the basic concepts.

#scenes
"main_scene"
    AbsoluteGridNode{left:30% width:40vw min_height:30vh}
    BackgroundColor(Hsla{ hue:221 saturation:0.5 lightness:0.15 alpha:0.5 })
    "title"
        FlexNode{justify_main:Center}
        "text"
            TextLine{ text: "Tabs education" }
            TextLineColor(Hsla{ hue:60 saturation:0.55 lightness:0.55 alpha:1.0 })

    // Actual tab menu
    "tab_menu"
        GridNode{grid_auto_flow:Column}
        RadioGroup
        "info"
            RadioButton
            FlexNode{justify_main:Center}
            ControlRoot
            Multi<Animated<BackgroundColor>>[
                {
                    idle: Hsla{ hue:221 saturation:0.5 lightness:0.15 alpha:0.5 }
                    hover: Hsla{ hue:24 saturation:0.5 lightness:0.50 alpha:1.0 }
                }
                {
                    state: [Selected]
                    idle: Hsla{ hue:221 saturation:0.5 lightness:0.20 alpha:0.5 }
                }
            ]
            Multi<Animated<NodeShadow>>[
                {
                    idle: {
                        color:Hsla{ hue:221 saturation:0.5 lightness:0.20 alpha:0.5 }
                        x_offset: 0px,
                        y_offset: 0px,
                        spread_radius: 1px,
                        blur_radius: 1px,
                    }
                }
                {
                    state: [Selected]
                    idle: {
                        color:Hsla{ hue:221 saturation:0.5 lightness:0.20 alpha:0.5 }
                        x_offset: 0px,
                        y_offset: 0px,
                        spread_radius: 10px,
                        blur_radius: 10px,
                    }
                }
            ]  

            "text"
                TextLine{ text: "Info" }
                ControlMember
                Multi<Animated<TextLineColor>>[
                    {
                        idle: Hsla{ hue:60 saturation:0.85 lightness:0.90 alpha:1.0 }
                        hover: Hsla{ hue:221 saturation:0.0 lightness:1.0 alpha:1.0 }
                    }
                    {
                        state: [Selected]
                        idle: #FFFF00
                    }
                ]  

            
        "exit"
            FlexNode{justify_main:Center}
            RadioButton
            ControlRoot
            Multi<Animated<BackgroundColor>>[
                {
                    idle: Hsla{ hue:221 saturation:0.5 lightness:0.15 alpha:0.5 }
                    hover: Hsla{ hue:24 saturation:0.5 lightness:0.50 alpha:1.0 }
                }
                {
                    state: [Selected]
                    idle: Hsla{ hue:221 saturation:0.5 lightness:0.20 alpha:0.5 }
                }
            ]  
            Multi<Animated<NodeShadow>>[
                {
                    idle: {
                        color:Hsla{ hue:221 saturation:0.5 lightness:0.20 alpha:0.5 }
                        x_offset: 0px,
                        y_offset: 0px,
                        spread_radius: 1px,
                        blur_radius: 1px,
                    }
                }
                {
                    state: [Selected]
                    idle: {
                        color:Hsla{ hue:221 saturation:0.5 lightness:0.20 alpha:0.5 }
                        x_offset: 0px,
                        y_offset: 0px,
                        spread_radius: 10px,
                        blur_radius: 10px,
                    }
                }
            ]  
            "text"
                ControlMember
                TextLine{ text: "Exit button" }
                Multi<Animated<TextLineColor>>[
                    {
                        idle: Hsla{ hue:60 saturation:0.85 lightness:0.90 alpha:1.0 }
                        hover: Hsla{ hue:221 saturation:0.0 lightness:1.0 alpha:1.0 }
                    }
                    {
                        state: [Selected]
                        idle: #FFFF00
                    }
                ]  

    // This is what changes based on menu selection
    "tab_content"
        GridNode{ height:25vh }
        BackgroundColor(Hsla{ hue:221 saturation:0.5 lightness:0.20 alpha:0.5 })

    "footer_content"
            FlexNode{justify_main:Center}
            "text"
                TextLine{ text: "I don't change" }
                TextLineColor(Hsla{hue:0 saturation:0.00 lightness:0.85 alpha:1.0})
        


// Tab content that will be spawned at runtime

"info_tab"
    FlexNode{justify_main:Center}
    "text"
        TextLine{text:"You are in the info tab"}

"exit_tab"
    // Not implemented
    FlexNode{justify_main:Center}
    "text"
        TextLine{text:"Click me to quit"}

#defs

We should start with defining colour names as we repeat many colours that are related.

At the top of the file.

#defs
$window_colour = Hsla{ hue:221 saturation:0.5 lightness:0.15 alpha:0.5 }


#scenes
"main_scene"
    AbsoluteGridNode{left:30% width:40vw min_height:30vh   }
    BackgroundColor($window_colour)
    //snip

We will be repeating this pattern.

I postfixed all the names with colour to disambiguate from other data we could add here like widths.

#defs
$window_colour = Hsla{ hue:221 saturation:0.5 lightness:0.15 alpha:0.5 }
$title_text_colour = Hsla{ hue:60 saturation:0.55 lightness:0.55 alpha:1.0 }

$tab_background_colour = Hsla{ hue:221 saturation:0.5 lightness:0.15 alpha:0.5 }

$tab_selected_background_colour = Hsla{ hue:221 saturation:0.5 lightness:0.20 alpha:0.5 }

$tab_hover_colour = Hsla{ hue:24 saturation:0.5 lightness:0.50 alpha:1.0 }

$box_shadow_colour = Hsla{ hue:221 saturation:0.5 lightness:0.20 alpha:0.5 }

$text_line_idle_colour = Hsla{ hue:60 saturation:0.85 lightness:0.90 alpha:1.0 }
$text_line_hover_colour =  Hsla{ hue:221 saturation:0.0 lightness:1.0 alpha:1.0 }

$text_selected_colour = #FFFF00

$footer_text_colour = Hsla{hue:0 saturation:0.00 lightness:0.85 alpha:1.0}

#scenes
"main_scene"
    AbsoluteGridNode{left:30% width:40vw min_height:30vh   }
    BackgroundColor($window_colour)
    "title"
        FlexNode{justify_main:Center}
        "text"
            TextLine{ text: "Tabs education" }
            TextLineColor($title_text_colour)

    // Actual tab menu
    "tab_menu"
        GridNode{grid_auto_flow:Column}
        RadioGroup
        "info"
            RadioButton
            FlexNode{justify_main:Center}
            ControlRoot
            Multi<Animated<BackgroundColor>>[
                {
                    idle: $tab_background_colour
                    hover: $tab_hover_colour
                }
                {
                    state: [Selected]
                    idle: $tab_selected_background_colour
                }
            ]
            Multi<Animated<NodeShadow>>[
                {
                    idle: {
                        color:$box_shadow_colour
                        x_offset: 0px,
                        y_offset: 0px,
                        spread_radius: 1px,
                        blur_radius: 1px,
                    }
                }
                {
                    state: [Selected]
                    idle: {
                        color:$box_shadow_colour
                        x_offset: 0px,
                        y_offset: 0px,
                        spread_radius: 10px,
                        blur_radius: 10px,
                    }
                }
            ]  

            "text"
                TextLine{ text: "Info" }
                ControlMember
                Multi<Animated<TextLineColor>>[
                    {
                        idle: $text_line_idle_colour
                        hover: $text_line_hover_colour
                    }
                    {
                        state: [Selected]
                        idle: $text_selected_colour
                    }
                ]  

            
        "exit"
            FlexNode{justify_main:Center}
            RadioButton
            ControlRoot
            Multi<Animated<BackgroundColor>>[
                {
                    idle: $tab_background_colour
                    hover: $tab_hover_colour
                }
                {
                    state: [Selected]
                    idle: $tab_selected_background_colour
                }
            ]  
            Multi<Animated<NodeShadow>>[
                {
                    idle: {
                        color:$box_shadow_colour
                        x_offset: 0px,
                        y_offset: 0px,
                        spread_radius: 1px,
                        blur_radius: 1px,
                    }
                }
                {
                    state: [Selected]
                    idle: {
                        color:$box_shadow_colour
                        x_offset: 0px,
                        y_offset: 0px,
                        spread_radius: 10px,
                        blur_radius: 10px,
                    }
                }
            ]  
            "text"
                ControlMember
                TextLine{ text: "Exit button" }
                Multi<Animated<TextLineColor>>[
                    {
                        idle: $text_line_idle_colour
                        hover: $text_line_hover_colour
                    }
                    {
                        state: [Selected]
                        idle: $text_selected_colour
                    }
                ]  

    // This is what changes based on menu selection
    "tab_content"
        GridNode{ height:25vh }
        BackgroundColor($tab_selected_background_colour)

    "footer_content"
            FlexNode{justify_main:Center}
            "text"
                TextLine{ text: "I don't change" }
                TextLineColor($footer_text_colour)
        


// Tab content that will be spawned at runtime

"info_tab"
    FlexNode{justify_main:Center}
    "text"
        TextLine{text:"You are in the info tab"}
"calm_tab" // <-- We will be adding this below

"exit_tab"
    // Not implemented
    FlexNode{justify_main:Center}
    "text"
        TextLine{text:"Click me to quit"}

Scene macros

Our two tab titles have an almost identical structure that we can share using a scene macro.

Changing the template is as simple as overwriting parts of its structure.

Code is now much shorter and it's easier to change the colour scheme.

#defs
$window_colour = Hsla{ hue:221 saturation:0.5 lightness:0.15 alpha:0.5 }
$title_text_colour = Hsla{ hue:60 saturation:0.55 lightness:0.55 alpha:1.0 }

$tab_background_colour = Hsla{ hue:221 saturation:0.5 lightness:0.15 alpha:0.5 }

$tab_selected_background_colour = Hsla{ hue:221 saturation:0.5 lightness:0.20 alpha:0.5 }

$tab_hover_colour = Hsla{ hue:24 saturation:0.5 lightness:0.50 alpha:1.0 }

$box_shadow_colour = Hsla{ hue:221 saturation:0.5 lightness:0.20 alpha:0.5 }

$text_line_idle_colour = Hsla{ hue:60 saturation:0.85 lightness:0.90 alpha:1.0 }
$text_line_hover_colour =  Hsla{ hue:221 saturation:0.0 lightness:1.0 alpha:1.0 }

$text_selected_colour = #FFFF00

$footer_text_colour = Hsla{hue:0 saturation:0.00 lightness:0.85 alpha:1.0}


+tab_selector = \
    RadioButton
    FlexNode{justify_main:Center}
    ControlRoot
    Multi<Animated<BackgroundColor>>[
        {
            idle: $tab_background_colour
            hover: $tab_hover_colour
        }
        {
            state: [Selected]
            idle: $tab_selected_background_colour
        }
    ]
    Multi<Animated<NodeShadow>>[
        {
            idle: {
                color:$box_shadow_colour
                x_offset: 0px,
                y_offset: 0px,
                spread_radius: 1px,
                blur_radius: 1px,
            }
        }
        {
            state: [Selected]
            idle: {
                color:$box_shadow_colour
                x_offset: 0px,
                y_offset: 0px,
                spread_radius: 10px,
                blur_radius: 10px,
            }
        }
    ]

    "text"
        TextLine // Placeholder
        ControlMember
        Multi<Animated<TextLineColor>>[
            {
                idle: $text_line_idle_colour
                hover: $text_line_hover_colour
            }
            {
                state: [Selected]
                idle: $text_selected_colour
            }
        ]  
\

#scenes
"main_scene"
    AbsoluteGridNode{left:30% width:40vw min_height:30vh   }
    BackgroundColor($window_colour)
    "title"
        FlexNode{justify_main:Center}
        "text"
            TextLine{ text: "Tabs education" }
            TextLineColor($title_text_colour)

    // Actual tab menu
    "tab_menu"
        GridNode{grid_auto_flow:Column}
        RadioGroup
        "info"
            +tab_selector{
                "text"
                    TextLine{text:"Info"}
            }
        "exit"
            +tab_selector{
                "text"
                    TextLine{text:"Exit"}
            }

    // This is what changes based on menu selection
    "tab_content"
        GridNode{ height:25vh }
        BackgroundColor($tab_selected_background_colour)

    "footer_content"
            FlexNode{justify_main:Center}
            "text"
                TextLine{ text: "I don't change" }
                TextLineColor($footer_text_colour)
        


// Tab content that will be spawned at runtime

"info_tab"
    FlexNode{justify_main:Center}
    "text"
        TextLine{text:"You are in the info tab"}

"exit_tab"
    // Not implemented
    FlexNode{justify_main:Center}
    "text"
        TextLine{text:"Click me to quit"}

Adding one more tab

With our fancy new scene macro it would be nice to add a new tab before we clean up the rust code.

    // Actual tab menu
    "tab_menu"
        GridNode{grid_auto_flow:Column}
        RadioGroup //Stores RadioButton State
        "info"
            +tab_selector{
                "text"
                    TextLine{text:"Info"}
            }
        "calm"
            +tab_selector{
                "text"
                    TextLine{text:"Calm"}
            }
        "exit"
            +tab_selector{
                "text"
                    TextLine{text:"Exit"}
            }

Calm will just be empty

Rust code

This is our rust code from the previous chapter.

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

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

fn build_ui(mut c: Commands, mut s: SceneBuilder) {
    c.spawn(Camera2d);
    c.ui_root()
        .spawn_scene(("main.cob", "main_scene"), &mut s, |scene_handle| {
            // Get entity to place in our scene
            let tab_content_entity = scene_handle.get("tab_content").id();
            scene_handle.edit("tab_menu::info", |scene_handle| {
                scene_handle.on_select(move |mut c: Commands, mut s: SceneBuilder| {
                    c.get_entity(tab_content_entity).result()?.despawn_descendants();

                    // Use this instead of c.get_entity()
                    c.ui_builder(tab_content_entity)
                        .spawn_scene_simple(("main.cob", "info_tab"), &mut s);
                    DONE
                });
            });
            // handling exit tab
            scene_handle.edit("tab_menu::exit", |scene_handle| {
                scene_handle.on_select(move |mut c: Commands, mut s: SceneBuilder| {
                    c.get_entity(tab_content_entity).result()?.despawn_descendants();

                    // Use this instead of c.get_entity()
                    c.ui_builder(tab_content_entity)
                        .spawn_scene_simple(("main.cob", "exit_tab"), &mut s);
                    DONE
                });
            });
        });
}

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

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

Starting tab

We can consolodate our tab change code into a function.

fn setup_tab_content(h: &mut UiSceneHandle, content_entity: Entity, scene: &'static str) {
    h.on_select(move |mut c: Commands, mut s: SceneBuilder| {
        c.get_entity(content_entity).result()?.despawn_descendants();
        c.ui_builder(content_entity)
            .spawn_scene_simple(("main.cob", scene), &mut s);
        DONE
    });
}

Now the code for using that function and setting up the starting tab.

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

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

fn setup_tab_content(h: &mut UiSceneHandle, content_entity: Entity, scene: &'static str) {
    h.on_select(move |mut c: Commands, mut s: SceneBuilder| {
        c.get_entity(content_entity).result()?.despawn_descendants();
        c.ui_builder(content_entity)
            .spawn_scene_simple(("main.cob", scene), &mut s);
        DONE
    });
}

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

fn build_ui(mut c: Commands, mut s: SceneBuilder) {
    c.spawn(Camera2d);
    c.ui_root()
        .spawn_scene(("main.cob", "main_scene"), &mut s, |scene_handle| {
            // Get entity to place our tab scenes in.
            let tab_content_entity = scene_handle.get("tab_content").id();

            scene_handle.edit("tab_menu::info", |scene_handle| {
                setup_tab_content(scene_handle, tab_content_entity, "info_tab");

                // Set this up as the starting tab.
                let id = scene_handle.id();
                scene_handle.react().entity_event(id, Select);
            });
            // calm tab
            scene_handle.edit("tab_menu::calm", |scene_handle| {
                setup_tab_content(scene_handle, tab_content_entity, "calm_tab");
            });
            // handling exit tab
            scene_handle.edit("tab_menu::exit", |scene_handle| {
                setup_tab_content(scene_handle, tab_content_entity, "exit_tab");
            });
        });
}

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

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