waterui_macros/
lib.rs

1//! Procedural macros for `WaterUI` framework.
2//!
3//! This crate provides derive macros and procedural macros for the `WaterUI` framework,
4//! including form generation, reactive signal formatting, and view builder patterns.
5
6use proc_macro::TokenStream;
7use quote::quote;
8use syn::{Data, DeriveInput, Fields, ItemFn, Meta, parse_macro_input};
9
10/// Derives the `FormBuilder` trait for structs, enabling automatic form generation.
11///
12/// This macro generates a complete `FormBuilder` implementation that creates a vertical
13/// stack of form fields. Each struct field is automatically mapped to an appropriate
14/// interactive form component based on its type.
15///
16/// # Type-to-Component Mapping
17///
18/// The macro uses these mapping rules:
19///
20/// | Rust Type | Form Component | Description |
21/// |-----------|----------------|-------------|
22/// | `String`, `&str`, `alloc::string::String` | `TextField` | Single-line text input |
23/// | `bool` | `Toggle` | Switch/checkbox for boolean values |
24/// | `i8`, `i16`, `i32`, `i64`, `i128`, `isize` | `Stepper` | Numeric input with +/- buttons |
25/// | `u8`, `u16`, `u32`, `u64`, `u128`, `usize` | `Stepper` | Unsigned numeric input |
26/// | `f64` | `Slider` | Slider with 0.0-1.0 range |
27/// | `Color` | `ColorPicker` | Color selection widget |
28///
29/// # Panics
30///
31/// This function will panic if the struct contains fields without identifiers,
32/// which should not happen with named fields in normal Rust structs.
33#[proc_macro_derive(FormBuilder)]
34pub fn derive_form_builder(input: TokenStream) -> TokenStream {
35    let input = parse_macro_input!(input as DeriveInput);
36    let name = &input.ident;
37
38    let fields = match &input.data {
39        Data::Struct(data_struct) => match &data_struct.fields {
40            Fields::Named(fields) => &fields.named,
41            _ => {
42                return syn::Error::new_spanned(
43                    input,
44                    "FormBuilder can only be derived for structs with named fields",
45                )
46                .to_compile_error()
47                .into();
48            }
49        },
50        _ => {
51            return syn::Error::new_spanned(input, "FormBuilder can only be derived for structs")
52                .to_compile_error()
53                .into();
54        }
55    };
56
57    // Collect field information
58    let field_views = fields.iter().map(|field| {
59        let field_name = field
60            .ident
61            .as_ref()
62            .expect("field should have an identifier");
63        let field_type = &field.ty;
64
65        // Convert field name from snake_case to "Title Case" for label
66        let field_name_str = field_name.to_string();
67        let label_text = snake_to_title_case(&field_name_str);
68
69        // Extract doc comments as placeholder text
70        let placeholder = field
71            .attrs
72            .iter()
73            .filter_map(|attr| {
74                if attr.path().is_ident("doc")
75                    && let Meta::NameValue(meta) = &attr.meta
76                    && let syn::Expr::Lit(expr_lit) = &meta.value
77                    && let syn::Lit::Str(lit_str) = &expr_lit.lit
78                {
79                    let doc = lit_str.value();
80                    // Clean up the doc comment (remove leading/trailing whitespace)
81                    let cleaned = doc.trim();
82                    if !cleaned.is_empty() {
83                        return Some(cleaned.to_string());
84                    }
85                }
86                None
87            })
88            .collect::<Vec<_>>()
89            .join(" ");
90
91        // Use FormBuilder trait for all types
92        // The FormBuilder::view method will handle whether to use the placeholder or not
93        quote! {
94            <#field_type as crate::FormBuilder>::view(
95                &projected.#field_name,
96                ::waterui::AnyView::new(#label_text),
97                ::waterui::Str::from(#placeholder)
98            )
99        }
100    });
101
102    // Check if we need to require Project trait
103    let requires_project = !fields.is_empty();
104
105    let view_body = if requires_project {
106        quote! {
107            // Use the Project trait to get individual field bindings
108            let projected = <Self as ::waterui::reactive::project::Project>::project(binding);
109
110            // Create a vstack with all form fields
111            ::waterui::component::stack::vstack((
112                #(#field_views,)*
113            ))
114        }
115    } else {
116        // Empty struct case
117        quote! {
118            ::waterui::component::stack::vstack(())
119        }
120    };
121
122    let field_types = fields.iter().map(|field| &field.ty);
123
124    // Generate the implementation
125    let expanded = quote! {
126        impl crate::FormBuilder for #name {
127            type View = ::waterui::component::stack::VStack<((#(<#field_types as crate::FormBuilder>::View),*),)>;
128
129            fn view(binding: &::waterui::Binding<Self>, _label: ::waterui::AnyView, _placeholder: ::waterui::Str) -> Self::View {
130                #view_body
131            }
132        }
133    };
134
135    TokenStream::from(expanded)
136}
137
138/// Converts `snake_case` to "Title Case"
139fn snake_to_title_case(s: &str) -> String {
140    s.split('_')
141        .map(|word| {
142            let mut chars = word.chars();
143            chars.next().map_or_else(String::new, |first| {
144                first
145                    .to_uppercase()
146                    .chain(chars.as_str().to_lowercase().chars())
147                    .collect()
148            })
149        })
150        .collect::<Vec<_>>()
151        .join(" ")
152}
153
154/// The `#[form]` attribute macro that automatically derives multiple traits commonly used for forms.
155///
156/// This macro derives the following traits:
157/// - `Default`
158/// - `Clone`
159/// - `Debug`
160/// - `FormBuilder`
161/// - `Project` (from `waterui::reactive` for reactive state management)
162/// - `Serialize` and `Deserialize` (from serde, if available)
163///
164/// # Example
165///
166/// ```text
167/// use waterui::{form, FormBuilder};
168///
169/// #[form]
170/// pub struct UserForm {
171///     /// User's full name
172///     pub name: String,
173///     /// User's age
174///     pub age: i32,
175///     /// Email notifications enabled
176///     pub notifications: bool,
177/// }
178///
179/// fn create_form() -> impl View {
180///     let form_binding = UserForm::binding();
181///     form(&form_binding)
182/// }
183/// ```
184///
185/// This is equivalent to manually writing:
186///
187/// ```text
188/// #[derive(Default, Clone, Debug, FormBuilder)]
189/// #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
190/// pub struct UserForm {
191///     pub name: String,
192///     pub age: i32,
193///     pub notifications: bool,
194/// }
195///
196/// impl Project for UserForm {
197///     // ... implementation provided by waterui::reactive derive
198/// }
199/// ```
200#[proc_macro_attribute]
201pub fn form(_args: TokenStream, input: TokenStream) -> TokenStream {
202    let input = parse_macro_input!(input as DeriveInput);
203    let _name = &input.ident;
204    let (_impl_generics, _ty_generics, _where_clause) = input.generics.split_for_impl();
205
206    // Check if it's a struct with named fields
207    let _fields = match &input.data {
208        Data::Struct(data_struct) => match &data_struct.fields {
209            Fields::Named(fields) => fields,
210            _ => {
211                return syn::Error::new_spanned(
212                    input,
213                    "The #[form] macro can only be applied to structs with named fields",
214                )
215                .to_compile_error()
216                .into();
217            }
218        },
219        _ => {
220            return syn::Error::new_spanned(
221                input,
222                "The #[form] macro can only be applied to structs",
223            )
224            .to_compile_error()
225            .into();
226        }
227    };
228
229    let expanded = quote! {
230        #[derive(Default, Clone, Debug, ::waterui::FormBuilder, ::waterui::Project)]
231        #input
232    };
233
234    TokenStream::from(expanded)
235}
236
237use syn::{Expr, LitStr, Token, Type, parse::Parse, punctuated::Punctuated};
238
239/// Derive macro for implementing the `Project` trait on structs.
240///
241/// This macro automatically generates a `Project` implementation that allows
242/// decomposing a struct binding into separate bindings for each field.
243///
244/// # Examples
245///
246/// ```rust,ignore
247/// use waterui::reactive::{Binding, binding, project::Project};
248/// use waterui_macros::Project;
249///
250/// #[derive(Project, Clone)]
251/// struct Person {
252///     name: String,
253///     age: u32,
254/// }
255///
256/// let person_binding: Binding<Person> = binding(Person {
257///     name: "Alice".to_string(),
258///     age: 30,
259/// });
260///
261/// let projected = person_binding.project();
262/// projected.name.set("Bob".to_string());
263/// projected.age.set(25u32);
264///
265/// let person = person_binding.get();
266/// assert_eq!(person.name, "Bob");
267/// assert_eq!(person.age, 25);
268/// ```
269#[proc_macro_derive(Project)]
270pub fn derive_project(input: TokenStream) -> TokenStream {
271    let input = parse_macro_input!(input as DeriveInput);
272
273    match &input.data {
274        Data::Struct(data_struct) => match &data_struct.fields {
275            Fields::Named(fields_named) => derive_project_struct(&input, fields_named),
276            Fields::Unnamed(fields_unnamed) => derive_project_tuple_struct(&input, fields_unnamed),
277            Fields::Unit => derive_project_unit_struct(&input),
278        },
279        Data::Enum(_) => {
280            syn::Error::new_spanned(input, "Project derive macro does not support enums")
281                .to_compile_error()
282                .into()
283        }
284        Data::Union(_) => {
285            syn::Error::new_spanned(input, "Project derive macro does not support unions")
286                .to_compile_error()
287                .into()
288        }
289    }
290}
291
292fn derive_project_struct(input: &DeriveInput, fields: &syn::FieldsNamed) -> TokenStream {
293    let struct_name = &input.ident;
294    let (_impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
295
296    // Create the projected struct type
297    let projected_struct_name =
298        syn::Ident::new(&format!("{struct_name}Projected"), struct_name.span());
299
300    // Generate fields for the projected struct
301    let projected_fields = fields.named.iter().map(|field| {
302        let field_name = &field.ident;
303        let field_type = &field.ty;
304        quote! {
305            pub #field_name: ::waterui::reactive::Binding<#field_type>
306        }
307    });
308
309    // Generate the projection logic
310    let field_projections = fields.named.iter().map(|field| {
311        let field_name = &field.ident;
312        quote! {
313            #field_name: {
314                let source = source.clone();
315                ::waterui::reactive::Binding::mapping(
316                    &source,
317                    |value| value.#field_name.clone(),
318                    move |binding, value| {
319                        binding.get_mut().#field_name = value;
320                    },
321                )
322            }
323        }
324    });
325
326    // Add lifetime bounds to generic parameters
327    let mut generics_with_static = input.generics.clone();
328    for param in &mut generics_with_static.params {
329        if let syn::GenericParam::Type(type_param) = param {
330            type_param.bounds.push(syn::parse_quote!('static));
331        }
332    }
333    let (impl_generics_with_static, _, _) = generics_with_static.split_for_impl();
334
335    let expanded = quote! {
336        /// Projected version of #struct_name with each field wrapped in a Binding.
337        #[derive(Debug)]
338        pub struct #projected_struct_name #ty_generics #where_clause {
339            #(#projected_fields,)*
340        }
341
342        impl #impl_generics_with_static ::waterui::reactive::project::Project for #struct_name #ty_generics #where_clause {
343            type Projected = #projected_struct_name #ty_generics;
344
345            fn project(source: &::waterui::reactive::Binding<Self>) -> Self::Projected {
346                #projected_struct_name {
347                    #(#field_projections,)*
348                }
349            }
350        }
351    };
352
353    TokenStream::from(expanded)
354}
355
356fn derive_project_tuple_struct(input: &DeriveInput, fields: &syn::FieldsUnnamed) -> TokenStream {
357    let struct_name = &input.ident;
358    let (_impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
359
360    // Generate tuple type for projection
361    let field_types: Vec<&Type> = fields.unnamed.iter().map(|field| &field.ty).collect();
362    let projected_tuple = if field_types.len() == 1 {
363        quote! { (::waterui::reactive::Binding<#(#field_types)*>,) }
364    } else {
365        quote! { (#(::waterui::reactive::Binding<#field_types>),*) }
366    };
367
368    // Generate field projections using index access
369    let field_projections = fields.unnamed.iter().enumerate().map(|(index, _)| {
370        let idx = syn::Index::from(index);
371        quote! {
372            {
373                let source = source.clone();
374                ::waterui::reactive::Binding::mapping(
375                    &source,
376                    |value| value.#idx.clone(),
377                    move |binding, value| {
378                        binding.get_mut().#idx = value;
379                    },
380                )
381            }
382        }
383    });
384
385    // Add lifetime bounds to generic parameters
386    let mut generics_with_static = input.generics.clone();
387    for param in &mut generics_with_static.params {
388        if let syn::GenericParam::Type(type_param) = param {
389            type_param.bounds.push(syn::parse_quote!('static));
390        }
391    }
392    let (impl_generics_with_static, _, _) = generics_with_static.split_for_impl();
393
394    let projection_tuple = if field_projections.len() == 1 {
395        quote! { (#(#field_projections)*,) }
396    } else {
397        quote! { (#(#field_projections),*) }
398    };
399
400    let expanded = quote! {
401        impl #impl_generics_with_static ::waterui::reactive::project::Project for #struct_name #ty_generics #where_clause {
402            type Projected = #projected_tuple;
403
404            fn project(source: &::waterui::reactive::Binding<Self>) -> Self::Projected {
405                #projection_tuple
406            }
407        }
408    };
409
410    TokenStream::from(expanded)
411}
412
413fn derive_project_unit_struct(input: &DeriveInput) -> TokenStream {
414    let struct_name = &input.ident;
415    let (_impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
416
417    // Add lifetime bounds to generic parameters
418    let mut generics_with_static = input.generics.clone();
419    for param in &mut generics_with_static.params {
420        if let syn::GenericParam::Type(type_param) = param {
421            type_param.bounds.push(syn::parse_quote!('static));
422        }
423    }
424    let (impl_generics_with_static, _, _) = generics_with_static.split_for_impl();
425
426    let expanded = quote! {
427        impl #impl_generics_with_static ::waterui::reactive::project::Project for #struct_name #ty_generics #where_clause {
428            type Projected = ();
429
430            fn project(_source: &::waterui::reactive::Binding<Self>) -> Self::Projected {
431                ()
432            }
433        }
434    };
435
436    TokenStream::from(expanded)
437}
438
439/// Input structure for the `s!` macro
440struct SInput {
441    format_str: LitStr,
442    args: Punctuated<Expr, Token![,]>,
443}
444
445impl Parse for SInput {
446    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
447        let format_str: LitStr = input.parse()?;
448        let args = if input.peek(Token![,]) {
449            input.parse::<Token![,]>()?;
450            Punctuated::parse_terminated(input)?
451        } else {
452            Punctuated::new()
453        };
454
455        Ok(Self { format_str, args })
456    }
457}
458
459/// Function-like procedural macro for creating formatted string signals with automatic variable capture.
460///
461/// This macro automatically detects named variables in format strings and captures them from scope.
462///
463/// # Examples
464///
465/// ```rust,ignore
466/// use waterui_macros::s;
467/// use waterui::reactive::constant;
468///
469/// let name = constant("Alice");
470/// let age = constant(25);
471///
472/// // Automatic variable capture from format string
473/// let msg = s!("Hello {name}, you are {age} years old");
474///
475/// // Positional arguments still work
476/// let msg2 = s!("Hello {}, you are {}", name, age);
477/// ```
478#[proc_macro]
479#[allow(clippy::similar_names, clippy::too_many_lines)]
480pub fn s(input: TokenStream) -> TokenStream {
481    let input = parse_macro_input!(input as SInput);
482    let format_str = input.format_str;
483    let format_value = format_str.value();
484
485    // Check for format string issues
486    let (has_positional, has_named, positional_count, named_vars) =
487        analyze_format_string(&format_value);
488
489    // If there are explicit arguments, validate and use positional approach
490    if !input.args.is_empty() {
491        // Check for mixed usage errors
492        if has_named {
493            return syn::Error::new_spanned(
494                &format_str,
495                format!(
496                    "Format string contains named arguments like {{{}}} but you provided positional arguments. \
497                    Either use positional placeholders like {{}} or remove the explicit arguments to use automatic variable capture.",
498                    named_vars.first().unwrap_or(&String::new())
499                )
500            )
501            .to_compile_error()
502            .into();
503        }
504
505        // Check argument count matches placeholders
506        if positional_count != input.args.len() {
507            return syn::Error::new_spanned(
508                &format_str,
509                format!(
510                    "Format string has {} positional placeholders but {} arguments were provided",
511                    positional_count,
512                    input.args.len()
513                ),
514            )
515            .to_compile_error()
516            .into();
517        }
518        let args: Vec<_> = input.args.iter().collect();
519        return match args.len() {
520            1 => {
521                let arg = &args[0];
522                quote! {
523                    {
524                        use ::waterui::reactive::SignalExt;
525                        SignalExt::map(#arg.clone(), |arg| waterui::reactive::__format!(#format_str, arg))
526                    }
527                }
528                .into()
529            }
530            2 => {
531                let arg1 = &args[0];
532                let arg2 = &args[1];
533                quote! {
534                    {
535                        use waterui::reactive::{SignalExt, zip::zip};
536                        SignalExt::map(zip(#arg1.clone(), #arg2.clone()), |(arg1, arg2)| {
537                            waterui::reactive::__format!(#format_str, arg1, arg2)
538                        })
539                    }
540                }
541                .into()
542            }
543            3 => {
544                let arg1 = &args[0];
545                let arg2 = &args[1];
546                let arg3 = &args[2];
547                quote! {
548                    {
549                        use ::waterui::reactive::{SignalExt, zip::zip};
550                        SignalExt::map(
551                            zip(zip(#arg1.clone(), #arg2.clone()), #arg3.clone()),
552                            |((arg1, arg2), arg3)| waterui::reactive::__format!(#format_str, arg1, arg2, arg3)
553                        )
554                    }
555                }
556                .into()
557            }
558            4 => {
559                let arg1 = &args[0];
560                let arg2 = &args[1];
561                let arg3 = &args[2];
562                let arg4 = &args[3];
563                quote! {
564                    {
565                        use ::waterui::reactive::{SignalExt, zip::zip};
566                        SignalExt::map(
567                            zip(
568                                zip(#arg1.clone(), #arg2.clone()),
569                                zip(#arg3.clone(), #arg4.clone())
570                            ),
571                            |((arg1, arg2), (arg3, arg4))| waterui::reactive::__format!(#format_str, arg1, arg2, arg3, arg4)
572                        )
573                    }
574                }.into()
575            }
576            _ => syn::Error::new_spanned(format_str, "Too many arguments, maximum 4 supported")
577                .to_compile_error()
578                .into(),
579        };
580    }
581
582    // Check for mixed placeholders when no explicit arguments
583    if has_positional && has_named {
584        return syn::Error::new_spanned(
585            &format_str,
586            "Format string mixes positional {{}} and named {{var}} placeholders. \
587            Use either all positional with explicit arguments, or all named for automatic capture.",
588        )
589        .to_compile_error()
590        .into();
591    }
592
593    // If has positional placeholders but no arguments provided
594    if has_positional && input.args.is_empty() {
595        return syn::Error::new_spanned(
596            &format_str,
597            format!(
598                "Format string has {positional_count} positional placeholder(s) {{}} but no arguments provided. \
599                Either provide arguments or use named placeholders like {{variable}} for automatic capture."
600            )
601        )
602        .to_compile_error()
603        .into();
604    }
605
606    // Parse format string to extract variable names for automatic capture
607    let var_names = named_vars;
608
609    // If no variables found, return constant
610    if var_names.is_empty() {
611        return quote! {
612            {
613                use ::waterui::reactive::constant;
614                constant(waterui::reactive::__format!(#format_str))
615            }
616        }
617        .into();
618    }
619
620    // Generate code for named variable capture
621    let var_idents: Vec<syn::Ident> = var_names
622        .iter()
623        .map(|name| syn::Ident::new(name, format_str.span()))
624        .collect();
625
626    match var_names.len() {
627        1 => {
628            let var = &var_idents[0];
629            quote! {
630                {
631                    use ::waterui::reactive::SignalExt;
632                    SignalExt::map(#var.clone(), |#var| {
633                        waterui::reactive::__format!(#format_str)
634                    })
635                }
636            }
637            .into()
638        }
639        2 => {
640            let var1 = &var_idents[0];
641            let var2 = &var_idents[1];
642            quote! {
643                {
644                    use ::waterui::reactive::{SignalExt, zip::zip};
645                    SignalExt::map(zip(#var1.clone(), #var2.clone()), |(#var1, #var2)| {
646                        waterui::reactive::__format!(#format_str)
647                    })
648                }
649            }
650            .into()
651        }
652        3 => {
653            let var1 = &var_idents[0];
654            let var2 = &var_idents[1];
655            let var3 = &var_idents[2];
656            quote! {
657                {
658                    use ::waterui::reactive::{SignalExt, zip::zip};
659                    SignalExt::map(
660                        zip(zip(#var1.clone(), #var2.clone()), #var3.clone()),
661                        |((#var1, #var2), #var3)| {
662                            ::waterui::reactive::__format!(#format_str)
663                        }
664                    )
665                }
666            }
667            .into()
668        }
669        4 => {
670            let var1 = &var_idents[0];
671            let var2 = &var_idents[1];
672            let var3 = &var_idents[2];
673            let var4 = &var_idents[3];
674            quote! {
675                {
676                    use ::waterui::reactive::{SignalExt, zip::zip};
677                    SignalExt::map(
678                        zip(
679                            zip(#var1.clone(), #var2.clone()),
680                            zip(#var3.clone(), #var4.clone())
681                        ),
682                        |((#var1, #var2), (#var3, #var4))| {
683                            ::waterui::reactive::__format!(#format_str)
684                        }
685                    )
686                }
687            }
688            .into()
689        }
690        _ => syn::Error::new_spanned(format_str, "Too many named variables, maximum 4 supported")
691            .to_compile_error()
692            .into(),
693    }
694}
695
696/// Analyze a format string to detect placeholder types and extract variable names
697fn analyze_format_string(format_str: &str) -> (bool, bool, usize, Vec<String>) {
698    let mut has_positional = false;
699    let mut has_named = false;
700    let mut positional_count = 0;
701    let mut named_vars = Vec::new();
702    let mut chars = format_str.chars().peekable();
703
704    while let Some(c) = chars.next() {
705        if c == '{' && chars.peek() == Some(&'{') {
706            // Skip escaped braces
707            chars.next();
708        } else if c == '{' {
709            let mut content = String::new();
710            let mut has_content = false;
711
712            while let Some(&next_char) = chars.peek() {
713                if next_char == '}' {
714                    chars.next(); // consume }
715                    break;
716                } else if next_char == ':' {
717                    // Format specifier found, we've captured the name/position part
718                    chars.next(); // consume :
719                    while let Some(&spec_char) = chars.peek() {
720                        if spec_char == '}' {
721                            chars.next(); // consume }
722                            break;
723                        }
724                        chars.next();
725                    }
726                    break;
727                }
728                content.push(chars.next().unwrap());
729                has_content = true;
730            }
731
732            // Analyze the content
733            if !has_content || content.is_empty() {
734                // Empty {} is positional
735                has_positional = true;
736                positional_count += 1;
737            } else if content.chars().all(|ch| ch.is_ascii_digit()) {
738                // Numeric like {0} or {1} is positional
739                has_positional = true;
740                positional_count += 1;
741            } else if content
742                .chars()
743                .next()
744                .is_some_and(|ch| ch.is_ascii_alphabetic() || ch == '_')
745            {
746                // Starts with letter or underscore, likely a variable name
747                has_named = true;
748                if !named_vars.contains(&content) {
749                    named_vars.push(content);
750                }
751            } else {
752                // Other cases treat as positional
753                has_positional = true;
754                positional_count += 1;
755            }
756        }
757    }
758
759    (has_positional, has_named, positional_count, named_vars)
760}
761
762/// Attribute macro for enabling hot reload on view functions.
763///
764/// This macro transforms a function returning `impl View` to support hot reloading.
765/// When the library is rebuilt during development, the view will automatically update
766/// without restarting the application.
767///
768/// # Example
769///
770/// ```ignore
771/// use waterui::prelude::*;
772///
773/// #[hot_reload]
774/// fn sidebar() -> impl View {
775///     vstack((
776///         text("Sidebar"),
777///         text("Content"),
778///     ))
779/// }
780///
781/// fn main() -> impl View {
782///     hstack((
783///         sidebar(),  // This view will hot reload
784///         content_panel(),
785///     ))
786/// }
787/// ```
788///
789/// # How It Works
790///
791/// The macro:
792/// 1. Wraps the function body in a `HotReloadView` that registers with the hot reload system
793/// 2. Generates a C-exported symbol (when built with `--cfg waterui_hot_reload_lib`) that
794///    the CLI can load to get the updated view
795///
796/// The generated symbol name follows the pattern: `waterui_hot_reload_<function_name>`
797///
798/// # Requirements
799///
800/// - The function must return `impl View`
801/// - Hot reload must be enabled via environment variables (set by `water run`)
802/// - For development, build with `RUSTFLAGS="--cfg waterui_hot_reload_lib"`
803#[proc_macro_attribute]
804pub fn hot_reload(_args: TokenStream, input: TokenStream) -> TokenStream {
805    let input_fn = parse_macro_input!(input as ItemFn);
806
807    let fn_name = &input_fn.sig.ident;
808    let fn_vis = &input_fn.vis;
809    let fn_attrs = &input_fn.attrs;
810    let fn_sig = &input_fn.sig;
811    let fn_block = &input_fn.block;
812
813    // Create the function ID: module_path::function_name
814    let fn_name_str = fn_name.to_string();
815
816    // Generate the export symbol name
817    let export_fn_name =
818        syn::Ident::new(&format!("waterui_hot_reload_{fn_name_str}"), fn_name.span());
819
820    if std::env::var("WATERUI_ENABLE_HOT_RELOAD").unwrap_or_default() != "1" {
821        // If hot reload is not enabled, return the original function unchanged
822        let expanded = quote! {
823            #(#fn_attrs)*
824            #fn_vis #fn_sig #fn_block
825        };
826        return TokenStream::from(expanded);
827    }
828
829    let expanded = quote! {
830        #(#fn_attrs)*
831        #fn_vis #fn_sig {
832            ::waterui::debug::HotReloadView::new(
833                concat!(module_path!(), "::", #fn_name_str),
834                || #fn_block
835            )
836        }
837
838        // Generate C export symbol for hot reload library
839        // Symbol name: waterui_hot_reload_<fn_name>
840        #[cfg(waterui_hot_reload_lib)]
841        #[doc(hidden)]
842        #[unsafe(no_mangle)]
843        pub unsafe extern "C" fn #export_fn_name() -> *mut () {
844            let view = #fn_block;
845            Box::into_raw(Box::new(::waterui::AnyView::new(view))).cast()
846        }
847    };
848
849    TokenStream::from(expanded)
850}