tessera_ui_macros/
lib.rs

1//! # Tessera Macros
2//!
3//! This crate provides procedural macros for the Tessera UI framework.
4//! The main export is the `#[tessera]` attribute macro, which transforms
5//! regular Rust functions into Tessera UI components.
6//!
7//! ## Usage
8//!
9//! ```rust,ignore
10//! use tessera_ui_macros::tessera;
11//!
12//! #[tessera]
13//! fn my_component() {
14//!     // Component logic here
15//!     // The macro provides access to `measure`, `state_handler` and `on_minimize` functions
16//! }
17//! ```
18//!
19//! The `#[tessera]` macro automatically:
20//! - Registers the function as a component in the Tessera component tree
21//! - Injects `measure`, `state_handler` and `on_minimize` functions into the component scope
22//! - Handles component tree management (adding/removing nodes)
23//! - Provides error safety by wrapping the function body
24
25use proc_macro::TokenStream;
26use quote::quote;
27use syn::{ItemFn, parse_macro_input};
28
29/// Helper: parse crate path from attribute TokenStream
30fn parse_crate_path(attr: proc_macro::TokenStream) -> syn::Path {
31    if attr.is_empty() {
32        // Default to `tessera_ui` if no path is provided
33        syn::parse_quote!(::tessera_ui)
34    } else {
35        // Parse the provided path, e.g., `crate` or `tessera_ui`
36        syn::parse(attr).expect("Expected a valid path like `crate` or `tessera_ui`")
37    }
38}
39
40/// Helper: tokens to register a component node
41fn register_node_tokens(crate_path: &syn::Path, fn_name: &syn::Ident) -> proc_macro2::TokenStream {
42    quote! {
43        {
44            use #crate_path::{TesseraRuntime, ComponentNode};
45
46            TesseraRuntime::with_mut(|runtime| {
47                runtime.component_tree.add_node(
48                    ComponentNode {
49                        fn_name: stringify!(#fn_name).to_string(),
50                        measure_fn: None,
51                        state_handler_fn: None,
52                    }
53                )
54            });
55        }
56    }
57}
58
59/// Helper: tokens to inject `measure`
60fn measure_inject_tokens(crate_path: &syn::Path) -> proc_macro2::TokenStream {
61    quote! {
62        let measure = {
63            use #crate_path::{MeasureFn, TesseraRuntime};
64            |fun: Box<MeasureFn>| {
65                TesseraRuntime::with_mut(|runtime| {
66                    runtime
67                        .component_tree
68                        .current_node_mut()
69                        .unwrap()
70                        .measure_fn = Some(fun)
71                });
72            }
73        };
74    }
75}
76
77/// Helper: tokens to inject `state_handler`
78fn state_handler_inject_tokens(crate_path: &syn::Path) -> proc_macro2::TokenStream {
79    quote! {
80        let state_handler = {
81            use #crate_path::{StateHandlerFn, TesseraRuntime};
82            |fun: Box<StateHandlerFn>| {
83                TesseraRuntime::with_mut(|runtime| {
84                    runtime
85                        .component_tree
86                        .current_node_mut()
87                        .unwrap()
88                        .state_handler_fn = Some(fun)
89                });
90            }
91        };
92    }
93}
94
95/// Helper: tokens to inject `on_minimize`
96fn on_minimize_inject_tokens(crate_path: &syn::Path) -> proc_macro2::TokenStream {
97    quote! {
98        let on_minimize = {
99            use #crate_path::TesseraRuntime;
100            |fun: Box<dyn Fn(bool) + Send + Sync + 'static>| {
101                TesseraRuntime::with_mut(|runtime| runtime.on_minimize(fun));
102            }
103        };
104    }
105}
106
107/// Helper: tokens to inject `on_close`
108fn on_close_inject_tokens(crate_path: &syn::Path) -> proc_macro2::TokenStream {
109    quote! {
110        let on_close = {
111            use #crate_path::TesseraRuntime;
112            |fun: Box<dyn Fn() + Send + Sync + 'static>| {
113                TesseraRuntime::with_mut(|runtime| runtime.on_close(fun));
114            }
115        };
116    }
117}
118
119/// Helper: tokens to cleanup (pop node)
120fn cleanup_tokens(crate_path: &syn::Path) -> proc_macro2::TokenStream {
121    quote! {
122        {
123            use #crate_path::TesseraRuntime;
124
125            TesseraRuntime::with_mut(|runtime| runtime.component_tree.pop_node());
126        }
127    }
128}
129
130/// The `#[tessera]` attribute macro transforms a regular Rust function into a Tessera UI component.
131///
132/// This macro performs several key transformations:
133/// 1. Registers the function as a node in the Tessera component tree
134/// 2. Injects `measure`, `state_handler` and `on_minimize` functions into the component scope
135/// 3. Manages component tree lifecycle (push/pop operations)
136/// 4. Provides error safety by wrapping the original function body
137///
138/// ## Parameters
139///
140/// - `_attr`: Attribute arguments (currently unused)
141/// - `item`: The function to be transformed into a component
142///
143/// ## Generated Code
144///
145/// The macro generates code that:
146///
147/// - Accesses the Tessera runtime to manage the component tree
148/// - Creates a new component node with the function name
149/// - Provides closures for `measure` and `state_handler` functionality
150/// - Executes the original function body within a safe closure
151/// - Cleans up the component tree after execution
152///
153/// ## Example
154///
155/// ```rust,ignore
156/// use tessera_ui_macros::tessera;
157///
158/// #[tessera]
159/// fn button_component(label: String) {
160///     // The macro provides access to these functions:
161///     measure(Box::new(|_| {
162///         // Custom layout logic
163///         use tessera_ui::{ComputedData, Px};
164///         Ok(ComputedData {
165///             width: Px(100),
166///             height: Px(50),
167///         })
168///     }));
169///     
170///     state_handler(Box::new(|_| {
171///         // Event handling logic
172///     }));
173///
174///     on_minimize(Box::new(|minimized| {
175///         if minimized {
176///             println!("Window minimized!");
177///         } else {
178///             println!("Window restored!");
179///         }
180///     }));
181/// }
182/// ```
183///
184/// ## Error Handling
185///
186/// The macro wraps the original function body in a closure to prevent
187/// early returns from breaking the component tree structure. This ensures
188/// that the component tree is always properly cleaned up, even if the
189/// component function returns early.
190#[proc_macro_attribute]
191pub fn tessera(attr: TokenStream, item: TokenStream) -> TokenStream {
192    let crate_path: syn::Path = parse_crate_path(attr);
193
194    // Parse the input function that will be transformed into a component
195    let input_fn = parse_macro_input!(item as ItemFn);
196    let fn_name = &input_fn.sig.ident; // Function name for component identification
197    let fn_vis = &input_fn.vis; // Visibility (pub, pub(crate), etc.)
198    let fn_attrs = &input_fn.attrs; // Attributes like #[doc], #[allow], etc.
199    let fn_sig = &input_fn.sig; // Function signature (parameters, return type)
200    let fn_block = &input_fn.block; // Original function body
201
202    // Prepare token fragments using helpers to keep function small and readable
203    let register_tokens = register_node_tokens(&crate_path, fn_name);
204    let measure_tokens = measure_inject_tokens(&crate_path);
205    let state_tokens = state_handler_inject_tokens(&crate_path);
206    let on_minimize_tokens = on_minimize_inject_tokens(&crate_path);
207    let on_close_tokens = on_close_inject_tokens(&crate_path);
208    let cleanup = cleanup_tokens(&crate_path);
209
210    // Generate the transformed function with Tessera runtime integration
211    let expanded = quote! {
212        #(#fn_attrs)*
213        #fn_vis #fn_sig {
214            #register_tokens
215
216            #measure_tokens
217
218            #state_tokens
219
220            #on_minimize_tokens
221
222            #on_close_tokens
223
224            // Execute the original function body within a closure to avoid early-return issues
225            let result = {
226                let closure = || #fn_block;
227                closure()
228            };
229
230            #cleanup
231
232            result
233        }
234    };
235
236    TokenStream::from(expanded)
237}
238
239#[cfg(feature = "shard")]
240#[proc_macro_attribute]
241pub fn shard(attr: TokenStream, input: TokenStream) -> TokenStream {
242    use heck::ToUpperCamelCase;
243    use syn::Pat;
244
245    let crate_path: syn::Path = if attr.is_empty() {
246        // Default to `tessera_ui` if no path is provided
247        syn::parse_quote!(::tessera_ui)
248    } else {
249        // Parse the provided path, e.g., `crate` or `tessera_ui`
250        syn::parse(attr).expect("Expected a valid path like `crate` or `tessera_ui`")
251    };
252
253    // 1. Parse the function marked by the macro
254    let mut func = parse_macro_input!(input as ItemFn);
255
256    // 2. Handle #[state] and #[route_controller] parameters, ensuring they are unique and removing them from the signature
257    let mut state_param = None;
258    let mut controller_param = None;
259    let mut new_inputs = syn::punctuated::Punctuated::new();
260    for arg in func.sig.inputs.iter() {
261        if let syn::FnArg::Typed(pat_type) = arg {
262            let is_state = pat_type
263                .attrs
264                .iter()
265                .any(|attr| attr.path().is_ident("state"));
266            let is_controller = pat_type
267                .attrs
268                .iter()
269                .any(|attr| attr.path().is_ident("route_controller"));
270            if is_state {
271                if state_param.is_some() {
272                    panic!(
273                        "#[shard] function must have at most one parameter marked with #[state]."
274                    );
275                }
276                state_param = Some(pat_type.clone());
277                continue;
278            }
279            if is_controller {
280                if controller_param.is_some() {
281                    panic!(
282                        "#[shard] function must have at most one parameter marked with #[route_controller]."
283                    );
284                }
285                controller_param = Some(pat_type.clone());
286                continue;
287            }
288        }
289        new_inputs.push(arg.clone());
290    }
291    func.sig.inputs = new_inputs;
292
293    // 3. Extract the name and type of the state/controller parameters
294    let (state_name, state_type) = if let Some(state_param) = state_param {
295        let name = match *state_param.pat {
296            Pat::Ident(ref pat_ident) => pat_ident.ident.clone(),
297            _ => panic!(
298                "Unsupported parameter pattern in #[shard] function. Please use a simple identifier like `state`."
299            ),
300        };
301        (Some(name), Some(state_param.ty))
302    } else {
303        (None, None)
304    };
305    let (controller_name, controller_type) = if let Some(controller_param) = controller_param {
306        let name = match *controller_param.pat {
307            Pat::Ident(ref pat_ident) => pat_ident.ident.clone(),
308            _ => panic!(
309                "Unsupported parameter pattern in #[shard] function. Please use a simple identifier like `ctrl`."
310            ),
311        };
312        (Some(name), Some(controller_param.ty))
313    } else {
314        (None, None)
315    };
316
317    // 4. Save the original function body and function name
318    let func_body = func.block;
319    let func_name_str = func.sig.ident.to_string();
320
321    // 5. Get the remaining function attributes and the modified signature
322    let func_attrs = &func.attrs;
323    let func_vis = &func.vis;
324    let func_sig_modified = &func.sig;
325
326    // Generate struct name for the new RouterDestination
327    let func_name = func.sig.ident.clone();
328    let struct_name = syn::Ident::new(
329        &format!("{}Destination", func_name_str.to_upper_camel_case()),
330        func_name.span(),
331    );
332
333    // Generate fields for the new struct that will implement `RouterDestination`
334    let dest_fields = func.sig.inputs.iter().map(|arg| match arg {
335        syn::FnArg::Typed(pat_type) => {
336            let ident = match *pat_type.pat {
337                syn::Pat::Ident(ref pat_ident) => &pat_ident.ident,
338                _ => panic!("Unsupported parameter pattern in #[shard] function."),
339            };
340            let ty = &pat_type.ty;
341            quote! { pub #ident: #ty }
342        }
343        _ => panic!("Unsupported parameter type in #[shard] function."),
344    });
345
346    // Only keep the parameters that are not marked with #[state] or #[route_controller]
347    let param_idents: Vec<_> = func
348        .sig
349        .inputs
350        .iter()
351        .map(|arg| match arg {
352            syn::FnArg::Typed(pat_type) => match *pat_type.pat {
353                syn::Pat::Ident(ref pat_ident) => pat_ident.ident.clone(),
354                _ => panic!("Unsupported parameter pattern in #[shard] function."),
355            },
356            _ => panic!("Unsupported parameter type in #[shard] function."),
357        })
358        .collect();
359
360    // 6. Use quote! to generate the new TokenStream code
361    let expanded = {
362        // `exec_component` only passes struct fields (unmarked parameters).
363        let exec_args = param_idents
364            .iter()
365            .map(|ident| quote! { self.#ident.clone() });
366
367        if let Some(state_type) = state_type {
368            let state_name = state_name.as_ref().unwrap();
369            let controller_inject = if let Some((ref ctrl_name, ref ctrl_ty)) =
370                controller_name.zip(controller_type.as_ref())
371            {
372                quote! {
373                    // Inject RouteController instance here
374                    let #ctrl_name = #ctrl_ty::new();
375                }
376            } else {
377                quote! {}
378            };
379            quote! {
380                // Generate a RouterDestination struct for the function
381                /// This struct represents a route destination for the #[shard] function
382                ///
383                /// # Example
384                ///
385                /// ```ignore
386                /// controller.push(AboutPageDestination {
387                ///     title: "About".to_string(),
388                ///     description: "This is the about page.".to_string(),
389                /// })
390                /// ```
391                #func_vis struct #struct_name {
392                    #(#dest_fields),*
393                }
394
395                // Implement the RouterDestination trait for the generated struct
396                impl #crate_path::tessera_ui_shard::router::RouterDestination for #struct_name {
397                    fn exec_component(&self) {
398                        #func_name(
399                            #(
400                                #exec_args
401                            ),*
402                        );
403                    }
404
405                    fn shard_id(&self) -> &'static str {
406                        concat!(module_path!(), "::", #func_name_str)
407                    }
408                }
409
410                // Rebuild the function, keeping its attributes and visibility, but using the modified signature
411                #(#func_attrs)*
412                #func_vis #func_sig_modified {
413                    // Generate a stable unique ID at the call site
414                    const SHARD_ID: &str = concat!(module_path!(), "::", #func_name_str);
415
416                    // Call the global registry and pass the original function body as a closure
417                    // Inject state/controller here
418                    unsafe {
419                        #crate_path::tessera_ui_shard::ShardRegistry::get().init_or_get::<#state_type, _, _>(
420                            SHARD_ID,
421                            |#state_name| {
422                                #controller_inject
423                                #func_body
424                            },
425                        )
426                    }
427                }
428            }
429        } else {
430            let controller_inject = if let Some((ref ctrl_name, ref ctrl_ty)) =
431                controller_name.zip(controller_type.as_ref())
432            {
433                quote! {
434                    // Inject RouteController instance here
435                    let #ctrl_name = #ctrl_ty::new();
436                }
437            } else {
438                quote! {}
439            };
440            quote! {
441                // Generate a RouterDestination struct for the function
442                #func_vis struct #struct_name {
443                    #(#dest_fields),*
444                }
445
446                // Implement the RouterDestination trait for the generated struct
447                impl #crate_path::tessera_ui_shard::router::RouterDestination for #struct_name {
448                    fn exec_component(&self) {
449                        #func_name(
450                            #(
451                                #exec_args
452                            ),*
453                        );
454                    }
455
456                    fn shard_id(&self) -> &'static str {
457                        concat!(module_path!(), "::", #func_name_str)
458                    }
459                }
460
461                // Rebuild the function, keeping its attributes and visibility, but using the modified signature
462                #(#func_attrs)*
463                #func_vis #func_sig_modified {
464                    #controller_inject
465                    #func_body
466                }
467            }
468        }
469    };
470
471    // 7. Return the generated code as a TokenStream
472    TokenStream::from(expanded)
473}