cuicui_dsl
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?
- Define a type that implements 
DslBundle - Define methods with a 
&mut selfreceiver on this type - Use the methods of the type in question in the 
dsl!macro 
#![allow(unused)] fn main() { 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 theMyDslmethods). Check the documentation page for the corresponding type you are using as DSL. All methods that accept an&mut selfare 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:
- (optional) the 
DslBundletype you want to use as “builder” for the DSL. - The 
&mut EntityCommandsto spawn the scene into. - 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
 
 - several methods within 
 
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:
#![allow(unused)] fn main() { 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:
#![allow(unused)] fn main() { 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:
#![allow(unused)] fn main() { 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:
#![allow(unused)] fn main() { 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 | rust1 | ✅ | 
| import from other files | rust2 | |
| 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.
A fn template is equivalent to defining a function that accepts an
EntityCommands and directly calls dsl! with it
#![allow(unused)] fn main() { 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"))) } } } }
You can — of course — import functions from other files in rust and use that instead.