#bevy #layout #ui

cuicui_dsl

An easily-extensible macro to spawn things in bevy

14 releases (9 breaking)

0.12.0 Nov 10, 2023
0.10.2 Oct 25, 2023
0.8.1 Jul 20, 2023

#1280 in Game dev

Download history 124/week @ 2023-11-02 121/week @ 2023-11-09 78/week @ 2023-11-16 59/week @ 2023-11-23 68/week @ 2023-11-30 25/week @ 2023-12-07 37/week @ 2023-12-14 51/week @ 2023-12-21 16/week @ 2023-12-28 34/week @ 2024-01-04 20/week @ 2024-01-11 20/week @ 2024-01-18 44/week @ 2024-01-25 38/week @ 2024-02-01 56/week @ 2024-02-08 188/week @ 2024-02-15

328 downloads per month
Used in 5 crates

MIT/Apache

35KB
239 lines

cuicui_dsl

The Book Documentation

cuicui_dsl is a crate exposing a single trait (DslBundle) and a single macro (dsl!) to define bevy scenes within rust code.

It is used in cuicui for UI, but can be used for any kind of scene.

When to use cuicui_dsl?

  • You want an extremely lightweight yet powerful scene definition DSL in bevy to replace the innane cmds.spawn().insert().with_children() dance.
  • You don't care about having to re-compile the whole game each time you change your scene.

How to use cuicui_dsl?

  1. Define a type that implements DslBundle
  2. Define methods with a &mut self receiver on this type
  3. Use the methods of the type in question in the dsl! macro
# use cuicui_dsl::macros::__doc_helpers::*; // ignore this line pls
# use std::borrow::Cow;
use cuicui_dsl::{dsl, DslBundle, EntityCommands};

// DslBundle requires Default impl
#[derive(Default)]
pub struct MyDsl {
    style: Style,
    bg_color: Color,
    font_size: f32,
    inner: BaseDsl,
}
impl MyDsl {
    pub fn named(&mut self, name: impl Into<Cow<'static, str>>) {
        self.inner.named(name);
    }
    pub fn style(&mut self, style: Style) {
        self.style = style;
    }
    pub fn bg_color(&mut self, bg_color: Color) {
        self.bg_color = bg_color;
    }
    pub fn font_size(&mut self, font_size: f32) {
        self.font_size = font_size;
    }
}
impl DslBundle for MyDsl {
    fn insert(&mut self, cmds: &mut EntityCommands) {
        cmds.insert(self.style.clone());
        cmds.insert(BackgroundColor(self.bg_color));
        self.inner.insert(cmds);
        // ...
    }
}
// Now you can use `MyDsl` in a `dsl!` macro
fn setup(mut cmds: Commands) {
    let height = px(32);
    dsl! {
        <MyDsl>
        &mut cmds.spawn_empty(),
        // The uppercase name at the start of a statement is the entity name.
        Root(style(Style { flex_direction: FlexDirection::Column, ..default()}) bg_color(Color::WHITE)) {
            Menu(style(Style { height, ..default()}) bg_color(Color::RED))
            Menu(style(Style { height, ..default()}) bg_color(Color::GREEN))
            Menu(style(Style { height, ..default()}) bg_color(Color::BLUE))
        }
    };
}

Documentation

Methods available in the dsl! macro are the methods available in the choosen DSL type (in this case, it would be the MyDsl methods). Check the documentation page for the corresponding type you are using as DSL. All methods that accept an &mut self are candidate.

This seems a bit verbose, that's because you should be using cuicui_layout and not bevy's native layouting algorithm (flexbox) for layouting :)

The docs.rs page already has extensive documentation on the dsl! macro, with a lot of examples.

The short of it is:

dsl! accepts three arguments:

  1. (optional) the DslBundle type you want to use as "builder" for the DSL.
  2. The &mut EntityCommands to spawn the scene into.
  3. A single statement

What is a statement? A statement is:

  • An EntityName (which is a single identifier) followed by either:
    • several methods within (parenthesis)
    • several children statements within {curly braces}
    • both of the above

A statement creates a Default::default() of the choosen DslBundle type. Then, each mehtod within parenthesis is called on the choosen DslBundle type. Finally, an entity is spawned using the DslBundle::insert method on the thus-constructed DslBundle. The spawned entity has the Name component set to the identifier provided for EntityName.

Children are added to that entity if child statements are specified within braces.

Still confused about it? I encourage you to either look at the examples or check the docs at:

DSL-specific documentation

Since dsl! is just a wrapper around method calls, you can refer to the docs.rs page for the DslBundle implementation you chose to use in your dsl!.

Tips and tricks

Behind the veil

The dsl! macro is basically a way to translate an imperative sequential API into a declarative functional API.

When you write:

# use cuicui_dsl::macros::__doc_helpers::*; // ignore this line pls
use cuicui_dsl::dsl;
# fn sys(mut cmds: EntityCommands) {
dsl! {
    <BlinkDsl>
    &mut cmds,
    Root {
        FastBlinker(frequency(0.5))
        SlowBlinker(amplitude(2.) frequency(3.0))
    }
}
# }

