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}