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