Skip to main content

ferroui_view_macros/
lib.rs

1//! ferroui-view-macros
2//!
3//! The macros of `ferroui-view`: [`rsx!`] writes an element tree in a form
4//! close to XAML and expands to the builders, and [`#[component]`](component)
5//! turns a function into a component with a builder of its parameters.
6//! Errors point at the user's code: an attribute that a class has no setter
7//! for is a missing method of its builder.
8
9use proc_macro::TokenStream;
10use proc_macro2::{Span, TokenStream as TokenStream2};
11use quote::{format_ident, quote, quote_spanned, ToTokens};
12use syn::ext::IdentExt;
13use syn::parse::{Parse, ParseStream};
14use syn::spanned::Spanned;
15use syn::{braced, token, Expr, FnArg, Ident, ItemFn, LitStr, Pat, Path, Token, Type};
16
17/// An element tree, expanded to the builders of `ferroui-view`.
18///
19/// ```ignore
20/// rsx! {
21///     StackPanel { spacing: 8.0,
22///         TextBlock { text: move || format!("Count: {}", count.get()) }
23///         Button { on_click: move |_| count.update(|c| *c += 1), "Increment" }
24///     }
25/// }
26/// ```
27///
28/// - `Name { ... }` is an element of the control class or component `Name`
29///   in scope (`<Name as Builder>::builder()`): `StackPanel` is
30///   `stack_panel()`, `todo::Row` the component `Row` of the module `todo`.
31/// - `name` alone gives the setter `name` the variable `name`, as a field of
32///   a struct is given (`TextBlock { text }`).
33/// - `name: value` calls the setter `.name(value)`: a property
34///   (`spacing: 8.0`), a handler (`on_click: ...`), `key`, `name`, `class`.
35/// - `Owner.name: value` sets an attached property: `Grid.row: 1` calls
36///   `.grid_row(1)`.
37/// - `name(a, b)` calls a method of the builder with several arguments:
38///   `on_changed(TextBox::text_property(), handler)`,
39///   `set(Grid::row_property(), 1)` (the indexer `control[Property] = 1`
40///   upstream), `bind(TextBlock::text_property(), binding)`.
41/// - `"text"` is a text child, `{expr}` any child (an element, a builder, a
42///   list, `for_each(..)`, `show(..)`), and a nested element a child.
43/// - `if cond { .. } else { .. }` and `for x in items { .. }` choose and
44///   repeat children when the tree is built (for children that follow state,
45///   use `{for_each(..)}`, `{show(..)}` or `{dynamic(..)}`).
46///
47/// Several nodes at the root make a fragment.
48#[proc_macro]
49pub fn rsx(input: TokenStream) -> TokenStream {
50    let nodes = syn::parse_macro_input!(input as Nodes);
51    let elements: Vec<TokenStream2> = nodes.0.iter().map(Node::to_element).collect();
52    let tokens = if elements.len() == 1 {
53        elements.into_iter().next().unwrap_or_default()
54    } else {
55        quote! { ::ferroui_view::Element::Fragment(::std::vec![#(#elements),*]) }
56    };
57    tokens.into()
58}
59
60struct Nodes(Vec<Node>);
61
62impl Parse for Nodes {
63    fn parse(input: ParseStream) -> syn::Result<Self> {
64        let mut nodes = Vec::new();
65        while !input.is_empty() {
66            nodes.push(input.parse()?);
67            if input.peek(Token![,]) {
68                input.parse::<Token![,]>()?;
69            }
70        }
71        Ok(Nodes(nodes))
72    }
73}
74
75enum Node {
76    Element(ElementNode),
77    Text(LitStr),
78    Expr(Expr, Span),
79    If(Box<Expr>, Vec<Node>, Option<Vec<Node>>, Span),
80    For(Box<Pat>, Box<Expr>, Vec<Node>, Span),
81}
82
83struct ElementNode {
84    path: Path,
85    attributes: Vec<Attribute>,
86    children: Vec<Node>,
87}
88
89enum Attribute {
90    /// `name: value` or `Owner.name: value`.
91    Value {
92        /// `Owner` of `Owner.name`.
93        owner: Option<Ident>,
94        name: Ident,
95        value: Expr,
96    },
97    /// `name(arguments)`: a method of the builder with several arguments.
98    Call { name: Ident, arguments: syn::punctuated::Punctuated<Expr, Token![,]> },
99}
100
101fn parse_children(input: ParseStream) -> syn::Result<Vec<Node>> {
102    let content;
103    braced!(content in input);
104    Ok(content.parse::<Nodes>()?.0)
105}
106
107impl Parse for Node {
108    fn parse(input: ParseStream) -> syn::Result<Self> {
109        if input.peek(LitStr) {
110            return Ok(Node::Text(input.parse()?));
111        }
112        if input.peek(token::Brace) {
113            let span = input.span();
114            let content;
115            braced!(content in input);
116            return Ok(Node::Expr(content.parse()?, span));
117        }
118        if input.peek(Token![if]) {
119            let span = input.span();
120            input.parse::<Token![if]>()?;
121            let condition = Expr::parse_without_eager_brace(input)?;
122            let then = parse_children(input)?;
123            let otherwise = if input.peek(Token![else]) {
124                input.parse::<Token![else]>()?;
125                if input.peek(Token![if]) {
126                    Some(vec![input.parse::<Node>()?])
127                } else {
128                    Some(parse_children(input)?)
129                }
130            } else {
131                None
132            };
133            return Ok(Node::If(Box::new(condition), then, otherwise, span));
134        }
135        if input.peek(Token![for]) {
136            let span = input.span();
137            input.parse::<Token![for]>()?;
138            let pattern = Pat::parse_single(input)?;
139            input.parse::<Token![in]>()?;
140            let items = Expr::parse_without_eager_brace(input)?;
141            let body = parse_children(input)?;
142            return Ok(Node::For(Box::new(pattern), Box::new(items), body, span));
143        }
144        let path: Path = input.call(Path::parse_mod_style)?;
145        let content;
146        braced!(content in input);
147        let mut attributes = Vec::new();
148        let mut children = Vec::new();
149        while !content.is_empty() {
150            if content.peek(Ident::peek_any) && content.peek2(token::Paren) {
151                let name = content.call(Ident::parse_any)?;
152                let arguments;
153                syn::parenthesized!(arguments in content);
154                let arguments = arguments.parse_terminated(Expr::parse, Token![,])?;
155                attributes.push(Attribute::Call { name, arguments });
156            } else if content.peek(Ident::peek_any) && (content.peek2(Token![,]) || is_last_token(&content)) {
157                // `name` alone: the setter of the name with the variable of
158                // the name, as a field of a struct is given.
159                let name = content.call(Ident::parse_any)?;
160                let value: Expr = syn::parse_quote!(#name);
161                attributes.push(Attribute::Value { owner: None, name, value });
162            } else if is_attribute(&content) {
163                let owner = if content.peek2(Token![.]) {
164                    let owner = content.call(Ident::parse_any)?;
165                    content.parse::<Token![.]>()?;
166                    Some(owner)
167                } else {
168                    None
169                };
170                let name = content.call(Ident::parse_any)?;
171                content.parse::<Token![:]>()?;
172                let value: Expr = content.parse()?;
173                attributes.push(Attribute::Value { owner, name, value });
174            } else {
175                children.push(content.parse()?);
176            }
177            if content.peek(Token![,]) {
178                content.parse::<Token![,]>()?;
179            }
180        }
181        Ok(Node::Element(ElementNode { path, attributes, children }))
182    }
183}
184
185/// Whether the next token is the last one of the stream.
186fn is_last_token(input: ParseStream) -> bool {
187    let fork = input.fork();
188    fork.parse::<proc_macro2::TokenTree>().is_ok() && fork.is_empty()
189}
190
191/// `name: ...` or `Owner.name: ...`, as opposed to a child element
192/// (`Name { ... }`, `module::Name { ... }`).
193fn is_attribute(input: ParseStream) -> bool {
194    let fork = input.fork();
195    if fork.call(Ident::parse_any).is_err() {
196        return false;
197    }
198    if fork.peek(Token![.]) {
199        let _ = fork.parse::<Token![.]>();
200        if fork.call(Ident::parse_any).is_err() {
201            return false;
202        }
203    }
204    fork.peek(Token![:]) && !fork.peek(Token![::])
205}
206
207/// `StackPanel` as `stack_panel`.
208fn snake_case(name: &str) -> String {
209    let mut out = String::new();
210    let chars: Vec<char> = name.chars().collect();
211    for (index, &char) in chars.iter().enumerate() {
212        if char.is_uppercase() {
213            let previous_lower = index > 0 && (chars[index - 1].is_lowercase() || chars[index - 1].is_ascii_digit());
214            let next_lower = chars.get(index + 1).is_some_and(|next| next.is_lowercase());
215            let previous_upper = index > 0 && chars[index - 1].is_uppercase();
216            if index > 0 && (previous_lower || (previous_upper && next_lower)) {
217                out.push('_');
218            }
219            out.extend(char.to_lowercase());
220        } else {
221            out.push(char);
222        }
223    }
224    out
225}
226
227impl Node {
228    /// The node as a child expression (what `.child(..)` takes).
229    fn to_child(&self) -> TokenStream2 {
230        match self {
231            Node::Element(element) => element.to_builder(),
232            Node::Text(text) => text.to_token_stream(),
233            Node::Expr(expr, span) => quote_spanned! {*span=> #expr },
234            Node::If(..) | Node::For(..) => self.to_element(),
235        }
236    }
237
238    /// The node as an `Element`.
239    fn to_element(&self) -> TokenStream2 {
240        match self {
241            Node::Element(element) => {
242                let builder = element.to_builder();
243                quote! { ::ferroui_view::IntoElement::into_element(#builder) }
244            }
245            Node::Text(text) => quote! { ::ferroui_view::IntoChild::into_child(#text) },
246            Node::Expr(expr, span) => quote_spanned! {*span=> ::ferroui_view::IntoChild::into_child(#expr) },
247            Node::If(condition, then, otherwise, span) => {
248                let then = fragment(then);
249                let otherwise = otherwise.as_ref().map_or_else(|| quote! { ::ferroui_view::Element::Empty }, |nodes| fragment(nodes));
250                quote_spanned! {*span=> if #condition { #then } else { #otherwise } }
251            }
252            Node::For(pattern, items, body, span) => {
253                let body = fragment(body);
254                quote_spanned! {*span=>
255                    ::ferroui_view::Element::Fragment(
256                        ::std::iter::IntoIterator::into_iter(#items).map(|#pattern| #body).collect::<::std::vec::Vec<_>>()
257                    )
258                }
259            }
260        }
261    }
262}
263
264fn fragment(nodes: &[Node]) -> TokenStream2 {
265    let elements: Vec<TokenStream2> = nodes.iter().map(Node::to_element).collect();
266    match elements.len() {
267        0 => quote! { ::ferroui_view::Element::Empty },
268        1 => elements.into_iter().next().unwrap_or_default(),
269        _ => quote! { ::ferroui_view::Element::Fragment(::std::vec![#(#elements),*]) },
270    }
271}
272
273impl ElementNode {
274    fn to_builder(&self) -> TokenStream2 {
275        let path = &self.path;
276        let span = self.path.span();
277        let mut tokens = quote_spanned! {span=> <#path as ::ferroui_view::Builder>::builder() };
278        for attribute in &self.attributes {
279            match attribute {
280                Attribute::Value { owner, name, value } => {
281                    let method = match owner {
282                        Some(owner) => {
283                            format_ident!("{}_{}", snake_case(&owner.unraw().to_string()), name.unraw(), span = name.span())
284                        }
285                        None => name.clone(),
286                    };
287                    tokens = quote_spanned! {method.span()=> #tokens.#method(#value) };
288                }
289                Attribute::Call { name, arguments } => {
290                    tokens = quote_spanned! {name.span()=> #tokens.#name(#arguments) };
291                }
292            }
293        }
294        for child in &self.children {
295            let child = child.to_child();
296            tokens = quote! { #tokens.child(#child) };
297        }
298        tokens
299    }
300}
301
302/// A component: a function whose parameters are its properties, with a
303/// builder so that `rsx!` and the builders use it like a control.
304///
305/// ```ignore
306/// #[component]
307/// fn Counter(initial: i32, #[prop(default = 1)] step: i32) -> Element {
308///     let count = signal(initial);
309///     rsx! { Button { on_click: move |_| count.update(|c| *c += step), {move || count.get()} } }
310/// }
311///
312/// rsx! { Counter { initial: 5 } }        // or counter().initial(5)
313/// ```
314///
315/// The name is in upper camel case. Besides the function, the macro declares
316/// a type of the name (what `rsx!` names: `Counter { .. }`) and a builder
317/// function of the name in snake case (`counter()`), with a setter per parameter that takes what
318/// converts into its type (`IntoValue`: the conversions of the setters of
319/// controls). A parameter is required unless it has
320/// `#[prop(default)]` (the default of its type), `#[prop(default = expr)]`,
321/// or an `Option` type. A parameter named `children` of type `Element` takes
322/// the children of the element (`.child(..)`). The function runs once, when
323/// the element is built.
324#[proc_macro_attribute]
325pub fn component(attribute: TokenStream, item: TokenStream) -> TokenStream {
326    if !attribute.is_empty() {
327        return syn::Error::new(Span::call_site(), "#[component] takes no arguments").to_compile_error().into();
328    }
329    let function = syn::parse_macro_input!(item as ItemFn);
330    match expand_component(function) {
331        Ok(tokens) => tokens.into(),
332        Err(error) => error.to_compile_error().into(),
333    }
334}
335
336struct Param {
337    name: Ident,
338    ty: Type,
339    default: Option<TokenStream2>,
340    attrs: Vec<syn::Attribute>,
341}
342
343fn is_option(ty: &Type) -> bool {
344    matches!(ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "Option"))
345}
346
347fn expand_component(mut function: ItemFn) -> syn::Result<TokenStream2> {
348    let name = function.sig.ident.clone();
349    let name_text = name.to_string();
350    if !name_text.chars().next().is_some_and(char::is_uppercase) {
351        return Err(syn::Error::new(name.span(), "a component is named in upper camel case (`TodoList`)"));
352    }
353    let builder_function = Ident::new(&snake_case(&name_text), name.span());
354    let builder_type = format_ident!("{}Builder", name);
355    let visibility = function.vis.clone();
356
357    let mut params = Vec::new();
358    for input in function.sig.inputs.iter_mut() {
359        let FnArg::Typed(typed) = input else {
360            return Err(syn::Error::new(input.span(), "a component is a free function"));
361        };
362        let Pat::Ident(pattern) = &*typed.pat else {
363            return Err(syn::Error::new(typed.pat.span(), "a parameter of a component is a name"));
364        };
365        let mut default = None;
366        let mut kept = Vec::new();
367        for attr in typed.attrs.drain(..) {
368            if attr.path().is_ident("prop") {
369                attr.parse_nested_meta(|meta| {
370                    if meta.path.is_ident("default") {
371                        if meta.input.peek(Token![=]) {
372                            let value: Expr = meta.value()?.parse()?;
373                            default = Some(value.to_token_stream());
374                        } else {
375                            default = Some(quote! { ::std::default::Default::default() });
376                        }
377                        Ok(())
378                    } else {
379                        Err(meta.error("expected `default` or `default = ...`"))
380                    }
381                })?;
382            } else {
383                kept.push(attr);
384            }
385        }
386        typed.attrs = kept.clone();
387        let ty = (*typed.ty).clone();
388        if default.is_none() && is_option(&ty) {
389            default = Some(quote! { ::std::option::Option::None });
390        }
391        params.push(Param { name: pattern.ident.clone(), ty, default, attrs: kept });
392    }
393
394    let fields = params.iter().map(|param| {
395        let (name, ty) = (&param.name, &param.ty);
396        quote! { #name: ::std::option::Option<#ty> }
397    });
398    let setters = params.iter().map(|param| {
399        let (name, ty) = (&param.name, &param.ty);
400        let docs = param.attrs.iter().filter(|attr| attr.path().is_ident("doc"));
401        if name == "children" {
402            quote! {
403                #(#docs)*
404                pub fn children(mut self, children: impl ::ferroui_view::IntoChildren) -> Self {
405                    let mut items = match self.children.take() {
406                        ::std::option::Option::Some(::ferroui_view::Element::Fragment(items)) => items,
407                        _ => ::std::vec::Vec::new(),
408                    };
409                    items.extend(::ferroui_view::IntoChildren::into_children(children));
410                    self.children = ::std::option::Option::Some(::ferroui_view::Element::Fragment(items));
411                    self
412                }
413
414                /// Adds a child.
415                pub fn child(mut self, child: impl ::ferroui_view::IntoChild) -> Self {
416                    let mut items = match self.children.take() {
417                        ::std::option::Option::Some(::ferroui_view::Element::Fragment(items)) => items,
418                        _ => ::std::vec::Vec::new(),
419                    };
420                    items.push(::ferroui_view::IntoChild::into_child(child));
421                    self.children = ::std::option::Option::Some(::ferroui_view::Element::Fragment(items));
422                    self
423                }
424            }
425        } else {
426            quote! {
427                #(#docs)*
428                pub fn #name(mut self, value: impl ::ferroui_view::IntoValue<#ty>) -> Self {
429                    self.#name = ::std::option::Option::Some(::ferroui_view::IntoValue::into_value(value));
430                    self
431                }
432            }
433        }
434    });
435    let arguments = params.iter().map(|param| {
436        let name = &param.name;
437        let message = format!("`{}` is a required property of `{}`", name, name_text);
438        match &param.default {
439            Some(default) => quote! { self.#name.unwrap_or_else(|| #default) },
440            None if name == "children" => quote! { self.#name.unwrap_or_default() },
441            None => quote! { self.#name.unwrap_or_else(|| panic!(#message)) },
442        }
443    });
444    let field_names: Vec<&Ident> = params.iter().map(|param| &param.name).collect();
445    let doc = format!("The builder of the component [`{name_text}`].");
446    let start_doc = format!("Starts an element of the component [`{name_text}`].");
447    let type_doc = format!("The component [`{name_text}`] as the name of an element of `rsx!`.");
448
449    function.attrs.push(syn::parse_quote!(#[allow(non_snake_case)]));
450    Ok(quote! {
451        #function
452
453        #[doc = #doc]
454        #[allow(non_camel_case_types)]
455        #visibility struct #builder_type {
456            #(#fields,)*
457            key: ::std::option::Option<::ferroui_view::Key>,
458        }
459
460        #[doc = #type_doc]
461        #[allow(non_camel_case_types)]
462        #visibility struct #name {}
463
464        impl ::ferroui_view::Builder for #name {
465            type Output = #builder_type;
466            fn builder() -> #builder_type {
467                #builder_function()
468            }
469        }
470
471        #[doc = #start_doc]
472        #visibility fn #builder_function() -> #builder_type {
473            #builder_type { #(#field_names: ::std::option::Option::None,)* key: ::std::option::Option::None }
474        }
475
476        impl #builder_type {
477            #(#setters)*
478
479            /// The key of the element among its siblings.
480            pub fn key(mut self, key: impl ::std::convert::Into<::ferroui_view::Key>) -> Self {
481                self.key = ::std::option::Option::Some(key.into());
482                self
483            }
484        }
485
486        impl ::ferroui_view::IntoElement for #builder_type {
487            fn into_element(self) -> ::ferroui_view::Element {
488                let element = #name(#(#arguments),*);
489                match self.key {
490                    ::std::option::Option::Some(key) => ::ferroui_view::element::with_key(element, key),
491                    ::std::option::Option::None => element,
492                }
493            }
494        }
495    })
496}