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}