rxtui_macros/lib.rs
1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::parse::{Parse, ParseStream};
4use syn::{
5 DeriveInput, Expr, FnArg, Ident, ImplItem, ItemFn, ItemImpl, LitStr, Pat, PatType, Token, Type,
6 parse_macro_input,
7};
8
9//--------------------------------------------------------------------------------------------------
10// Types
11//--------------------------------------------------------------------------------------------------
12
13/// Represents a topic mapping like "timer" => TimerMsg or self.topic => TimerMsg
14enum TopicKey {
15 Static(LitStr),
16 Dynamic(Expr),
17}
18
19struct TopicMapping {
20 key: TopicKey,
21 _arrow: Token![=>],
22 msg_type: Type,
23}
24
25/// Parse the update attribute arguments with new syntax
26struct UpdateArgs {
27 msg_type: Option<Type>,
28 topics: Vec<TopicMapping>,
29}
30
31//--------------------------------------------------------------------------------------------------
32// Trait Implementations
33//--------------------------------------------------------------------------------------------------
34
35impl Parse for TopicMapping {
36 fn parse(input: ParseStream) -> syn::Result<Self> {
37 // Try to parse as a string literal first
38 let key = if input.peek(LitStr) {
39 TopicKey::Static(input.parse()?)
40 } else {
41 // Otherwise parse as an expression (e.g., self.topic_name)
42 TopicKey::Dynamic(input.parse()?)
43 };
44
45 Ok(TopicMapping {
46 key,
47 _arrow: input.parse()?,
48 msg_type: input.parse()?,
49 })
50 }
51}
52
53impl Parse for UpdateArgs {
54 fn parse(input: ParseStream) -> syn::Result<Self> {
55 let mut msg_type = None;
56 let mut topics = Vec::new();
57
58 while !input.is_empty() {
59 // Parse identifier (msg or topics)
60 let ident: Ident = input.parse()?;
61 input.parse::<Token![=]>()?;
62
63 if ident == "msg" {
64 msg_type = Some(input.parse()?);
65 } else if ident == "topics" {
66 // Parse array of topic mappings
67 let content;
68 syn::bracketed!(content in input);
69
70 while !content.is_empty() {
71 topics.push(content.parse::<TopicMapping>()?);
72
73 if !content.is_empty() {
74 content.parse::<Token![,]>()?;
75 }
76 }
77 }
78
79 if !input.is_empty() {
80 input.parse::<Token![,]>()?;
81 }
82 }
83
84 Ok(UpdateArgs { msg_type, topics })
85 }
86}
87
88//--------------------------------------------------------------------------------------------------
89// Functions
90//--------------------------------------------------------------------------------------------------
91
92/// Extract parameter name and type from a function argument
93fn extract_param_info(arg: &FnArg) -> Option<(Ident, Type)> {
94 if let FnArg::Typed(PatType { pat, ty, .. }) = arg
95 && let Pat::Ident(pat_ident) = &**pat
96 {
97 let name = pat_ident.ident.clone();
98 let ty = (**ty).clone();
99 return Some((name, ty));
100 }
101 None
102}
103
104/// Derive macro that implements the Component trait
105///
106/// This macro automatically implements all the boilerplate methods
107/// required by the Component trait.
108///
109/// # Example
110///
111/// ```ignore
112/// #[derive(Component)]
113/// struct MyComponent {
114/// // any fields you need
115/// }
116///
117/// // Or for unit structs:
118/// #[derive(Component)]
119/// struct MyComponent;
120///
121/// impl MyComponent {
122/// fn update(&self, ctx: &Context, msg: Box<dyn Message>, topic: Option<&str>) -> Action {
123/// // your implementation
124/// }
125///
126/// fn view(&self, ctx: &Context) -> Node {
127/// // your implementation
128/// }
129/// }
130/// ```
131#[proc_macro_derive(Component)]
132pub fn derive_component(input: TokenStream) -> TokenStream {
133 let input = parse_macro_input!(input as DeriveInput);
134 let name = &input.ident;
135
136 // Generate the implementation
137 let expanded = quote! {
138 impl rxtui::Component for #name {
139 fn as_any(&self) -> &dyn std::any::Any {
140 self
141 }
142
143 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
144 self
145 }
146
147 // Use method resolution to call inherent __component_update_impl if it exists,
148 // otherwise fall back to the trait's default implementation (Action::None)
149 fn update(&self, ctx: &rxtui::Context, msg: Box<dyn rxtui::Message>, topic: Option<&str>) -> rxtui::Action {
150 use rxtui::providers::UpdateProvider;
151 self.__component_update_impl(ctx, msg, topic)
152 }
153
154 // Use method resolution to call inherent __component_view_impl if it exists,
155 // otherwise fall back to the trait's default implementation (empty Node)
156 fn view(&self, ctx: &rxtui::Context) -> rxtui::Node {
157 use rxtui::providers::ViewProvider;
158 self.__component_view_impl(ctx)
159 }
160
161 // Use method resolution to call inherent __component_effects_impl if it exists,
162 // otherwise fall back to the trait's default implementation (empty vec)
163 fn effects(&self, ctx: &rxtui::Context) -> Vec<rxtui::effect::Effect> {
164 use rxtui::providers::EffectsProvider;
165 self.__component_effects_impl(ctx)
166 }
167 }
168
169 };
170
171 TokenStream::from(expanded)
172}
173
174/// Simplifies component update methods by automatically handling message downcasting,
175/// state fetching, and topic routing.
176///
177/// # Basic usage
178///
179/// The simplest form just handles a single message type:
180///
181/// ```ignore
182/// #[update]
183/// fn update(&self, ctx: &Context, msg: CounterMsg) -> Action {
184/// match msg {
185/// CounterMsg::Exit => Action::Exit,
186/// _ => Action::None,
187/// }
188/// }
189/// ```
190///
191/// # With state management
192///
193/// Add a state parameter and it will be automatically fetched and passed in:
194///
195/// ```ignore
196/// #[update]
197/// fn update(&self, ctx: &Context, msg: CounterMsg, mut state: CounterState) -> Action {
198/// match msg {
199/// CounterMsg::Increment => {
200/// state.count += 1;
201/// Action::Update(Box::new(state))
202/// }
203/// CounterMsg::Exit => Action::Exit,
204/// }
205/// }
206/// ```
207///
208/// # With topic-based messaging
209///
210/// Components can also listen to topic messages. Topics can be static strings or
211/// dynamic expressions from self:
212///
213/// ```ignore
214/// #[update(msg = AppMsg, topics = ["timer" => TimerMsg, self.topic_name => UpdateMsg])]
215/// fn update(&self, ctx: &Context, messages: Messages, mut state: AppState) -> Action {
216/// match messages {
217/// Messages::AppMsg(msg) => { /* handle regular message */ }
218/// Messages::TimerMsg(msg) => { /* handle timer topic */ }
219/// Messages::UpdateMsg(msg) => { /* handle dynamic topic */ }
220/// }
221/// }
222/// ```
223///
224/// # How it works
225///
226/// The macro transforms your simplified function into the full Component trait implementation:
227///
228/// ```text
229/// ┌─────────────────────────────────────────────────────────────────┐
230/// │ #[update(msg = CounterMsg, topics = [self.topic => ResetMsg])] │
231/// │ fn update(&self, ctx: &Context, msg: Messages, │
232/// │ mut state: CounterState) -> Action { │
233/// │ match msg { │
234/// │ Messages::CounterMsg(m) => { ... } │
235/// │ Messages::ResetMsg(m) => { ... } │
236/// │ } │
237/// │ } │
238/// └─────────────────────────────────────────────────────────────────┘
239/// ↓
240/// ┌─────────────────────────────────────────────────────────────────┐
241/// │ fn update(&self, ctx: &Context, │
242/// │ msg: Box<dyn Message>, │
243/// │ topic: Option<&str>) -> Action { │
244/// │ │
245/// │ enum Messages { /* generated */ } │
246/// │ let mut state = ctx.get_state::<CounterState>(); │
247/// │ │
248/// │ if let Some(topic) = topic { │
249/// │ if topic == &*(self.topic) { │
250/// │ if let Some(m) = msg.downcast::<ResetMsg>() { │
251/// │ let msg = Messages::ResetMsg(m.clone()); │
252/// │ return { /* user's match block */ }; │
253/// │ } │
254/// │ } │
255/// │ return Action::None; │
256/// │ } │
257/// │ │
258/// │ if let Some(m) = msg.downcast::<CounterMsg>() { │
259/// │ let msg = Messages::CounterMsg(m.clone()); │
260/// │ return { /* user's match block */ }; │
261/// │ } │
262/// │ │
263/// │ Action::None │
264/// │ } │
265/// └─────────────────────────────────────────────────────────────────┘
266/// ```
267///
268/// # Parameters
269///
270/// The function parameters are detected by position:
271/// - `&self` (required)
272/// - `&Context` (required) - any name allowed
273/// - Message type (required) - any name allowed
274/// - State type (optional) - any name allowed
275#[proc_macro_attribute]
276pub fn update(args: TokenStream, input: TokenStream) -> TokenStream {
277 let input_fn = parse_macro_input!(input as ItemFn);
278
279 let _fn_name = &input_fn.sig.ident;
280 let fn_vis = &input_fn.vis;
281 let fn_block = &input_fn.block;
282
283 // Parse function parameters by position
284 let mut params = input_fn.sig.inputs.iter();
285
286 // Position 0: &self (skip it)
287 params
288 .next()
289 .expect("#[update] function must have &self as first parameter");
290
291 // Position 1: &Context
292 let ctx_param = params
293 .next()
294 .expect("#[update] function must have &Context as second parameter");
295 let (ctx_name, _ctx_type) =
296 extract_param_info(ctx_param).expect("Failed to extract context parameter info");
297
298 // Position 2: Message type
299 let msg_param = params
300 .next()
301 .expect("#[update] function must have message type as third parameter");
302 let (msg_name, msg_type) =
303 extract_param_info(msg_param).expect("Failed to extract message parameter info");
304
305 // Position 3: State type (optional)
306 let state_info = params.next().and_then(extract_param_info);
307
308 // Check if we have topic arguments
309 if args.is_empty() {
310 // Simple case: no topics specified
311 // Generate state fetching code if state parameter exists
312 let state_setup = if let Some((state_name, state_type)) = &state_info {
313 quote! { let mut #state_name = #ctx_name.get_state::<#state_type>(); }
314 } else {
315 quote! {}
316 };
317
318 let expanded = quote! {
319 #fn_vis fn __component_update_impl(&self, #ctx_name: &rxtui::Context, msg: Box<dyn rxtui::Message>, _topic: Option<&str>) -> rxtui::Action {
320 if let Some(#msg_name) = msg.downcast::<#msg_type>() {
321 #state_setup
322 let #msg_name = #msg_name.clone();
323 return #fn_block;
324 }
325
326 rxtui::Action::None
327 }
328 };
329
330 TokenStream::from(expanded)
331 } else {
332 // Complex case: with topics
333 let args = parse_macro_input!(args as UpdateArgs);
334
335 // Use provided msg type or fall back to first positional arg
336 let regular_type = args.msg_type.unwrap_or(msg_type.clone());
337
338 // Generate enum name from the message parameter type
339 let enum_name = &msg_type;
340
341 // Generate enum variants
342 let mut enum_variants = vec![];
343 let regular_variant =
344 format_ident!("{}", quote!(#regular_type).to_string().replace("::", "_"));
345 enum_variants.push(quote! { #regular_variant(#regular_type) });
346
347 // Generate topic handling code
348 let mut topic_matches = vec![];
349 for topic in &args.topics {
350 let topic_type = &topic.msg_type;
351 let variant_name =
352 format_ident!("{}", quote!(#topic_type).to_string().replace("::", "_"));
353
354 enum_variants.push(quote! { #variant_name(#topic_type) });
355
356 let topic_check = match &topic.key {
357 TopicKey::Static(lit_str) => {
358 quote! { topic == #lit_str }
359 }
360 TopicKey::Dynamic(expr) => {
361 // Use &* to convert String to &str
362 quote! { topic == &*(#expr) }
363 }
364 };
365
366 topic_matches.push(quote! {
367 if #topic_check {
368 if let Some(msg) = msg.downcast::<#topic_type>() {
369 let #msg_name = #enum_name::#variant_name(msg.clone());
370 return #fn_block;
371 }
372 }
373 });
374 }
375
376 // Generate state setup
377 let state_setup = if let Some((state_name, state_type)) = &state_info {
378 quote! { let mut #state_name = #ctx_name.get_state::<#state_type>(); }
379 } else {
380 quote! {}
381 };
382
383 // Generate the complete function
384 let expanded = quote! {
385 #fn_vis fn __component_update_impl(&self, #ctx_name: &rxtui::Context, msg: Box<dyn rxtui::Message>, topic: Option<&str>) -> rxtui::Action {
386 // Generate the enum for message types
387 #[allow(non_camel_case_types)]
388 enum #enum_name {
389 #(#enum_variants),*
390 }
391
392 #state_setup
393
394 // Handle topic messages first
395 if let Some(topic) = topic {
396 #(#topic_matches)*
397 return rxtui::Action::None;
398 }
399
400 // Handle regular message
401 if let Some(msg) = msg.downcast::<#regular_type>() {
402 let #msg_name = #enum_name::#regular_variant(msg.clone());
403 return #fn_block;
404 }
405
406 rxtui::Action::None
407 }
408 };
409
410 TokenStream::from(expanded)
411 }
412}
413
414/// Simplifies component view methods by automatically fetching state from the context.
415///
416/// # With state
417///
418/// If you include a state parameter, it will be automatically fetched:
419///
420/// ```ignore
421/// #[view]
422/// fn view(&self, ctx: &Context, state: CounterState) -> Node {
423/// node! {
424/// div [
425/// text(format!("Count: {}", state.count))
426/// ]
427/// }
428/// }
429/// ```
430///
431/// # Without state
432///
433/// For stateless components, just omit the state parameter:
434///
435/// ```ignore
436/// #[view]
437/// fn view(&self, ctx: &Context) -> Node {
438/// node! {
439/// div [
440/// text("Static content")
441/// ]
442/// }
443/// }
444/// ```
445///
446/// The macro automatically detects whether a state parameter is present and generates
447/// the appropriate code to fetch it from the context.
448///
449/// # Parameters
450///
451/// The function parameters are detected by position:
452/// - `&self` (required)
453/// - `&Context` (required) - any name allowed
454/// - State type (optional) - any name allowed
455#[proc_macro_attribute]
456pub fn view(_args: TokenStream, input: TokenStream) -> TokenStream {
457 let input_fn = parse_macro_input!(input as ItemFn);
458
459 let _fn_name = &input_fn.sig.ident;
460 let fn_vis = &input_fn.vis;
461 let fn_block = &input_fn.block;
462
463 // Parse function parameters by position
464 let mut params = input_fn.sig.inputs.iter();
465
466 // Position 0: &self (skip it)
467 params
468 .next()
469 .expect("#[view] function must have &self as first parameter");
470
471 // Position 1: &Context
472 let ctx_param = params
473 .next()
474 .expect("#[view] function must have &Context as second parameter");
475 let (ctx_name, _ctx_type) =
476 extract_param_info(ctx_param).expect("Failed to extract context parameter info");
477
478 // Position 2: State type (optional)
479 if let Some(state_param) = params.next() {
480 let (state_name, state_type) =
481 extract_param_info(state_param).expect("Failed to extract state parameter info");
482
483 // Generate with state fetching
484 let expanded = quote! {
485 #fn_vis fn __component_view_impl(&self, #ctx_name: &rxtui::Context) -> rxtui::Node {
486 let #state_name = #ctx_name.get_state::<#state_type>();
487 #fn_block
488 }
489 };
490
491 TokenStream::from(expanded)
492 } else {
493 // No state parameter - just forward as-is
494 let expanded = quote! {
495 #fn_vis fn __component_view_impl(&self, #ctx_name: &rxtui::Context) -> rxtui::Node {
496 #fn_block
497 }
498 };
499
500 TokenStream::from(expanded)
501 }
502}
503
504/// Marks an async method as a single effect that runs in the background.
505///
506/// # Basic usage
507///
508/// Define an async effect that runs in the background:
509///
510/// ```ignore
511/// #[effect]
512/// async fn timer_effect(&self, ctx: &Context) {
513/// loop {
514/// tokio::time::sleep(Duration::from_secs(1)).await;
515/// ctx.send(Msg::Tick);
516/// }
517/// }
518/// ```
519///
520/// # With state
521///
522/// Effects can access component state:
523///
524/// ```ignore
525/// #[effect]
526/// async fn fetch_data(&self, ctx: &Context, state: MyState) {
527/// let url = &state.api_url;
528/// let data = fetch(url).await;
529/// ctx.send(Msg::DataLoaded(data));
530/// }
531/// ```
532///
533/// # Multiple effects
534///
535/// You can define multiple effects on a component - they will all be collected
536/// into a single `effects()` method:
537///
538/// ```ignore
539/// impl MyComponent {
540/// #[effect]
541/// async fn timer(&self, ctx: &Context) {
542/// // Timer logic
543/// }
544///
545/// #[effect]
546/// async fn websocket(&self, ctx: &Context) {
547/// // WebSocket logic
548/// }
549/// }
550/// ```
551///
552/// # Parameters
553///
554/// The function parameters are detected by position:
555/// - `&self` (required)
556/// - `&Context` (required) - any name allowed
557/// - State type (optional) - any name allowed
558///
559/// Note: Use the #[component] macro on the impl block to automatically collect
560/// all methods marked with #[effect] into the effects() method.
561#[proc_macro_attribute]
562pub fn effect(_args: TokenStream, input: TokenStream) -> TokenStream {
563 let input_fn = parse_macro_input!(input as ItemFn);
564
565 let fn_name = &input_fn.sig.ident;
566 let fn_vis = &input_fn.vis;
567 let fn_block = &input_fn.block;
568
569 // Parse function parameters by position
570 let mut params = input_fn.sig.inputs.iter();
571
572 // Position 0: &self (skip it)
573 params
574 .next()
575 .expect("#[effects] function must have &self as first parameter");
576
577 // Position 1: &Context
578 let ctx_param = params
579 .next()
580 .expect("#[effects] function must have &Context as second parameter");
581 let (ctx_name, _ctx_type) =
582 extract_param_info(ctx_param).expect("Failed to extract context parameter info");
583
584 // Position 2: State type (optional)
585 let state_setup = if let Some(state_param) = params.next() {
586 let (state_name, state_type) =
587 extract_param_info(state_param).expect("Failed to extract state parameter info");
588 quote! { let #state_name = #ctx_name.get_state::<#state_type>(); }
589 } else {
590 quote! {}
591 };
592
593 // Generate a helper method that creates the effect
594 let helper_name = format_ident!("__{}_effect", fn_name);
595
596 let expanded = quote! {
597 #[allow(dead_code)]
598 #fn_vis fn #helper_name(&self, #ctx_name: &rxtui::Context) -> rxtui::effect::Effect {
599 Box::pin({
600 let #ctx_name = #ctx_name.clone();
601 #state_setup
602 async move #fn_block
603 })
604 }
605
606 // Keep the original async function for reference/testing if needed
607 #[allow(dead_code)]
608 #fn_vis async fn #fn_name(&self, #ctx_name: &rxtui::Context) #fn_block
609 };
610
611 TokenStream::from(expanded)
612}
613
614/// Impl-level macro that automatically handles Component trait boilerplate.
615///
616/// This macro processes an impl block and:
617/// 1. Collects all methods marked with `#[effect]`
618/// 2. Generates helper methods for each effect
619/// 3. Automatically creates the `effects()` method
620///
621/// # Example
622///
623/// ```ignore
624/// #[component]
625/// impl MyComponent {
626/// #[update]
627/// fn update(&self, ctx: &Context, msg: Msg, mut state: State) -> Action {
628/// // update logic
629/// }
630///
631/// #[view]
632/// fn view(&self, ctx: &Context, state: State) -> Node {
633/// // view logic
634/// }
635///
636/// #[effect]
637/// async fn timer(&self, ctx: &Context) {
638/// // async effect logic
639/// }
640/// }
641/// ```
642///
643/// The macro will automatically generate the `effects()` method that collects
644/// all methods marked with `#[effect]`.
645#[proc_macro_attribute]
646pub fn component(_args: TokenStream, input: TokenStream) -> TokenStream {
647 let mut impl_block = parse_macro_input!(input as ItemImpl);
648
649 // Find all methods marked with #[effect]
650 let mut effect_methods = Vec::new();
651 let mut processed_items = Vec::new();
652
653 for item in impl_block.items.drain(..) {
654 if let ImplItem::Fn(mut method) = item {
655 // Check if this method has the #[effect] attribute
656 let has_effect_attr = method
657 .attrs
658 .iter()
659 .any(|attr| attr.path().is_ident("effect"));
660
661 if has_effect_attr {
662 // Remove the #[effect] attribute
663 method.attrs.retain(|attr| !attr.path().is_ident("effect"));
664
665 let method_name = &method.sig.ident;
666 let helper_name = format_ident!("__{}_effect", method_name);
667
668 // Parse parameters
669 let mut params = method.sig.inputs.iter();
670
671 // Skip &self
672 params.next();
673
674 // Get context parameter
675 let ctx_param = params.next();
676 let ctx_name = if let Some(FnArg::Typed(PatType { pat, .. })) = ctx_param {
677 if let Pat::Ident(pat_ident) = &**pat {
678 &pat_ident.ident
679 } else {
680 panic!("Expected context parameter");
681 }
682 } else {
683 panic!("Expected context parameter");
684 };
685
686 // Check for state parameter
687 let state_setup = if let Some(FnArg::Typed(PatType { pat, ty, .. })) = params.next()
688 {
689 if let Pat::Ident(pat_ident) = &**pat {
690 let state_name = &pat_ident.ident;
691 let state_type = &**ty;
692 quote! { let #state_name = #ctx_name.get_state::<#state_type>(); }
693 } else {
694 quote! {}
695 }
696 } else {
697 quote! {}
698 };
699
700 let method_block = &method.block;
701
702 // Generate helper method
703 let helper_method = quote! {
704 #[allow(dead_code)]
705 fn #helper_name(&self, #ctx_name: &rxtui::Context) -> rxtui::effect::Effect {
706 Box::pin({
707 let #ctx_name = #ctx_name.clone();
708 #state_setup
709 async move #method_block
710 })
711 }
712 };
713
714 // Store effect method info for later
715 effect_methods.push((helper_name, ctx_name.clone()));
716
717 // Add both the helper and original method
718 let helper_item: ImplItem = syn::parse2(helper_method).unwrap();
719 processed_items.push(helper_item);
720
721 // Add #[allow(dead_code)] to the original async method
722 method.attrs.push(syn::parse_quote! { #[allow(dead_code)] });
723 processed_items.push(ImplItem::Fn(method));
724 } else {
725 processed_items.push(ImplItem::Fn(method));
726 }
727 } else {
728 processed_items.push(item);
729 }
730 }
731
732 // Add all processed items back
733 impl_block.items = processed_items;
734
735 // Always generate effects() method - either with collected effects or empty vec
736 // If rxtui is compiled without effects, the Effect type won't exist and compilation will fail
737 // This is the correct behavior - using effects without the feature should be a compile error
738 let effects_method = if !effect_methods.is_empty() {
739 let effect_calls = effect_methods
740 .iter()
741 .map(|(helper_name, _)| {
742 quote! { self.#helper_name(ctx) }
743 })
744 .collect::<Vec<_>>();
745
746 quote! {
747 // Generated method that shadows the EffectsProvider trait method
748 // This will be called by Component::effects() through method resolution
749 fn __component_effects_impl(&self, ctx: &rxtui::Context) -> Vec<rxtui::effect::Effect> {
750 vec![#(#effect_calls),*]
751 }
752 }
753 } else {
754 quote! {
755 // No effects defined, but still generate the method to shadow the trait
756 // This ensures consistent behavior whether effects are present or not
757 fn __component_effects_impl(&self, _ctx: &rxtui::Context) -> Vec<rxtui::effect::Effect> {
758 vec![]
759 }
760 }
761 };
762
763 let effects_item: ImplItem = syn::parse2(effects_method).unwrap();
764 impl_block.items.push(effects_item);
765
766 // Just return the impl block with the effects method
767 TokenStream::from(quote! { #impl_block })
768}