Skip to main content

repose_macros/
lib.rs

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