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}