Skip to main content

repose_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4    Expr, Ident, Token, braced,
5    parse::{Parse, ParseStream},
6};
7
8struct ViewMacro {
9    layout: Option<Ident>,
10    modifiers: Vec<(Ident, Option<Expr>)>,
11    children: Vec<Expr>,
12}
13
14impl Parse for ViewMacro {
15    fn parse(input: ParseStream) -> syn::Result<Self> {
16        // If it's a single expression, treat as pass-through
17        if input.peek(syn::token::Paren) || input.peek(syn::token::Bracket) {
18            return Err(syn::Error::new(input.span(), "unexpected delimiters"));
19        }
20
21        // Parse optional layout identifier (followed by either { or ( )
22        let layout = if input.peek(Ident)
23            && (input.peek2(syn::token::Brace) || input.peek2(syn::token::Paren))
24        {
25            let ident: Ident = input.parse()?;
26            Some(ident)
27        } else {
28            None
29        };
30
31        // Parse optional modifier args: (key: val, ...)
32        let modifiers = if input.peek(syn::token::Paren) {
33            let content;
34            syn::parenthesized!(content in input);
35            let mut mods = Vec::new();
36            while !content.is_empty() {
37                let name: Ident = content.parse()?;
38                let value = if content.peek(Token![:]) {
39                    content.parse::<Token![:]>()?;
40                    Some(content.parse::<Expr>()?)
41                } else {
42                    None
43                };
44                mods.push((name, value));
45                if content.peek(Token![,]) {
46                    content.parse::<Token![,]>()?;
47                } else {
48                    break;
49                }
50            }
51            mods
52        } else {
53            Vec::new()
54        };
55
56        // Parse children block: { expr, expr, ... }
57        let children = if input.peek(syn::token::Brace) {
58            let content;
59            braced!(content in input);
60            let mut kids = Vec::new();
61            while !content.is_empty() {
62                let expr: Expr = content.parse()?;
63                kids.push(expr);
64                if content.peek(Token![,]) {
65                    content.parse::<Token![,]>()?;
66                } else {
67                    break;
68                }
69            }
70            kids
71        } else {
72            Vec::new()
73        };
74
75        Ok(Self {
76            layout,
77            modifiers,
78            children,
79        })
80    }
81}
82
83/// A view tree builder macro.
84///
85/// # Example
86///
87/// ```ignore
88/// // Pass-through single expression:
89/// View!(Text("hello"))
90///
91/// // Layout with children:
92/// View! {
93///     Column {
94///         Text("Hello"),
95///         Text("World"),
96///     }
97/// }
98///
99/// // With modifier args:
100/// View! {
101///     Column(padding: 16.0, gap: 8.0) {
102///         Text("Hello"),
103///         Text("World"),
104///     }
105/// }
106/// ```
107#[proc_macro]
108#[allow(non_snake_case)]
109pub fn View(input: TokenStream) -> TokenStream {
110    // Try ViewMacro parser first (handles `Ident { ... }` and `Ident(m: v) { ... }`)
111    let cloned = input.clone();
112    if let Ok(m) = syn::parse::<ViewMacro>(cloned) {
113        return expand_view(m).into();
114    }
115
116    // Fallback: pass-through single expression
117    if let Ok(expr) = syn::parse::<Expr>(input.clone()) {
118        return quote!(#expr).into();
119    }
120
121    quote!({}).into()
122}
123
124fn expand_view(m: ViewMacro) -> proc_macro2::TokenStream {
125    let ViewMacro {
126        layout,
127        modifiers,
128        children,
129    } = m;
130
131    if children.is_empty() && modifiers.is_empty() {
132        return quote!({});
133    }
134
135    let layout_name = layout.as_ref().map(|i| i.to_string()).unwrap_or_default();
136
137    let mod_calls = modifiers.iter().map(|(name, value)| {
138        if let Some(val) = value {
139            quote!(.#name(#val))
140        } else {
141            quote!(.#name())
142        }
143    });
144
145    if children.is_empty() {
146        // Layout with modifiers but no children
147        if modifiers.is_empty() {
148            quote!({})
149        } else {
150            quote! {
151                repose_ui::#layout_name(repose_core::Modifier::new() #(#mod_calls)*)
152            }
153        }
154    } else if layout.is_some() {
155        // Layout with children
156        let child_exprs = &children;
157        quote! {
158            repose_ui::#layout_name(repose_core::Modifier::new() #(#mod_calls)*)
159                .child((#(#child_exprs,)*))
160        }
161    } else {
162        // Bare children without layout: wrap in Column
163        let child_exprs = &children;
164        quote! {
165            repose_ui::Column(repose_core::Modifier::new()).child((#(#child_exprs,)*))
166        }
167    }
168}