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

bevy_bsml

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

18 releases

0.14.10 Aug 31, 2024
0.14.9 Aug 29, 2024
0.14.0 Jul 17, 2024
0.0.8 Jun 22, 2024
0.0.7 Jan 21, 2024

#113 in Game dev

Download history 173/week @ 2024-08-10 154/week @ 2024-08-17 148/week @ 2024-08-24 195/week @ 2024-08-31 5/week @ 2024-09-07 1/week @ 2024-09-14 6/week @ 2024-09-21 7/week @ 2024-09-28 3/week @ 2024-10-05 2/week @ 2024-10-12 47/week @ 2024-11-02 4/week @ 2024-11-16 1/week @ 2024-11-23

52 downloads per month

MIT/Apache

155KB
2.5K 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:

For Breaking Changes and New Features, see CHANGELOG.md

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.

Supported Bevy Versions

From version 0.14, bevy_bsml will keep the same minor version as Bevy, while patch version will vary.

Bevy bevy_bsml
0.14 0.14
0.13 0.0.8
0.12 0.0.7

Setup

Import the prelude module, and add BsmlPlugin.

use bevy_bsml::prelude::*;

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .add_plugin(BsmlPlugin)
        .run();
}

See the examples for more detailed usage.

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(100.0), h(100.0), BG_BLUE_400])

centering a node:

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

text in the blue box:

(node class=[w(100.0), h(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(100.0), h(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(100.0), h(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(100.0), h(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(100.0), h(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!(...).

Optionally, you can also provide text value via class fn text(impl ToString).

Available attributes are labels and class.

Examples:

basic:

(text) { "hello world" }

// same as
(text class=[text("hello world")])

with arguments:

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

// same as
(text class=[text(format!("{} + {} = {}", 1, 2, 1 + 2))])

with styling:

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

with custom font:

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

with labels:

#[derive(Component)]
pub struct MyText;

#[derive(Component)]
pub struct Menu;

bsml! {Menu;
    (node) {
        (text labels=[MyText] class=[TEXT_XS]) { "I'm a tiny little text" }
    }
}

reactive example:

#[derive(Component)]
pub struct MyText(pub String);

// Initialize text in class with its value.
// Changes text size, and color when hovered
bsml! {MyText;
    (text class=[
        TEXT_XS, TEXT_BLACK, text(self.0),
        hovered(TEXT_2XL), hovered(TEXT_BLUE)
    ])
}

// change text when value changes
fn reactive_text(
    query: Query<(&MyText, &mut BsmlClasses), Changed<MyText>>
) {
    let Ok((value, mut classes)) = query.get_single_mut() else {
        return;
    };

    classes.insert(Interaction::None, text(&value.0));
}

img

(img) element is used to render an image.

This element is also different than other elements in that you cannot nest elements inside it.

Instead, content inside {...} can be any expression that returns UiImage.

Available attributes are labels and class.

Examples:

basic:

(img) { some_fn_returning_UiImage() }

(img) { UiImage { color: ..., texture: ..., flip_x: false, flip_y: false } }

with styling:

(img class=[W_FULL]) { UiImage { color: ..., texture: ..., flip_x: false, flip_y: false } }

with labels:

#[derive(Component)]
pub struct MyImg;
(img labels=[MyImg] class=[W_FULL]) { UiImage { color: ..., texture: ..., flip_x: false, flip_y: false }

material

(material) element is used to render material in a ui Node; internally, it uses MaterialNodeBundle.

This element is also different than other elements in that you cannot nest elements inside it.

Instead, content inside {...} can be any expression that returns Handle<M: UiMaterial>.

Available attributes are labels and class.

Examples:

basic:

(material) { some_fn_returning_material_handle() }

with styling:

(material class=[W_FULL]) { some_fn_returning_material_handle() }

with labels:

#[derive(Component)]
pub struct MyMaterial;
(material labels=[MyMaterial] class=[W_FULL]) { some_fn_returning_material_handle() }

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(100.0), w(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(100.0), w(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(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(100.0), w(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(100.0), w(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(100.0), w(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(100.0), w(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

~36–73MB
~1.5M SLoC