Generic markup macro?

Not yet there working in my graphics API, but I'm wondering if there's a way to do something like the following. More context: I'd have a graphics API based on nodes of a limited set of variants (Button, Canvas, TabBar, Svg, Bitmap and lots more) and an UI API for defining reactive components that can reuse the graphics API.

impl Component for PS3Button {
    fn render(self: &Arc<Self>, context: &Arc<ComponentContext>) -> Node {
        markup! {
            <Button>
                <Svg src="app://res/img/foo.svg"/>
                { Svg::from_file("app://res/img/bar.svg") }
                <Row>
                    <PS3OtherComponent/>
                    <PS3OtherComponent/>
                </Row>
            </Button>
        }
    }
}
  • All nodes have children. A node kind, after constructed via ::new, returns a Node. The callback given to ::new receives the node kind itself, allowing to chain methods without calling ::to<K>().
  • Button, Svg and Row are nodes, not reactive UI components.
  • <Svg src={...}/> translates to Svg::new(|svg| svg.set_src(...)) (see the set_ prefix)
  • Interpolation should work too ({}, accepting iterable via {..} maybe)
  • <Row>{...}</Row> translates to Row::new().append_children(...)
  • PS3OtherComponent is another reactive UI component.

I've thought of using chainable methods when constructing nodes, allowing for clarity, but it looks interesting to be able to use a markup macro for reactive components.

That's how it looks without markup!:

impl Component for PS3Button {
    fn render(self: &Arc<Self>, context: &Arc<ComponentContext>) -> Node {
        Button::new(|b| b).append_children([
            Svg::from_file("app://res/img/foo.svg"),
            Svg::from_file("app://res/img/bar.svg"),
            Row::new(|row| row).append_children([
                PS3OtherComponent::new().render(context),
            ]),
        ])
    }
}

The macro syntax seems like GitHub - chinedufn/percy: Build frontend browser apps with Rust + WebAssembly. Supports server side rendering. or specifically https://crates.io/crates/html-macro (I have never used it, just sharing what I saw before)

        let end_view = html! {
           // Use regular Rust comments within your html
           <div class=["big", "blue"]>
              /* Interpolate values using braces */
              <strong>{ greetings }</strong>

              <button
                class="giant-button"
                onclick=|_event| {
                   web_sys::console::log_1(&"Button Clicked!".into());
                }
              >
                // No need to wrap text in quotation marks (:
                Click me and check your console
              </button>
           </div>
        };
2 Likes

Hm, if I use a proc_macro, how could I detect individual types from their identifier (lexically and not by string)?

Types don't exist at the time macros are expanded; those come later.

1 Like

To be a little more specific: you can parse the tokens and the parse tree will identify sequences of tokens which will eventually identify types, but there's not a reliable way to identify specific types. e.g. Arc might refer to ::std::sync::Arc or it might refer to ::some_geometry_crate::Arc.

2 Likes

What exactly do you mean by that? These two sound like exactly the same.

Examples:

  • The developer may alias Button as something else
  • The developer may have a type with the same name as Button and doesn't want to create rialight::graphics::Node inside a markup, but rather their own UI component. (Using proc_macro, the Button identifier can only expand to use ::rialight::graphics::Button::new().)

Proc macros run before name resolution, so at that point there's no way to know whether an identifier refers to ::rialight::graphics::Button or something else. You're better off requiring every type used in the macro to be in scope and not use absolute paths for them. For example if the use writes Button you generate just Button::new(), it will be the user's responsibility to use rialight::Button. You could also offer a prelude module so that users can just do use rialight::prelude::* and most things they will need will be automatically imported (including Button).

3 Likes

The problem is that:

  • Node does have several methods available to all nodes, but very specific ones are only available in the kind types themselves
  • Button::new() returns Node
  • Button has a set_warning method, but it's not available in Node

I think figured out a good way to achieve this with procedural macros as you guys said, but it wasn't clear to me.

First, I wanted to use composition (i.e. Node holding any node kind inside it among base fields) and not a trait (i.e. Arc<dyn Node>) because of the dynamic dispatch for accessing the base fields, to which other operations rely (children, node paths, parent, inheritable properties such as skin, scale and visibility). Maybe this dynamic dispatch isn't a problem after all, even if there are hundreds of Node implementors, but using trait looks ugly too (imagine typing Arc<dyn Node> rather than Node or Arc<Button> instead...)

What I decided is, the node kind (K), won't be the actual data stored inside Node; rather, it'll have a separate internal data type (KKindData) that is hidden. K::new() returns K. K will contain both Node and KKindData, therefore... everything changed and I'll be able to even do:

  • let button: Button = markup!(<Button/>);
  • let node: Node = button.into();

I didn't put it into pratice yet though. I want to work at more fundamental things first.

dyn doesn't bother me at all (its absense was worse), but for those who are bothered, type aliases exist.

2 Likes

Given an Iterator<TokenTree>, I wonder how I'll reach the comma at the end of each field, because:

  • While types may use angle brackets enclosed sequences, angle brackets may also be used for lt and gt operators.
define_node! {
    pub type Example {
        foo: RwLock<i64> = RwLock::new(0),

        // `explicit_setter` tells `define_node!`
        // to not generate a `set_`.
        #[explicit_setter]
        bar: RwLock<i64> = RwLock::new(0),
    }
}

The field assignment here could use any expression, so just visiting each parentheses and brackets to ignore the expression won't allow using the < or > operators properly, I suppose?

Have you considered using syn to parse your code? When you reach the RwLock::new(0), #[explicit_setter] etc etc you should be able to parse a syn::Expr and be left with the , #[explicit_setter] etc etc.

2 Likes

This might be useful, gonna give it a look when needed!