#markup-language #ui-elements #bevy-ui #component #class #ui-framework #tailwind

bevy_bsml

A UI library to compose UI elements using simple markup language, inspired by svelte and tailwindcss

6 releases

0.0.7 Jan 21, 2024
0.0.6 Jan 21, 2024

#241 in Game dev

MIT/Apache

145KB
3K SLoC

bevy_bsml

github crates.io docs.rs

BSML stands for Bevy's Simple Markup Language, or just BS Markup Language.

bevy_bsml allows you to compose UI elements using simple markup language, inspired by svelte and tailwindcss.

It's built on top of the official bevy_ui library, so you can still use bevy_ui to manually interact with the ui node or styles.

To see the basic usages of bevy_bsml, check out examples:

Table of Contents

Why not HTML or XML?

Because the angle bracketed markup languages don't work well with rust macros, and (...) and {...} work naturally.

Basics of BSML

In BSML, (...) contains the name of the element and its attributes, and {...} contains the content of the element, like nested elements.

Here is a basic example of BSML:

(node) {
    (text) { "hello world" }
}

which is equal to following html:

<div>
  hello world
</div>

If an element doesn't have any content, you can skip the braces:

(node)

This is a valid bsml; it will spawn a NodeBundle with default styles with no content.

You can also nest elements inside braces:

(node) {
    (node)
    (text) { "hello world" }
    (node) {
        (text) { "nested text" }
    }
}

is equal to

<div>
  <div />
  hello world
  <div>
    nested text
  </div>
</div>

Element Types in BSML

node

(node) is like div in html; you can have nested elements in it, or just use one without nested elements just to style it.

Available attributes are labels and class.

Examples:

100x100px blue box with no nested elements:

(node class=[w_px(100.0), h_px(100.0), BG_BLUE_400])

centering a node:

(node class=[W_FULL, H_FULL, JUSTIFY_CENTER, ITEMS_CENTER]) {
    (node class=[h_px(100.0), w_px(100.0), BG_BLUE_400])
}

text in the blue box:

(node class=[w_px(100.0), h_px(100.0), BG_BLUE_400]) {
    (text class=[TEXT_WHITE]}) { "hello world" }
}

with labels:

#[derive(Component)]
pub struct MyNode { i: u8 }
(node labels=[MyNode { i: 0 }] class=[w_px(100.0), h_px(100.0), BG_BLUE_400])

for

(for) is a node that iterates over a given iterator and repeats nested elements for each item.

The syntax is (for {<item> in <iterator>}) { <nested elements> }.

<item> is a variable name, and <iterator> can be any expression that implements IntoIterator, just like in for ... in ... loop.

Optionally, you can also get the index of the item like:

(for {<index>, <item> in <iterator>}) { <nested elements> }

Available attributes are labels and class.

Examples:

simple menu screen:

(for
    {name in ["Continue", "Setting", "Exit"]}
    class=[W_FULL, H_FULL, JUSTIFY_CENTER, ITEMS_CENTER, BG_WHITE]
) {
    (node class=[w_px(100.0), h_px(100.0), BG_BLUE_400]) {
        (text class=[TEXT_BASE]) { "{}", name }
    }
}

simple menu screen with index:

(for
    {i, name in ["Continue", "Setting", "Exit"]}
    class=[W_FULL, H_FULL, JUSTIFY_CENTER, ITEMS_CENTER, BG_WHITE]
) {
    (node class=[w_px(100.0), h_px(100.0), BG_BLUE_400]) {
        (text class=[TEXT_BASE]) { "{}: {}", i, name }
    }
}

slot

(slot) is a special element; it's used in reusable component definitions to expose space for its child elements.

When it has nested elements in braces, it will be used as default content of the slot, replaced when the slot is used. If you don't need default content, you can skip the braces.

Available attributes are labels and class.

Examples:

Define a Button component:

#[derive(Component)]
pub struct MyButton;

