rxtui_macros/lib.rs
1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{DeriveInput, Fields, parse_macro_input};
4
5/// Derive macro that implements the Component trait
6///
7/// This macro automatically implements all the boilerplate methods
8/// required by the Component trait.
9///
10/// By default, it looks for a field named `id`. You can specify a different
11/// field using the `#[component_id]` attribute.
12///
13/// # Example
14///
15/// ```ignore
16/// #[derive(Component, Clone)]
17/// struct MyComponent {
18/// id: Option<ComponentId>, // Default field name
19/// // other fields...
20/// }
21///
22/// // Or with a custom field name:
23/// #[derive(Component, Clone)]
24/// struct MyComponent {
25/// #[component_id]
26/// key: Option<ComponentId>,
27/// // other fields...
28/// }
29///
30/// impl MyComponent {
31/// fn update(&self, ctx: &Context, msg: Box<dyn Message>, topic: Option<&str>) -> Action {
32/// // your implementation
33/// }
34///
35/// fn view(&self, ctx: &Context) -> Node {
36/// // your implementation
37/// }
38/// }
39/// ```
40#[proc_macro_derive(Component, attributes(component_id))]
41pub fn derive_component(input: TokenStream) -> TokenStream {
42 let input = parse_macro_input!(input as DeriveInput);
43 let name = &input.ident;
44
45 // Verify it's a struct
46 let fields = match &input.data {
47 syn::Data::Struct(data) => &data.fields,
48 _ => panic!("Component can only be derived for structs"),
49 };
50
51 // Find the ID field - either marked with #[component_id] or named "id"
52 let id_field =
53 match fields {
54 Fields::Named(fields) => {
55 // First, look for a field with #[component_id] attribute
56 let marked_field = fields.named.iter().find(|f| {
57 f.attrs
58 .iter()
59 .any(|attr| attr.path().is_ident("component_id"))
60 });
61
62 if let Some(field) = marked_field {
63 field
64 .ident
65 .as_ref()
66 .expect("Named field should have an identifier")
67 } else {
68 // Otherwise, look for a field named "id"
69 fields.named.iter()
70 .find(|f| f.ident.as_ref().is_some_and(|ident| ident == "id"))
71 .and_then(|f| f.ident.as_ref())
72 .expect(
73 "Component derive requires either a field named `id: Option<ComponentId>` \
74 or a field marked with `#[component_id]` attribute"
75 )
76 }
77 }
78 _ => panic!("Component can only be derived for structs with named fields"),
79 };
80
81 // Generate the implementation
82 let expanded = quote! {
83 impl rxtui::Component for #name {
84 fn get_id(&self) -> Option<rxtui::ComponentId> {
85 self.#id_field.clone()
86 }
87
88 fn set_id(&mut self, id: rxtui::ComponentId) {
89 self.#id_field = Some(id);
90 }
91
92 fn as_any(&self) -> &dyn std::any::Any {
93 self
94 }
95
96 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
97 self
98 }
99
100 fn clone_box(&self) -> Box<dyn rxtui::Component> {
101 Box::new(self.clone())
102 }
103
104 // Forward to the user's implementations
105 fn update(&self, ctx: &rxtui::Context, msg: Box<dyn rxtui::Message>, topic: Option<&str>) -> rxtui::Action {
106 <#name>::update(self, ctx, msg, topic)
107 }
108
109 fn view(&self, ctx: &rxtui::Context) -> rxtui::Node {
110 <#name>::view(self, ctx)
111 }
112 }
113 };
114
115 TokenStream::from(expanded)
116}