The dsl! macro translates it into:

# use cuicui_dsl::macros::__doc_helpers::*; // ignore this line pls
# fn sys(mut cmds: EntityCommands) {
let mut root = BlinkDsl::default();
root.named("Root");
root.node(&mut cmds, |cmds| {
    let mut fast_blinker = BlinkDsl::default();
    fast_blinker.named("FastBlinker");
    fast_blinker.frequency(0.5);
    fast_blinker.insert(&mut cmds.spawn_empty());

    let mut slow_blinker = BlinkDsl::default();
    slow_blinker.named("SlowBlinker");
    slow_blinker.amplitude(2.);
    slow_blinker.frequency(3.0);
    slow_blinker.insert(&mut cmds.spawn_empty());
});
# }

The DslBundle::insert impl of BlinkDsl takes care of converting itself into a set of components it will insert on an entity.

See the dsl! documentation for more details and examples.

Inheritance

The cuicui crates compose different DslBundles with a very filthy trick.

Using DerefMut, you can get both the methods of your custom DslBundle and the methods of another DslBundle embedded into your custom DslBundle (and this works recursively).

Use the bevy Deref and DerefMut derive macros to accomplish this:

# use cuicui_dsl::macros::__doc_helpers::*; // ignore this line pls
use cuicui_dsl::DslBundle;

// `= ()` means that if not specified, there is no inner DslBundle
#[derive(Default, Deref, DerefMut)]
pub struct MyDsl<D = ()> {
    #[deref]
    inner: D,
    style: Style,
    bg_color: Color,
    font_size: f32,
}
impl<D: DslBundle> DslBundle for MyDsl<D> {
    fn insert(&mut self, cmds: &mut EntityCommands) {
        cmds.insert(self.style.clone());
        // ... other components to insert ...
        // Always call the inner type at the end so that insertion order follows
        // the type declaration order.
        self.inner.insert(cmds);
    }
}
// Both the methods defined on `MyDsl`
// and the provided `D` are available in the `dsl!` macro for `<MyDsl<D>>`

Performance

The downside of the aforementioned trick is the size of your DslBundles. Very large DslBundles tend to generate a lot of machine code just to move them in and out of functions.

Try keeping the size of your DslBundles down using bitsets crates such as enumset or bitflags instead of bool fields.

Consider also Boxing some large components such as Style to avoid the cost of moving them.

Storing a dynamic set of bundles in your DslBundle

If you are a lazy butt like me, you don't need to add a field per bundles/component managed by your DslBundle, you can store a Vec of bundle spawners as follow:

# use cuicui_dsl::macros::__doc_helpers::*; // ignore this line pls
use cuicui_dsl::{EntityCommands, DslBundle};

#[derive(Default)]
pub struct MyDynamicDsl(Vec<Box<dyn FnOnce(&mut EntityCommands)>>);

impl MyDynamicDsl {
    pub fn named(&mut self, name: &str) {
        let name = name.to_string();
        self.0.push(Box::new(move |cmds| {cmds.insert(Name::new(name));}));
    }
    pub fn transform(&mut self, transform: Transform) {
        self.0.push(Box::new(move |cmds| {cmds.insert(transform);}));
    }
    pub fn style(&mut self, style: Style) {
        self.0.push(Box::new(move |cmds| {cmds.insert(style);}));
    }
    // ... Hopefully you get the idea ...
}
impl DslBundle for MyDynamicDsl {
    fn insert(&mut self, cmds: &mut EntityCommands) {
        for spawn in self.0.drain(..) {
            spawn(cmds);
        }
    }
}

What is the relationship between cuicui_dsl and cuicui_chirp?

cuicui_dsl is a macro (dsl!), while cuicui_chirp is a scene file format, parser and bevy loader. cuicui_chirp builds on top of cuicui_dsl, and has different features than cuicui_dsl. Here is a feature matrix:

features cuicui_dsl cuicui_chirp
statements & methods
code blocks with in-line rust code
code calling registered functions
fn templates rust[^1]
import from other files rust[^2]
hot-reloading
reflection-based methods
special syntax for colors, rules
lightweight
Allows for non-Reflect components

You may use cuicui_dsl in combination with cuicui_chirp, both crates fill different niches.

[^1]: A fn template is equivalent to defining a function that accepts an EntityCommands and directly calls dsl! with it \

# use cuicui_dsl::macros::__doc_helpers::*; // ignore this line pls
use cuicui_dsl::{dsl, EntityCommands};

fn rust_template(cmds: &mut EntityCommands, serv: &AssetServer) {
  dsl! {
    cmds,
    Root(screen_root column) {
      Menu(image(&serv.load("menu1.png")))
      Menu(image(&serv.load("menu2.png")))
    }
  }
}

[^2]: You can — of course — import functions from other files in rust and use that instead.

Dependencies

~16–58MB
~1M SLoC