// define a button component
bsml!{MyButton;
    (slot class=[w_px(100.0), h_px(100.0), BG_BLUE_400, hovered(BG_BLUE_600)]) {
        (text class=[TEXT_WHITE]) { "I am button" } // default content
    }
}

fn spawn_ui_system(mut commands: Commands) {
    // spawn a screen using bsml!
    commands.spawn_bsml(bsml!(
        (node class=[W_FULL, H_FULL, JUSTIFY_CENTER, ITEMS_CENTER, BG_TRANSPARENT]) {
            (MyButton) { (text class=[TEXT_WHITE]) { "button 1" } }    // displays "button 1"
            (MyButton) { (text class=[TEXT_WHITE]) { "button 2" } }    // displays "button 2"
            (MyButton)                              // displays "i am button"
        }
    ));
}

text

(text) element is different than other elements in that you cannot nest elements inside it.

Instead, content inside {...} is exactly like arguments of format!(...).

Available attributes are labels and class.

Examples:

basic:

(text) { "hello world" }

with arguments:

(text) { "{} + {} = {}", 1, 2, 1 + 2 }

with styling:

(text class=[TEXT_XS]) { "I'm a tiny wittle text" }

with labels:

#[derive(Component)]
pub struct MyText;
(text labels=[MyText] class=[TEXT_XS]) { "I'm a tiny little text" }

Attributes in BSML

class

class attribute is used to specify the styles of the element, like tailwindcss.

In class attribute, you can provide list of provided style classes like W_FULL or BG_SLATE_200, or any expressions that return of one: StyleClass, BackgroundColorClass, BorderColorClass, and ZIndex.

Example Centering a node in the center of the screen

(node class=[W_FULL, H_FULL, JUSTIFY_CENTER, ITEMS_CENTER, BG_TRANSPARENT]) {
    (node class=[h_px(100.0), w_px(100.0), BG_BLUE_400])
}

The outer node will fill the entire screen, and center its content. The inner node will be 100x100px with blue background color.

Changing Styles on Interaction

You can specify styles that are applied when the node is hovered or pressed, like in tailwindcss.

(node class=[h_px(100.0), w_px(100.0), BG_BLUE_400, hovered(BG_BLUE_600), pressed(BG_BLUE_800)])

caveat: if you plan to use hovered or pressed style classes, you must specify also specify the base class, else it will not return back to previous style.

labels

labels attribute is a list of bevy components that are spawned with the node.

You can use these components to query for the node, or change data when the node is hovered or pressed.

You can then query for the node using the label component.

(node labels=[MyComponent])
#[derive(Component)]
pub struct MyComponent;

// get the entity of the node with MyComponent
// when the node is clicked or hovered
fn my_system(query: Query<Entity, (With<MyComponent>, Changed<Interaction>)>) {
    // ...
}

If your label component has fields, you can also provide expression that returns the initialized component.

(node labels=[MyComponent { i: 0, name: "hello".to_owned() }, label2()])
#[derive(Component)]
pub struct MyComponent {
    pub i: u8,
    pub name: String,
}

#[derive(Component)]
pub struct Label2;

fn label2() -> Label2 {
    Label2
}

and you can interact with the components in your systems:

fn my_system(query: Query<&MyComponent, (With<Label2>, Changed<Interaction>)>) {
    for my_component in query.iter() {
        println!("Interacted with node {}: {}", my_component.i, my_component.name);
    }
}

advanced: you can also use the fields of the label component in your bsml:

#[derive(Component)]
pub struct Label {
    pub text: &'static str,
    pub width: f32
}
(node labels=[Label { text: "hello world", width: 100.0 }]) {
    (text class=[w_px(labels.0.width)]) { "{}", labels.0.text }
}

Custom Reusable Components

Defining a Reusable Component

You can define your own reusable components using bsml.

The only requirement is that you must derive bevy::prelude::Component trait on the struct.

Here is an example of a reusable component definition:

#[derive(Component)]
pub struct MyComponent {
    pub i: u8,
    pub name: &'static str,
}

