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
7use proc_macro::TokenStream;
8use quote::quote;
9use syn::{ItemFn, parse_macro_input};
10
11/// Helper: parse crate path from attribute TokenStream
12fn parse_crate_path(attr: proc_macro::TokenStream) -> syn::Path {
13    if attr.is_empty() {
14        // Default to `tessera_ui` if no path is provided
15        syn::parse_quote!(::tessera_ui)
16    } else {
17        // Parse the provided path, e.g., `crate` or `tessera_ui`
18        syn::parse(attr).expect("Expected a valid path like `crate` or `tessera_ui`")
19    }
20}
21
22/// Helper: tokens to register a component node
23fn register_node_tokens(crate_path: &syn::Path, fn_name: &syn::Ident) -> proc_macro2::TokenStream {
24    quote! {
25        {
26            use #crate_path::{TesseraRuntime, ComponentNode};
27
28            TesseraRuntime::with_mut(|runtime| {
29                runtime.component_tree.add_node(
30                    ComponentNode {
31                        fn_name: stringify!(#fn_name).to_string(),
32                        measure_fn: None,
33                        input_handler_fn: None,
34                    }
35                )
36            });
37        }
38    }
39}
40
41/// Helper: tokens to inject `measure`
42fn measure_inject_tokens(crate_path: &syn::Path) -> proc_macro2::TokenStream {
43    quote! {
44        let measure = {
45            use #crate_path::{MeasureFn, TesseraRuntime};
46            |fun: Box<MeasureFn>| {
47                TesseraRuntime::with_mut(|runtime| {
48                    runtime
49                        .component_tree
50                        .current_node_mut()
51                        .unwrap()
52                        .measure_fn = Some(fun)
53                });
54            }
55        };
56    }
57}
58
59/// Helper: tokens to inject `input_handler`
60fn input_handler_inject_tokens(crate_path: &syn::Path) -> proc_macro2::TokenStream {
61    quote! {
62        let input_handler = {
63            use #crate_path::{InputHandlerFn, TesseraRuntime};
64            |fun: Box<InputHandlerFn>| {
65                TesseraRuntime::with_mut(|runtime| {
66                    runtime
67                        .component_tree
68                        .current_node_mut()
69                        .unwrap()
70                        .input_handler_fn = Some(fun)
71                });
72            }
73        };
74    }
75}
76
77/// Helper: tokens to inject `on_minimize`
78fn on_minimize_inject_tokens(crate_path: &syn::Path) -> proc_macro2::TokenStream {
79    quote! {
80        let on_minimize = {
81            use #crate_path::TesseraRuntime;
82            |fun: Box<dyn Fn(bool) + Send + Sync + 'static>| {
83                TesseraRuntime::with_mut(|runtime| runtime.on_minimize(fun));
84            }
85        };
86    }
87}
88
89/// Helper: tokens to inject `on_close`
90fn on_close_inject_tokens(crate_path: &syn::Path) -> proc_macro2::TokenStream {
91    quote! {
92        let on_close = {
93            use #crate_path::TesseraRuntime;
94            |fun: Box<dyn Fn() + Send + Sync + 'static>| {
95                TesseraRuntime::with_mut(|runtime| runtime.on_close(fun));
96            }
97        };
98    }
99}
100
101/// Helper: tokens to cleanup (pop node)
102fn cleanup_tokens(crate_path: &syn::Path) -> proc_macro2::TokenStream {
103    quote! {
104        {
105            use #crate_path::TesseraRuntime;
106
107            TesseraRuntime::with_mut(|runtime| runtime.component_tree.pop_node());
108        }
109    }
110}
111
112/// Transforms a regular Rust function into a Tessera UI component.
113///
114/// # What It Generates
115///
116/// The macro rewrites the function body so that on every invocation (every frame in an
117/// immediate‑mode pass) it:
118/// 1. Registers a new component node (push) into the global `ComponentTree`
119/// 2. Injects helper closures:
120///    * `measure(Box<MeasureFn>)` – supply layout measuring logic
121///    * `input_handler(Box<InputHandlerFn>)` – supply per‑frame interaction / event handling
122///    * `on_minimize(Box<dyn Fn(bool) + Send + Sync>)` – window minimize life‑cycle hook
123///    * `on_close(Box<dyn Fn() + Send + Sync>)` – window close life‑cycle hook
124/// 3. Executes the original user code inside an inner closure to prevent early `return`
125///    from skipping cleanup
126/// 4. Pops (removes) the component node (ensuring balanced push/pop even with early return)
127///
128/// # Usage
129///
130/// Annotate a free function (no captured self) with `#[tessera]`. You may then (optionally)
131/// call any of the injected helpers exactly once (last call wins if repeated).
132///
133/// # Parameters
134///
135/// * Attribute arguments are currently unused; pass nothing or `#[tessera]`.
136///
137/// # When NOT to Use
138///
139/// * For function that should not be a ui component.
140///
141/// # See Also
142///
143/// * [`#[shard]`](crate::shard) for navigation‑aware components with injectable shard state.
144#[proc_macro_attribute]
145pub fn tessera(attr: TokenStream, item: TokenStream) -> TokenStream {
146    let crate_path: syn::Path = parse_crate_path(attr);
147
148    // Parse the input function that will be transformed into a component
149    let input_fn = parse_macro_input!(item as ItemFn);
150    let fn_name = &input_fn.sig.ident; // Function name for component identification
151    let fn_vis = &input_fn.vis; // Visibility (pub, pub(crate), etc.)
152    let fn_attrs = &input_fn.attrs; // Attributes like #[doc], #[allow], etc.
153    let fn_sig = &input_fn.sig; // Function signature (parameters, return type)
154    let fn_block = &input_fn.block; // Original function body
155
156    // Prepare token fragments using helpers to keep function small and readable
157    let register_tokens = register_node_tokens(&crate_path, fn_name);
158    let measure_tokens = measure_inject_tokens(&crate_path);
159    let state_tokens = input_handler_inject_tokens(&crate_path);
160    let on_minimize_tokens = on_minimize_inject_tokens(&crate_path);
161    let on_close_tokens = on_close_inject_tokens(&crate_path);
162    let cleanup = cleanup_tokens(&crate_path);
163
164    // Generate the transformed function with Tessera runtime integration
165    let expanded = quote! {
166        #(#fn_attrs)*
167        #fn_vis #fn_sig {
168            #register_tokens
169
170            #measure_tokens
171
172            #state_tokens
173
174            #on_minimize_tokens
175
176            #on_close_tokens
177
178            // Execute the original function body within a closure to avoid early-return issues
179            let result = {
180                let closure = || #fn_block;
181                closure()
182            };
183
184            #cleanup
185
186            result
187        }
188    };
189
190    TokenStream::from(expanded)
191}
192
193#[cfg(feature = "shard")]
194/// Transforms a function into a *shard component* that can be navigated to via the routing
195/// system and (optionally) provided with a lazily‑initialized per‑shard state.
196///
197/// # Features
198///
199/// * Generates a `StructNameDestination` (UpperCamelCase + `Destination`) implementing
200///   `tessera_ui_shard::router::RouterDestination`
201/// * (Optional) Injects a single `#[state]` parameter whose type:
202///   - Must implement `Default + Send + Sync + 'static`
203///   - Is constructed (or reused) and passed to your function body
204/// * Produces a stable shard ID: `module_path!()::function_name`
205///
206/// # Lifecycle
207/// Controlled by the generated destination (via `#[state(...)]`).
208/// * Default: `Shard` – state is removed when the destination is `pop()`‑ed
209/// * Override: `#[state(app)]` (or `#[state(application)]`) – persist for the entire application
210///
211/// When `pop()` is called and the destination lifecycle is `Shard`, the registry
212/// entry is removed, freeing the state.
213///
214/// # Parameter Transformation
215/// * At most one parameter may be annotated with `#[state]`.
216/// * That parameter is removed from the *generated* function signature and supplied implicitly.
217/// * All other parameters remain explicit and become public fields on the generated
218///   `*Destination` struct.
219///
220/// # Generated Destination (Conceptual)
221///
222/// ```rust,ignore
223/// struct ProfilePageDestination { /* non-state params as public fields */ }
224/// impl RouterDestination for ProfilePageDestination {
225///     fn exec_component(&self) { profile_page(/* fields */); }
226///     fn shard_id(&self) -> &'static str { "<module>::profile_page" }
227/// }
228/// ```
229///
230/// # Limitations
231///
232/// * No support for multiple `#[state]` params (compile panic if violated)
233/// * Do not manually implement `RouterDestination` for these pages; rely on generation
234///
235/// # See Also
236///
237/// * Routing helpers: `tessera_ui::router::{push, pop, router_root}`
238/// * Shard state registry: `tessera_ui_shard::ShardRegistry`
239///
240/// # Safety
241///
242/// Internally uses an unsafe cast inside the registry to recover `Arc<T>` from
243/// `Arc<dyn ShardState>`; this is encapsulated and not exposed.
244///
245/// # Errors / Panics
246///
247/// * Panics at compile time if multiple `#[state]` parameters are used or unsupported
248///   pattern forms are encountered.
249#[proc_macro_attribute]
250pub fn shard(attr: TokenStream, input: TokenStream) -> TokenStream {
251    use heck::ToUpperCamelCase;
252    use syn::Pat;
253
254    let crate_path: syn::Path = if attr.is_empty() {
255        syn::parse_quote!(::tessera_ui)
256    } else {
257        syn::parse(attr).expect("Expected a valid path like `crate` or `tessera_ui`")
258    };
259
260    // 1. Parse the function marked by the macro
261    let mut func = parse_macro_input!(input as ItemFn);
262
263    // 2. Handle #[state] parameters, ensuring it's unique and removing it from the signature
264    //    Also parse optional lifecycle argument: #[state(app)] or #[state(shard)]
265    let mut state_param = None;
266    let mut state_lifecycle: Option<proc_macro2::TokenStream> = None;
267    let mut new_inputs = syn::punctuated::Punctuated::new();
268    for arg in func.sig.inputs.iter() {
269        if let syn::FnArg::Typed(pat_type) = arg {
270            // Detect #[state] and parse optional argument
271            let mut is_state = false;
272            let mut lifecycle_override: Option<proc_macro2::TokenStream> = None;
273            for attr in &pat_type.attrs {
274                if attr.path().is_ident("state") {
275                    is_state = true;
276                    // Try parse an optional argument: #[state(app)] / #[state(shard)]
277                    if let Ok(arg_ident) = attr.parse_args::<syn::Ident>() {
278                        let s = arg_ident.to_string().to_lowercase();
279                        if s == "app" || s == "application" {
280                            lifecycle_override = Some(
281                                quote! { #crate_path::tessera_ui_shard::ShardStateLifeCycle::Application },
282                            );
283                        } else if s == "shard" {
284                            lifecycle_override = Some(
285                                quote! { #crate_path::tessera_ui_shard::ShardStateLifeCycle::Shard },
286                            );
287                        } else {
288                            panic!(
289                                "Unsupported #[state(...)] argument in #[shard]: expected `app` or `shard`"
290                            );
291                        }
292                    }
293                }
294            }
295            if is_state {
296                if state_param.is_some() {
297                    panic!(
298                        "#[shard] function must have at most one parameter marked with #[state]."
299                    );
300                }
301                state_param = Some(pat_type.clone());
302                state_lifecycle = lifecycle_override;
303                continue;
304            }
305        }
306        new_inputs.push(arg.clone());
307    }
308    func.sig.inputs = new_inputs;
309
310    // 3. Extract the name and type of the state parameter
311    let (state_name, state_type) = if let Some(state_param) = state_param {
312        let name = match *state_param.pat {
313            Pat::Ident(ref pat_ident) => pat_ident.ident.clone(),
314            _ => panic!(
315                "Unsupported parameter pattern in #[shard] function. Please use a simple identifier like `state`."
316            ),
317        };
318        (Some(name), Some(state_param.ty))
319    } else {
320        (None, None)
321    };
322
323    // 4. Save the original function body and function name
324    let func_body = func.block;
325    let func_name_str = func.sig.ident.to_string();
326
327    // 5. Get the remaining function attributes and the modified signature
328    let func_attrs = &func.attrs;
329    let func_vis = &func.vis;
330    let func_sig_modified = &func.sig;
331
332    // Generate struct name for the new RouterDestination
333    let func_name = func.sig.ident.clone();
334    let struct_name = syn::Ident::new(
335        &format!("{}Destination", func_name_str.to_upper_camel_case()),
336        func_name.span(),
337    );
338
339    // Generate fields for the new struct that will implement `RouterDestination`
340    let dest_fields = func.sig.inputs.iter().map(|arg| match arg {
341        syn::FnArg::Typed(pat_type) => {
342            let ident = match *pat_type.pat {
343                syn::Pat::Ident(ref pat_ident) => &pat_ident.ident,
344                _ => panic!("Unsupported parameter pattern in #[shard] function."),
345            };
346            let ty = &pat_type.ty;
347            quote! { pub #ident: #ty }
348        }
349        _ => panic!("Unsupported parameter type in #[shard] function."),
350    });
351
352    // Only keep the parameters that are not marked with #[state]
353    let param_idents: Vec<_> = func
354        .sig
355        .inputs
356        .iter()
357        .map(|arg| match arg {
358            syn::FnArg::Typed(pat_type) => match *pat_type.pat {
359                syn::Pat::Ident(ref pat_ident) => pat_ident.ident.clone(),
360                _ => panic!("Unsupported parameter pattern in #[shard] function."),
361            },
362            _ => panic!("Unsupported parameter type in #[shard] function."),
363        })
364        .collect();
365
366    // 6. Use quote! to generate the new TokenStream code
367    //    Prepare optional lifecycle override method for RouterDestination impl.
368    let lifecycle_method_tokens = if let Some(lc) = state_lifecycle.clone() {
369        quote! {
370            fn life_cycle(&self) -> #crate_path::tessera_ui_shard::ShardStateLifeCycle {
371                #lc
372            }
373        }
374    } else {
375        // Default is `Shard` per RouterDestination trait; no override needed.
376        quote! {}
377    };
378
379    let expanded = {
380        // `exec_component` only passes struct fields (unmarked parameters).
381        let exec_args = param_idents
382            .iter()
383            .map(|ident| quote! { self.#ident.clone() });
384
385        if let Some(state_type) = state_type {
386            let state_name = state_name.as_ref().unwrap();
387            quote! {
388                // Generate a RouterDestination struct for the function
389                #func_vis struct #struct_name {
390                    #(#dest_fields),*
391                }
392
393                // Implement the RouterDestination trait for the generated struct
394                impl #crate_path::tessera_ui_shard::router::RouterDestination for #struct_name {
395                    fn exec_component(&self) {
396                        #func_name(
397                            #(
398                                #exec_args
399                            ),*
400                        );
401                    }
402
403                    fn shard_id(&self) -> &'static str {
404                        concat!(module_path!(), "::", #func_name_str)
405                    }
406
407                    #lifecycle_method_tokens
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                    unsafe {
418                        #crate_path::tessera_ui_shard::ShardRegistry::get().init_or_get::<#state_type, _, _>(
419                            SHARD_ID,
420                            |#state_name| {
421                                #func_body
422                            },
423                        )
424                    }
425                }
426            }
427        } else {
428            quote! {
429                // Generate a RouterDestination struct for the function
430                #func_vis struct #struct_name {
431                    #(#dest_fields),*
432                }
433
434                // Implement the RouterDestination trait for the generated struct
435                impl #crate_path::tessera_ui_shard::router::RouterDestination for #struct_name {
436                    fn exec_component(&self) {
437                        #func_name(
438                            #(
439                                #exec_args
440                            ),*
441                        );
442                    }
443
444                    fn shard_id(&self) -> &'static str {
445                        concat!(module_path!(), "::", #func_name_str)
446                    }
447
448                    #lifecycle_method_tokens
449                }
450
451                // Rebuild the function, keeping its attributes and visibility, but using the modified signature
452                #(#func_attrs)*
453                #func_vis #func_sig_modified {
454                    #func_body
455                }
456            }
457        }
458    };
459
460    // 7. Return the generated code as a TokenStream
461    TokenStream::from(expanded)
462}