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:

  1. Define a Blink component.
  2. Define a system that reads the Blink component and update some color/sprite.
  3. 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:

  1. (optionally) between <$ty>, a DslBundle type. By default, it will use the identifier Dsl in scope. This will be referred as Dsl in the rest of this documentation.
  2. An expression of type &mut EntityCommands.
  3. 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);