Skip to main content

blaeck_macros/
lib.rs

1//! Blaeck procedural macros.
2//!
3//! This crate provides the `element!` macro for declaring UI elements.
4
5use proc_macro::TokenStream;
6use quote::{quote, ToTokens};
7use syn::{
8    braced, parenthesized,
9    parse::{Parse, ParseStream},
10    parse_macro_input,
11    punctuated::Punctuated,
12    token::{Brace, Comma, Paren},
13    Expr, FieldValue, Result, Token, Type,
14};
15
16/// A parsed child element - either an element or an expression for dynamic children.
17enum ParsedChild {
18    Element(ParsedElement),
19    Expr(Expr),
20}
21
22/// A parsed element declaration.
23///
24/// Elements have the form:
25/// ```ignore
26/// ComponentType(prop1: value1, prop2: value2) {
27///     ChildElement1
28///     ChildElement2
29/// }
30/// ```
31struct ParsedElement {
32    /// The component type (e.g., Box, Text, Spacer)
33    ty: Type,
34    /// Property assignments (e.g., content: "Hello", bold: true)
35    props: Punctuated<FieldValue, Comma>,
36    /// Child elements
37    children: Vec<ParsedChild>,
38}
39
40impl Parse for ParsedElement {
41    fn parse(input: ParseStream) -> Result<Self> {
42        // Parse the component type
43        let ty: Type = input.parse()?;
44
45        // Parse optional props in parentheses
46        let props = if input.peek(Paren) {
47            let props_input;
48            parenthesized!(props_input in input);
49            Punctuated::parse_terminated(&props_input)?
50        } else {
51            Punctuated::new()
52        };
53
54        // Parse optional children in braces
55        let mut children = Vec::new();
56        if input.peek(Brace) {
57            let children_input;
58            braced!(children_input in input);
59            while !children_input.is_empty() {
60                if children_input.peek(Token![#]) {
61                    // Dynamic child: #(expr)
62                    children_input.parse::<Token![#]>()?;
63                    let expr_input;
64                    parenthesized!(expr_input in children_input);
65                    children.push(ParsedChild::Expr(expr_input.parse()?));
66                } else {
67                    // Static child element
68                    children.push(ParsedChild::Element(children_input.parse()?));
69                }
70            }
71        }
72
73        Ok(Self {
74            ty,
75            props,
76            children,
77        })
78    }
79}
80
81impl ToTokens for ParsedElement {
82    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
83        let ty = &self.ty;
84
85        // Generate property assignments
86        let prop_assignments = self.props.iter().map(|fv| {
87            let member = &fv.member;
88            let expr = &fv.expr;
89            quote! { _quill_props.#member = (#expr).into(); }
90        });
91
92        // Generate children
93        let has_children = !self.children.is_empty();
94        let children_code = if has_children {
95            let child_elements = self.children.iter().map(|child| match child {
96                ParsedChild::Element(elem) => {
97                    quote! { #elem }
98                }
99                ParsedChild::Expr(expr) => {
100                    quote! { #expr }
101                }
102            });
103            Some(quote! {
104                let _quill_children: ::std::vec::Vec<Element> = ::std::vec![#(#child_elements),*];
105            })
106        } else {
107            None
108        };
109
110        let children_vec = if has_children {
111            quote! { _quill_children }
112        } else {
113            quote! { ::std::vec![] }
114        };
115
116        // Generate the element creation code
117        // We use types that must be in scope from the prelude
118        tokens.extend(quote! {
119            {
120                type Props = <#ty as Component>::Props;
121                let mut _quill_props: Props = ::std::default::Default::default();
122                #(#prop_assignments)*
123                #children_code
124                Element::node::<#ty>(_quill_props, #children_vec)
125            }
126        });
127    }
128}
129
130/// Used to declare an element and its properties.
131///
132/// **Important:** The following types must be in scope for the macro to work:
133/// - `Component` - the component trait
134/// - `Element` - the element type
135/// - Any component types used (e.g., `Box`, `Text`, `Spacer`)
136///
137/// The easiest way to ensure this is to use `use quill::prelude::*;`
138///
139/// Elements are declared starting with their type. All properties are optional, so the simplest
140/// use of this macro is just a type name:
141///
142/// ```ignore
143/// element!(Text)
144/// ```
145///
146/// This will evaluate to an `Element` with no properties set.
147///
148/// To specify properties, you can add them in a parenthesized block after the type name:
149///
150/// ```ignore
151/// element! {
152///     Text(content: "Hello, World!", color: Color::Green, bold: true)
153/// }
154/// ```
155///
156/// If the element has children, you can pass one or more child elements in braces:
157///
158/// ```ignore
159/// element! {
160///     Box {
161///         Text(content: "Hello")
162///         Text(content: "World")
163///     }
164/// }
165/// ```
166///
167/// You can also use Rust expressions to conditionally add child elements via `#()` blocks:
168///
169/// ```ignore
170/// element! {
171///     Box {
172///         #(if show_greeting {
173///             element! { Text(content: "Hello!") }
174///         } else {
175///             Element::empty()
176///         })
177///     }
178/// }
179/// ```
180///
181/// # Examples
182///
183/// Simple text:
184/// ```ignore
185/// element! {
186///     Text(content: "Hello")
187/// }
188/// ```
189///
190/// Box with border and children:
191/// ```ignore
192/// element! {
193///     Box(border_style: BorderStyle::Single, padding: 1.0) {
194///         Text(content: "Title", bold: true)
195///         Spacer
196///         Text(content: "Footer")
197///     }
198/// }
199/// ```
200///
201/// Nested layout:
202/// ```ignore
203/// element! {
204///     Box(flex_direction: FlexDirection::Column) {
205///         Box(flex_direction: FlexDirection::Row) {
206///             Text(content: "Left")
207///             Spacer
208///             Text(content: "Right")
209///         }
210///         Text(content: "Bottom")
211///     }
212/// }
213/// ```
214#[proc_macro]
215pub fn element(input: TokenStream) -> TokenStream {
216    let element = parse_macro_input!(input as ParsedElement);
217    quote!(#element).into()
218}
219
220#[cfg(test)]
221mod tests {
222    // Tests run through the quill crate's integration tests
223}