bsml! {MyComponent;
    (node class=[h_px(100.0), w_px(100.0), BG_BLUE_400]) {
        (text class=[TEXT_WHITE, TEXT_BASE]) { "index: {}, name: {}", self.i, self.name }
    }
}

In the macro, notice the component struct followed by semicolon, which is not part of the bsml syntax.

This may seem a bit jarring, but this is intentional to make it clear that you are defining a reusable component.

Using Component Fields in BSML

As you may have noticed, you can also use the fields of the component in bsml.

You can use it in class, labels, or in nested element contents like text element, by referencing them like self.<field_name>.

#[derive(Component)]
pub struct MyComponent {
    pub i: u8,
    pub name: &'static str,
    pub width: f32,
}

bsml! {MyComponent;
    (node class=[h_px(100.0), w_px(self.width), BG_BLUE_400]) {
        (text class=[TEXT_WHITE, TEXT_BASE]) { "index: {}, name: {}", self.i, self.name }
    }
}

note that these are not reactive. They are evaluated only once when the component is initialized.

Even if the field value changes, the bsml UI will not be updated.

Using a Reusable Component

After you have defined a reusable component, you can use it either by spawning it directly or including in other bsml elements.

Spawning component directly

The easiest way to use a reusable component is to spawn it directly.

use bevy_bsml::prelude::*;

fn spawn_bsml_ui(mut commands: Commands) {
    commands.spawn_bsml(MyComponent { i: 0, name: "hello" });
}

Including in other bsml elements

When using a reusable component in other bsml elements, you can use any expression that returns the initialized component in a parenthesis.

use bevy_bsml::prelude::*;

#[derive(Component)]
pub struct MyContainer;

bsml! {MyContainer;
    (node class=[W_FULL, H_FULL, JUSTIFY_CENTER, ITEMS_CENTER, BG_TRANSPARENT]) {
        (MyComponent { i: 0, name: "hello" })
    }
}

fn spawn_bsml_ui(mut commands: Commands) {
    commands.spawn_bsml(MyContainer);
}

The expression can be anything. You could even do something like:

use bevy_bsml::prelude::*;

#[derive(Component)]
pub struct MyContainer;

fn component(i: u8, name: &'static str) -> MyComponent {
    MyComponent { i, name }
}

bsml! {MyContainer;
    (node class=[W_FULL, H_FULL, JUSTIFY_CENTER, ITEMS_CENTER, BG_TRANSPARENT]) {
        (component(0, "hello"))
    }
}

fn spawn_bsml_ui(mut commands: Commands) {
    commands.spawn_bsml(MyContainer);
}

Spawning UI Elements using BSML

There are two ways to spawn UI elements using bsml.

Spawning an anonymous UI element

You can directly include the bsml! macro in Commands::spawn_bsml method.

use bevy_bsml::prelude::*;

fn spawn_bsml_ui(mut commands: Commands) {
    commands.spawn_bsml(bsml!(
        (node class=[W_FULL, H_FULL, JUSTIFY_CENTER, ITEMS_CENTER, BG_TRANSPARENT]) {
            (node class=[h_px(100.0), w_px(100.0), BG_BLUE_400])
        }
    ));
}

Spawning a Reusable Component

See Spawning Component Directly.

Despawning BSML Elements

To despawn bsml elements, you can use Commands::despawn_bsml method, and provide the component's entity.

use bevy_bsml::prelude::*;

fn spawn_bsml_ui(mut commands: Commands) {
    // spawn a screen using bsml!
    let entity = commands.spawn_bsml(bsml!(
        (node class=[W_FULL, H_FULL, JUSTIFY_CENTER, ITEMS_CENTER, BG_TRANSPARENT]) {
            (node class=[h_px(100.0), w_px(100.0), BG_BLUE_400])
        }
    ))
    .id();

    // despawn the screen entity
    commands.despawn_bsml(entity);
}

Reactivity with Spawned Components

Since bsml spawns bev_ui components internally, you can just use bevy::ui::Interaction to detect and react to UI interactions.

Check out examples to see how to react to UI interactions:

Dependencies

~22MB
~417K SLoC