cuicui_dsl/
macros.rs

1/// Reorganize rust method syntax to play wonderfully with bevy's
2/// hierarchy spawning mechanism.
3///
4/// Basically, this is a way to use `&mut self` methods on an arbitrary type
5/// but in a declarative way.
6///
7/// # Usage
8///
9/// The crate-level doc for this has a nice example, you can check it out:
10/// [`crate`].
11///
12/// ## Cheat sheet
13///
14/// You already know how to use `dsl!`? here are the quick links:
15///
16/// - [**dsl statements**](#dsl-statements):
17///   - [**Entity**](#entity)
18///   - [**leaf node**](#leaf-node)
19///   - [**parent node**](#parent-node)
20///   - [**code**](#code)
21/// - [**dsl methods**](#dsl-methods)
22///
23/// ## Extending `dsl!`
24///
25/// Since `dsl!` is straight up nothing more than sugar on top of rust's
26/// method call syntax, it's trivial to add your own methods/statements.
27///
28/// With bevy's `DerefMut` derive, it's even possible to build on top of
29/// existing implementations.
30///
31/// > **Warning**: Is it wise to abuse the `DerefMut` trait this way?
32/// >
33/// > I dunno, but it makes everything so much more convenient.
34/// > See <https://github.com/nicopap/cuicui_layout/issues/26>
35///
36/// Consider [`BaseDsl`], it only has a single method: `named`. But we want
37/// to create blinking UI. How do we do it?
38///
39/// Like in any bevy project, we would do as follow:
40///
41/// 1. Define a `Blink` component.
42/// 2. Define a system that reads the `Blink` component and update some color/sprite.
43/// 3. Optionally create a `BlinkBundle` that adds to an entity all things necessary
44///    for blinking to work.
45///
46/// ```
47/// # use cuicui_dsl::macros::__doc_helpers::*; use cuicui_dsl::dsl;
48/// #[derive(Component, Default)]
49/// struct Blink {
50///     frequency: f32,
51///     amplitude: f32,
52/// }
53/// #[derive(Bundle, Default)]
54/// struct BlinkBundle {
55///     blink: Blink,
56///     spatial: SpatialBundle,
57/// }
58/// ```
59///
60/// We want to have a DSL that let us set the `frequency` and `amplitude`
61/// of the `Blink` component.
62///
63/// More importantly though, we want our DSL to compose with any other DSL!
64/// For this, we will add an `inner` field and use the bevy `DerefMut` derive
65/// macro:
66///
67/// ```
68/// # use cuicui_dsl::macros::__doc_helpers::*; use cuicui_dsl::dsl;
69/// #[derive(Deref, DerefMut, Default)]
70/// struct BlinkDsl<D = ()> {
71///     #[deref]
72///     inner_dsl: D,
73///     pub blink: Blink,
74/// }
75/// impl<D: DslBundle> DslBundle for BlinkDsl<D> {
76///     fn insert(&mut self, cmds: &mut EntityCommands) {
77///         // We insert first `Blink`, as to avoid overwriting things
78///         // `inner_dsl.insert`  might insert itself.
79///         cmds.insert(BlinkBundle { blink: self.blink, ..default() });
80///         self.inner_dsl.insert(cmds);
81///     }
82/// }
83///
84/// // `dsl!` relies on method calls, so we need to define methods:
85/// impl<D> BlinkDsl<D> {
86///     pub fn frequency(&mut self, frequency: f32) {
87///         self.blink.frequency = frequency;
88///     }
89///     pub fn amplitude(&mut self, amplitude: f32) {
90///         self.blink.amplitude = amplitude;
91///     }
92/// }
93///
94/// # let mut w = WorldCheck::new(); let mut cmds = w.cmd(); let mut cmds = cmds.spawn_empty();
95/// type Dsl = BlinkDsl<BaseDsl>;
96/// dsl! {
97///     &mut cmds,
98///     Entity {
99///         FastBlinker(frequency(0.5))
100///         SlowBlinker(amplitude(2.) frequency(3.0))
101///     }
102/// }
103/// ```
104///
105/// If we want to use a pre-existing DSL with ours, we would nest them.
106/// Since we `#[deref] inner: D`, all methods on the inner DSL are available
107/// on the outer DSL.
108///
109/// ```
110/// # use cuicui_dsl::macros::__doc_helpers::*; use cuicui_dsl::dsl; type BlinkDsl<T> = DocDsl<T>;
111/// # let mut w = WorldCheck::new(); let mut cmds = w.cmd(); let mut cmds = cmds.spawn_empty();
112/// type Dsl = BlinkDsl<LayoutDsl>;
113/// dsl! {
114///     &mut cmds,
115///     Entity {
116///         Entity(ui("Fast blink") frequency(0.5) color(Color::GREEN))
117///         Entity(row frequency(1.) amplitude(1.0) main_margin(10.) fill_main_axis) {
118///             Entity(ui("Some text") amplitude(10.0) color(Color::BLUE))
119///         }
120///         Entity(ui("Slow blink") frequency(2.) color(Color::RED))
121///     }
122/// }
123/// ```
124///
125/// We made our DSL nestable so that it is itself composable. Say we are making
126/// a public crate, and our users want the UI DSL on top of ours. They would
127/// simply define their own DSL as follow:
128///
129/// ```ignore
130/// type UserDsl = UiDsl<BlinkDsl<LayoutDsl>>;
131/// ```
132///
133/// And it would work as is.
134///
135/// # Syntax
136///
137/// `dsl!` accepts as argument:
138///
139/// 1. (optionally) between `<$ty>`, a [`DslBundle`] type.
140///    By default, it will use the identifier `Dsl` in scope.
141///    This will be referred as **`Dsl`** in the rest of this documentation.
142/// 2. An expression of type `&mut EntityCommands`.
143/// 3. A single [**DSL statement**](#dsl-statements).
144///    * DSL statements contain themselves series of [**DSL methods**](#dsl-methods).
145///
146/// ## DSL statements
147///
148/// A DSL statement spawns a single entity.
149///
150/// There are three kinds of DSL statements:
151/// - Entity statements
152/// - leaf node statement
153/// - parent node statement
154/// - code statement
155///
156/// ### Entity
157///
158/// Entity statements create an `Entity` and calls [`DslBundle::insert`].
159/// They basically spawn an entity with the given [**DSL methods**](#dsl-methods).
160///
161/// Optionally, they can act like parent nodes if they are directly followed
162/// by curly braces:
163///
164/// ```text
165/// Entity([dsl methods]*)
166/// Entity([dsl methods]*) {
167///     [dsl statements]*
168/// }
169/// ```
170/// Concretely:
171/// ```
172/// # use cuicui_dsl::macros::__doc_helpers::*; use cuicui_dsl::dsl;
173/// # let mut w = WorldCheck::new(); let mut cmds = w.cmd(); let mut cmds = cmds.spawn_empty();
174/// dsl!{ &mut cmds,
175///     Entity(color(Color::BLUE) rules(px(40), pct(100)))
176/// };
177/// dsl!{ &mut cmds,
178///     Entity(fill_main_axis) {
179///         Entity(color(Color::GREEN))
180///     }
181/// };
182/// ```
183/// This will expand to the following code:
184/// ```
185/// # use cuicui_dsl::macros::__doc_helpers::*; use cuicui_dsl::dsl;
186/// # let mut w = WorldCheck::new(); let mut cmds = w.cmd(); let mut cmds = cmds.spawn_empty();
187/// let mut x = <Dsl>::default();
188/// x.color(Color::BLUE);
189/// x.rules(px(40), pct(100));
190/// x.insert(&mut cmds);
191///
192/// let mut x = <Dsl>::default();
193/// x.fill_main_axis();
194/// x.node(&mut cmds, |cmds| {
195///     let mut x = <Dsl>::default();
196///     x.color(Color::GREEN);
197///     x.insert(&mut cmds.spawn_empty());
198/// });
199/// ```
200///
201/// ### Leaf node
202///
203/// Leaf node statements are statements without subsequent braces.
204///
205/// The head identifier is used as the spawned entity's name. You may also use
206/// any [**rust literal**][literal] (including strings) instead of an identifier.
207///
208/// It looks as follow:
209///
210/// ```text
211/// <ident>([dsl methods]*)
212/// ```
213/// Concretely:
214/// ```
215/// # use cuicui_dsl::macros::__doc_helpers::*; use cuicui_dsl::dsl;
216/// # let mut w = WorldCheck::new(); let mut cmds = w.cmd(); let mut cmds = cmds.spawn_empty();
217/// # dsl!{ &mut cmds, Entity {
218/// ButtonText(color(Color::BLUE) width(px(40)) height(pct(100)) button_named)
219/// # } }
220/// ```
221/// This expands to:
222/// ```
223/// # use cuicui_dsl::macros::__doc_helpers::*; use cuicui_dsl::dsl;
224/// # let mut w = WorldCheck::new(); let mut cmds = w.cmd(); let mut cmds = cmds.spawn_empty();
225/// let mut x = <Dsl>::default();
226/// let mut c: &mut EntityCommands = &mut cmds;
227/// x.named("ButtonText");
228/// x.color(Color::BLUE);
229/// x.width(px(40));
230/// x.height(pct(100));
231/// x.button_named();
232/// x.insert(c);
233/// ```
234///
235/// ### Parent node
236///
237/// The parent node statement has the following syntax:
238/// ```text
239/// <ident>([dsl method]*) {
240///     [dsl statement]*
241/// }
242/// ```
243/// Concretely, it looks like the following:
244/// ```
245/// # use cuicui_dsl::macros::__doc_helpers::*; use cuicui_dsl::dsl;
246/// # let mut w = WorldCheck::new(); let mut cmds = w.cmd(); let (bg, board) = ((),()); let mut cmds = cmds.spawn_empty();
247/// # dsl!{ &mut cmds,
248/// Root(screen_root main_margin(100.) align_start image(&bg) row) {
249///     ButtonText1(color(Color::BLUE) rules(px(40), pct(100)) button_named)
250///     ButtonText2(color(Color::RED) rules(px(40), pct(100)) button_named)
251///     Menu(width(px(310)) main_margin(10.) fill_main_axis image(&board) column) {
252///         TitleCard(rules(pct(100), px(100)))
253///     }
254/// }
255/// # }
256/// ```
257///
258/// The part between parenthesis (`()`) is a list of [DSL methods](#dsl-methods).
259/// They are applied to the `Dsl` [`DslBundle`] each one after the other.
260/// Then, an entity is spawned with the so-constructed bundle,
261/// following, the DSL statements within braces (`{}`) are spawned
262/// as children of the parent node entity.
263///
264/// For the visually-minded, this is how the previous code would look like without
265/// the macro:
266///
267/// ```
268/// # use cuicui_dsl::macros::__doc_helpers::*; use cuicui_dsl::dsl;
269/// # let mut w = WorldCheck::new(); let mut cmds = w.cmd(); let bg = (); let mut cmds = cmds.spawn_empty();
270/// let mut x = <Dsl>::default();
271/// x.named("Root");
272/// x.screen_root();
273/// x.main_margin(100.);
274/// x.align_start();
275/// x.image(&bg);
276/// x.row();
277/// x.node(&mut cmds, |cmds| {
278///     // Same goes with the children:
279///     // ButtonText1(color(Color::BLUE) rules(px(40), pct(100)) button_named)
280///     // ButtonText2(color(Color::RED) rules(px(40), pct(100)) button_named)
281///     // Menu(width(px(310)) main_margin(10.) fill_main_axis image(&board) column) {
282///     //     TitleCard(rules(pct(100), px(100)))
283///     // }
284/// });
285/// ```
286///
287/// ### Code
288///
289/// One last statement type exists, it gives the user back full control over
290/// the `cmds`, even nested within a parent node.
291/// It looks like this:
292///
293/// ```text
294/// code(let <cmd_ident>) {
295///     <rust code>
296/// }
297/// ```
298/// Concretely:
299/// ```
300/// # use cuicui_dsl::macros::__doc_helpers::*; use cuicui_dsl::dsl;
301/// # let mut w = WorldCheck::new(); let mut cmds = w.cmd(); let mut cmds = cmds.spawn_empty();
302/// let menu_buttons = ["Hello", "This is a", "Menu"];
303///
304/// dsl!{ &mut cmds,
305///    code(let my_cmds) {
306///        my_cmds.with_children(|mut cmds| {
307///            for n in &menu_buttons {
308///                let name = format!("{n} button");
309///                println!("{name}");
310///                cmds.spawn(Name::new(name));
311///            }
312///        });
313///    }
314/// }
315/// ```
316/// This is directly inserted as-is in the macro, so it would look as follow:
317/// ```
318/// # use cuicui_dsl::macros::__doc_helpers::*; use cuicui_dsl::dsl;
319/// # let mut w = WorldCheck::new(); let mut cmds = w.cmd(); let mut cmds = cmds.spawn_empty();
320/// # let menu_buttons = ["Hello", "This is a", "Menu"];
321/// let my_cmd = &mut cmds;
322/// my_cmd.with_children(|mut cmds| {
323///     for n in &menu_buttons {
324///         let name = format!("{n} button");
325///         println!("{name}");
326///         cmds.spawn(Name::new(name));
327///     }
328/// });
329/// ```
330/// Nothing prevents you from using `code` inside a parent node,
331/// neither using the `dsl!` macro within rust code within a code statement:
332/// ```
333/// # use cuicui_dsl::macros::__doc_helpers::*; use cuicui_dsl::dsl;
334/// # let mut w = WorldCheck::new(); let mut cmds = w.cmd(); let mut cmds = cmds.spawn_empty();
335/// # let menu_buttons = ["Hello", "This is a", "Menu"];
336/// dsl!(&mut cmds,
337///     Entity(height(pct(100)) fill_main_axis row) {
338///         code(let my_cmds) {
339///             my_cmds.with_children(|mut cmds| {
340///                 for name in &menu_buttons {
341///                     let mut entity = cmds.spawn_empty();
342///                     dsl!(&mut entity, Entity(button(name) color(Color::BLUE)))
343///                 }
344///             });
345///         }
346///     }
347/// )
348/// ```
349///
350/// ## DSL methods
351///
352/// Stuff within parenthesis in a DSL statement are **DSL methods**.
353/// Methods are translated directly into rust method calls on `Dsl`:
354///
355/// ```text
356/// some_method                   // bare method
357/// method_with_args ([<expr>],*) // arguments method
358/// ```
359/// Which would be translated into rust code as follow:
360/// ```ignore
361/// x.some_method();
362/// x.method_with_args(15 * 25. as u32);
363/// x.method_with_args("hi folks", variable_name, Color::RED);
364/// ```
365///
366/// [literal]: https://doc.rust-lang.org/reference/expressions/literal-expr.html
367/// [`DslBundle`]: crate::DslBundle
368/// [`DslBundle::insert`]: crate::DslBundle::insert
369/// [`BaseDsl`]: crate::BaseDsl
370/// [`IntoEntityCommands`]: crate::IntoEntityCommands
371#[rustfmt::skip]
372#[macro_export]
373macro_rules! dsl {
374    (@arg [$x:tt] ) => {};
375    (@arg [$x:tt] $m:ident ($($arg:tt)*) $($t:tt)*)=>{$x.$m($($arg)*) ; dsl!(@arg [$x] $($t)*)};
376    (@arg [$x:tt] $m:ident               $($t:tt)*)=>{$x.$m()         ; dsl!(@arg [$x] $($t)*)};
377
378    (@statement [$d_ty:ty, $cmds:expr] ) => { };
379    (@statement [$d_ty:ty, $cmds:expr] code (let $cmds_ident:ident) {$($code:tt)*} $($($t:tt)+)?) => {
380        let mut $cmds_ident: &mut EntityCommands = $cmds;
381        $($code)*
382        // Generate the rest of the code
383        $(; dsl!(@statement [$d_ty, $cmds] $($t)*))?
384    };
385    (@statement [$d_ty:ty, $cmds:expr] Entity ($($args:tt)*) {} $($t:tt)*) => {
386        let mut x = <$d_ty>::default();
387        dsl!(@arg [x] $($args)*);
388        x.insert($cmds);
389        // Generate the rest of the code
390        dsl!(@statement [$d_ty, $cmds] $($t)*)
391    };
392    (@statement [$d_ty:ty, $cmds:expr] Entity ($($args:tt)*) {$($inner:tt)*} $($t:tt)*) => {
393        let mut x = <$d_ty>::default();
394        dsl!(@arg [x] $($args)*);
395        x.node($cmds, |mut child_builder| {
396            // Generate code for statements inside curly braces
397            dsl!(@statement [$d_ty, &mut child_builder.spawn_empty()] $($inner)*);
398        });
399        // Generate the rest of the code
400        dsl!(@statement [$d_ty, $cmds] $($t)*)
401    };
402    (@statement [$d_ty:ty, $cmds:expr] spawn ($($args:tt)*) $($t:tt)*) => { // spawn: requires trailing ()
403        dsl!(@statement [$d_ty, $cmds] Entity ($($args)*) $($t)*)
404    };
405    (@statement [$d_ty:ty, $cmds:expr] Entity ($($args:tt)*) $($t:tt)*) => { // no {}
406        dsl!(@statement [$d_ty, $cmds] Entity ($($args)*) {} $($t)*)
407    };
408    (@statement [$d_ty:ty, $cmds:expr] Entity $($t:tt)*) => { // no ()
409        dsl!(@statement [$d_ty, $cmds] Entity () $($t)*)
410    };
411    (@statement [$d_ty:ty, $cmds:expr] $entity_name:literal ($($args:tt)*) $($t:tt)*) => {
412        dsl!(@statement [$d_ty, $cmds] Entity (named($entity_name.to_string()) $($args)*) $($t)*)
413    };
414    (@statement [$d_ty:ty, $cmds:expr] $entity_name:literal $($t:tt)*) => {
415        dsl!(@statement [$d_ty, $cmds] Entity (named($entity_name.to_string())) $($t)*)
416    };
417    (@statement [$d_ty:ty, $cmds:expr] $entity_name:ident ($($args:tt)*) $($t:tt)*) => {
418        dsl!(@statement [$d_ty, $cmds] Entity (named(stringify!($entity_name)) $($args)*) $($t)*)
419    };
420    (@statement [$d_ty:ty, $cmds:expr] $entity_name:ident $($t:tt)*) => {
421        dsl!(@statement [$d_ty, $cmds] Entity (named(stringify!($entity_name))) $($t)*)
422    };
423    (<$builder:ty> $cmds:expr, $($t:tt)*) => {{
424        use $crate::{DslBundle, EntityCommands};
425        fn is_dsl_bundle<D: DslBundle>() {} is_dsl_bundle::<$builder>();
426        let cmds: &mut EntityCommands = $cmds;
427        // Generate code for all statements
428        dsl!(@statement [$builder, cmds] $($t)*);
429    }};
430    // Just call the match above with <Dsl>
431    ($cmds:expr, $($t:tt)*) => { dsl!(<Dsl> $cmds, $($t)*) };
432}
433
434#[allow(clippy::all, clippy::pedantic, clippy::nursery)]
435#[doc(hidden)]
436#[cfg(feature = "test_and_doc")]
437pub mod __doc_helpers {
438    use std::fmt;
439    use std::num::ParseIntError;
440    use std::str::FromStr;
441
442    pub use crate::{BaseDsl, BuildChildren, ChildBuilder, DslBundle};
443    pub use bevy::ecs::system::EntityCommands;
444    pub use bevy::prelude::{
445        default, AssetServer, Bundle, Commands, Component, Deref, DerefMut, Handle, Image, Name,
446        Res, Transform,
447    };
448    use bevy::{ecs::system::CommandQueue, prelude::World};
449
450    #[derive(Component, Default, Clone)]
451    pub struct Style {
452        pub height: Val,
453        pub flex_direction: FlexDirection,
454    }
455    #[derive(Default, Clone)]
456    pub enum FlexDirection {
457        #[default]
458        Column,
459    }
460    #[derive(Component, Default)]
461    pub struct BackgroundColor(pub Color);
462
463    #[derive(Debug, Clone, Copy, PartialEq, Default)]
464    pub struct Color;
465    impl Color {
466        pub const WHITE: Self = Self;
467        pub const RED: Self = Self;
468        pub const GREEN: Self = Self;
469        pub const BLUE: Self = Self;
470    }
471    impl std::error::Error for Color {}
472    impl fmt::Display for Color {
473        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474            write!(f, "Color")
475        }
476    }
477    impl FromStr for Color {
478        type Err = Color;
479
480        fn from_str(s: &str) -> Result<Self, Self::Err> {
481            match s {
482                "red" => Ok(Color),
483                "green" => Ok(Color),
484                "blue" => Ok(Color),
485                _ => Err(Color),
486            }
487        }
488    }
489
490    #[derive(Bundle, Default)]
491    pub struct SpatialBundle {
492        f: (),
493    }
494
495    #[derive(Bundle, Default)]
496    pub struct BlinkBundle {
497        pub blink: Blink,
498        pub bundle: SpatialBundle,
499    }
500
501    #[derive(Deref, DerefMut, Default)]
502    pub struct DocDsl<D = BaseDsl> {
503        #[deref]
504        pub inner: D,
505        pub blink: Blink,
506    }
507    impl<D> DocDsl<D> {
508        pub fn column(&mut self) {}
509        pub fn main_margin(&mut self, _: f32) {}
510        pub fn align_start(&mut self) {}
511        pub fn image(&mut self, _: &impl Into<ImageMock>) {}
512        pub fn row(&mut self) {}
513        pub fn width(&mut self, _: Val) {}
514        pub fn height(&mut self, _: Val) {}
515        pub fn rules(&mut self, _: Val, _: Val) {}
516        pub fn button(&mut self, _: &str) {}
517        pub fn button_named(&mut self) {}
518        pub fn screen_root(&mut self) {}
519        pub fn fill_main_axis(&mut self) {}
520        pub fn color(&mut self, _color: Color) {}
521        pub fn ui(&mut self, _: &str) {}
522
523        pub fn amplitude(&mut self, _: f32) {}
524        pub fn frequency(&mut self, _: f32) {}
525
526        pub fn distrib_start(&mut self) {}
527    }
528    impl<D: DslBundle> DslBundle for DocDsl<D> {
529        fn insert(&mut self, cmds: &mut EntityCommands) {
530            self.inner.insert(cmds);
531        }
532    }
533    pub type Dsl = DocDsl;
534    pub type LayoutDsl = DocDsl;
535    pub type BlinkDsl = DocDsl;
536
537    #[derive(Default, Clone, Copy)]
538    pub struct Val;
539    impl FromStr for Val {
540        type Err = ParseIntError;
541        fn from_str(s: &str) -> Result<Self, Self::Err> {
542            match () {
543                () if s.starts_with("px(") => {
544                    let number = &s[3..s.len() - 1];
545                    let _ = number.parse::<i32>()?;
546                    Ok(Val)
547                }
548                () if s.starts_with("pct(") => {
549                    let number = &s[4..s.len() - 1];
550                    let _ = number.parse::<i32>()?;
551                    Ok(Val)
552                }
553                () => Err("badnumber".parse::<i32>().unwrap_err()),
554            }
555        }
556    }
557    pub fn px(_: i32) -> Val {
558        Val
559    }
560    pub fn pct(_: i32) -> Val {
561        Val
562    }
563
564    #[derive(Default, Component, Clone, Copy)]
565    pub struct Blink {
566        pub frequency: f32,
567        pub amplitude: f32,
568    }
569
570    pub struct WorldCheck(World, CommandQueue);
571    impl WorldCheck {
572        pub fn new() -> Self {
573            WorldCheck(World::new(), CommandQueue::default())
574        }
575        pub fn cmd<'a>(&'a mut self) -> Commands<'a, 'a> {
576            Commands::new(&mut self.1, &self.0)
577        }
578        pub fn check(&self) {
579            todo!(
580                "This would be called with some sort of hierarchy, and \
581                we would compare it to what's in the World, but not \
582                implemented yet"
583            )
584        }
585    }
586
587    pub struct ImageMock;
588    impl From<()> for ImageMock {
589        fn from(_: ()) -> Self {
590            ImageMock
591        }
592    }
593    impl From<Handle<Image>> for ImageMock {
594        fn from(_: Handle<Image>) -> Self {
595            ImageMock
596        }
597    }
598}