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}