Macro cuicui_layout::dsl
source · macro_rules! dsl { (@arg [$x:tt] ) => { ... }; (@arg [$x:tt] $m:ident ($($arg:tt)*) $($t:tt)*) => { ... }; (@arg [$x:tt] $m:ident $($t:tt)*) => { ... }; (@statement [$d_ty:ty, $cmds:expr] ) => { ... }; (@statement [$d_ty:ty, $cmds:expr] code (let $cmds_ident:ident) {$($code:tt)*} $($($t:tt)+)?) => { ... }; (@statement [$d_ty:ty, $cmds:expr] Entity ($($args:tt)*) {} $($t:tt)*) => { ... }; (@statement [$d_ty:ty, $cmds:expr] Entity ($($args:tt)*) {$($inner:tt)*} $($t:tt)*) => { ... }; (@statement [$d_ty:ty, $cmds:expr] spawn ($($args:tt)*) $($t:tt)*) => { ... }; (@statement [$d_ty:ty, $cmds:expr] Entity ($($args:tt)*) $($t:tt)*) => { ... }; (@statement [$d_ty:ty, $cmds:expr] Entity $($t:tt)*) => { ... }; (@statement [$d_ty:ty, $cmds:expr] $entity_name:literal ($($args:tt)*) $($t:tt)*) => { ... }; (@statement [$d_ty:ty, $cmds:expr] $entity_name:literal $($t:tt)*) => { ... }; (@statement [$d_ty:ty, $cmds:expr] $entity_name:ident ($($args:tt)*) $($t:tt)*) => { ... }; (@statement [$d_ty:ty, $cmds:expr] $entity_name:ident $($t:tt)*) => { ... }; (<$builder:ty> $cmds:expr, $($t:tt)*) => { ... }; ($cmds:expr, $($t:tt)*) => { ... }; }
Expand description
Reorganize rust method syntax to play wonderfully with bevy’s hierarchy spawning mechanism.
Basically, this is a way to use &mut self
methods on an arbitrary type
but in a declarative way.
Usage
The crate-level doc for this has a nice example, you can check it out:
crate
.
Cheat sheet
You already know how to use dsl!
? here are the quick links:
Extending dsl!
Since dsl!
is straight up nothing more than sugar on top of rust’s
method call syntax, it’s trivial to add your own methods/statements.
With bevy’s DerefMut
derive, it’s even possible to build on top of
existing implementations.
Warning: Is it wise to abuse the
DerefMut
trait this way?I dunno, but it makes everything so much more convenient. See https://github.com/nicopap/cuicui_layout/issues/26
Consider BaseDsl
, it only has a single method: named
. But we want
to create blinking UI. How do we do it?
Like in any bevy project, we would do as follow:
- Define a
Blink
component. - Define a system that reads the
Blink
component and update some color/sprite. - Optionally create a
BlinkBundle
that adds to an entity all things necessary for blinking to work.
#[derive(Component, Default)]
struct Blink {
frequency: f32,
amplitude: f32,
}
#[derive(Bundle, Default)]
struct BlinkBundle {
blink: Blink,
spatial: SpatialBundle,
}
We want to have a DSL that let us set the frequency
and amplitude
of the Blink
component.
More importantly though, we want our DSL to compose with any other DSL!
For this, we will add an inner
field and use the bevy DerefMut
derive
macro:
#[derive(Deref, DerefMut, Default)]
struct BlinkDsl<D = ()> {
#[deref]
inner_dsl: D,
pub blink: Blink,
}
impl<D: DslBundle> DslBundle for BlinkDsl<D> {
fn insert(&mut self, cmds: &mut EntityCommands) {
// We insert first `Blink`, as to avoid overwriting things
// `inner_dsl.insert` might insert itself.
cmds.insert(BlinkBundle { blink: self.blink, ..default() });
self.inner_dsl.insert(cmds);
}
}
// `dsl!` relies on method calls, so we need to define methods:
impl<D> BlinkDsl<D> {
pub fn frequency(&mut self, frequency: f32) {
self.blink.frequency = frequency;
}
pub fn amplitude(&mut self, amplitude: f32) {
self.blink.amplitude = amplitude;
}
}
type Dsl = BlinkDsl<BaseDsl>;
dsl! {
&mut cmds,
Entity {
FastBlinker(frequency(0.5))
SlowBlinker(amplitude(2.) frequency(3.0))
}
}
If we want to use a pre-existing DSL with ours, we would nest them.
Since we #[deref] inner: D
, all methods on the inner DSL are available
on the outer DSL.
type Dsl = BlinkDsl<LayoutDsl>;
dsl! {
&mut cmds,
Entity {
Entity(ui("Fast blink") frequency(0.5) color(Color::GREEN))
Entity(row frequency(1.) amplitude(1.0) main_margin(10.) fill_main_axis) {
Entity(ui("Some text") amplitude(10.0) color(Color::BLUE))
}
Entity(ui("Slow blink") frequency(2.) color(Color::RED))
}
}
We made our DSL nestable so that it is itself composable. Say we are making a public crate, and our users want the UI DSL on top of ours. They would simply define their own DSL as follow:
type UserDsl = UiDsl<BlinkDsl<LayoutDsl>>;
And it would work as is.
Syntax
dsl!
accepts as argument:
- (optionally) between
<$ty>
, aDslBundle
type. By default, it will use the identifierDsl
in scope. This will be referred asDsl
in the rest of this documentation. - An expression of type
&mut EntityCommands
. - A single DSL statement.
- DSL statements contain themselves series of DSL methods.
DSL statements
A DSL statement spawns a single entity.
There are three kinds of DSL statements:
- Entity statements
- leaf node statement
- parent node statement
- code statement
Entity
Entity statements create an Entity
and calls DslBundle::insert
.
They basically spawn an entity with the given DSL methods.
Optionally, they can act like parent nodes if they are directly followed by curly braces:
Entity([dsl methods]*)
Entity([dsl methods]*) {
[dsl statements]*
}
Concretely:
dsl!{ &mut cmds,
Entity(color(Color::BLUE) rules(px(40), pct(100)))
};
dsl!{ &mut cmds,
Entity(fill_main_axis) {
Entity(color(Color::GREEN))
}
};
This will expand to the following code:
let mut x = <Dsl>::default();
x.color(Color::BLUE);
x.rules(px(40), pct(100));
x.insert(&mut cmds);
let mut x = <Dsl>::default();
x.fill_main_axis();
x.node(&mut cmds, |cmds| {
let mut x = <Dsl>::default();
x.color(Color::GREEN);
x.insert(&mut cmds.spawn_empty());
});
Leaf node
Leaf node statements are statements without subsequent braces.
The head identifier is used as the spawned entity’s name. You may also use any rust literal (including strings) instead of an identifier.
It looks as follow:
<ident>([dsl methods]*)
Concretely:
ButtonText(color(Color::BLUE) width(px(40)) height(pct(100)) button_named)
This expands to:
let mut x = <Dsl>::default();
let mut c: &mut EntityCommands = &mut cmds;
x.named("ButtonText");
x.color(Color::BLUE);
x.width(px(40));
x.height(pct(100));
x.button_named();
x.insert(c);
Parent node
The parent node statement has the following syntax:
<ident>([dsl method]*) {
[dsl statement]*
}
Concretely, it looks like the following:
Root(screen_root main_margin(100.) align_start image(&bg) row) {
ButtonText1(color(Color::BLUE) rules(px(40), pct(100)) button_named)
ButtonText2(color(Color::RED) rules(px(40), pct(100)) button_named)
Menu(width(px(310)) main_margin(10.) fill_main_axis image(&board) column) {
TitleCard(rules(pct(100), px(100)))
}
}
The part between parenthesis (()
) is a list of DSL methods.
They are applied to the Dsl
DslBundle
each one after the other.
Then, an entity is spawned with the so-constructed bundle,
following, the DSL statements within braces ({}
) are spawned
as children of the parent node entity.
For the visually-minded, this is how the previous code would look like without the macro:
let mut x = <Dsl>::default();
x.named("Root");
x.screen_root();
x.main_margin(100.);
x.align_start();
x.image(&bg);
x.row();
x.node(&mut cmds, |cmds| {
// Same goes with the children:
// ButtonText1(color(Color::BLUE) rules(px(40), pct(100)) button_named)
// ButtonText2(color(Color::RED) rules(px(40), pct(100)) button_named)
// Menu(width(px(310)) main_margin(10.) fill_main_axis image(&board) column) {
// TitleCard(rules(pct(100), px(100)))
// }
});
Code
One last statement type exists, it gives the user back full control over
the cmds
, even nested within a parent node.
It looks like this:
code(let <cmd_ident>) {
<rust code>
}
Concretely:
let menu_buttons = ["Hello", "This is a", "Menu"];
dsl!{ &mut cmds,
code(let my_cmds) {
my_cmds.with_children(|mut cmds| {
for n in &menu_buttons {
let name = format!("{n} button");
println!("{name}");
cmds.spawn(Name::new(name));
}
});
}
}
This is directly inserted as-is in the macro, so it would look as follow:
let my_cmd = &mut cmds;
my_cmd.with_children(|mut cmds| {
for n in &menu_buttons {
let name = format!("{n} button");
println!("{name}");
cmds.spawn(Name::new(name));
}
});
Nothing prevents you from using code
inside a parent node,
neither using the dsl!
macro within rust code within a code statement:
dsl!(&mut cmds,
Entity(height(pct(100)) fill_main_axis row) {
code(let my_cmds) {
my_cmds.with_children(|mut cmds| {
for name in &menu_buttons {
let mut entity = cmds.spawn_empty();
dsl!(&mut entity, Entity(button(name) color(Color::BLUE)))
}
});
}
}
)
DSL methods
Stuff within parenthesis in a DSL statement are DSL methods.
Methods are translated directly into rust method calls on Dsl
:
some_method // bare method
method_with_args ([<expr>],*) // arguments method
Which would be translated into rust code as follow:
x.some_method();
x.method_with_args(15 * 25. as u32);
x.method_with_args("hi folks", variable_name, Color::RED);