raui_derive/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{
6    DeriveInput, FnArg, Ident, ItemFn, Pat, PatIdent, Path, Result, Token, Type, TypePath,
7    TypeReference,
8    parse::{Parse, ParseStream},
9    parse_macro_input, parse_str,
10    punctuated::Punctuated,
11};
12
13#[derive(Debug, Clone)]
14struct IdentList {
15    values: Punctuated<Ident, Token![,]>,
16}
17
18impl Parse for IdentList {
19    fn parse(input: ParseStream) -> Result<Self> {
20        Ok(Self {
21            values: input.parse_terminated(Ident::parse)?,
22        })
23    }
24}
25
26fn unpack_context(ty: &Type, pat: &Pat) -> Option<Ident> {
27    match ty {
28        Type::Path(TypePath { path, .. }) => {
29            if let Some(segment) = path.segments.iter().next_back()
30                && segment.ident == "WidgetContext"
31                && let Pat::Ident(PatIdent { ident, .. }) = pat
32            {
33                return Some(ident.to_owned());
34            }
35        }
36        Type::Reference(TypeReference { elem, .. }) => {
37            return unpack_context(elem, pat);
38        }
39        _ => {}
40    }
41    None
42}
43
44fn is_arg_context(arg: &FnArg) -> Option<Ident> {
45    if let FnArg::Typed(pat) = arg {
46        unpack_context(&pat.ty, &pat.pat)
47    } else {
48        None
49    }
50}
51
52// The links won't be broken when built in the context of the `raui` crate
53/// An attribute macro that allows you to add hooks that will execute before your component body
54///
55/// > **See Also:** [`macro@post_hooks`] for an alternative that runs _after_ your component body
56///
57/// Hooks allow you to create reusable logic that can be applied to multiple components.
58#[proc_macro_attribute]
59pub fn pre_hooks(attr: TokenStream, input: TokenStream) -> TokenStream {
60    let ItemFn {
61        attrs,
62        vis,
63        sig,
64        block,
65    } = parse_macro_input!(input as ItemFn);
66    let context = sig
67        .inputs
68        .iter()
69        .find_map(is_arg_context)
70        .unwrap_or_else(|| panic!("Could not find function context argument!"));
71    let list = parse_macro_input!(attr as IdentList);
72    let hooks = list
73        .values
74        .into_iter()
75        .map(|v| quote! { #context.use_hook(#v); });
76
77    let tokens = quote! {
78        #(#attrs)*
79        #vis #sig {
80            #({#hooks})*
81            #block
82        }
83    };
84    tokens.into()
85}
86
87/// Allows you to execute re-usable logic after your component body
88///
89/// See [`macro@pre_hooks`]
90#[proc_macro_attribute]
91pub fn post_hooks(attr: TokenStream, input: TokenStream) -> TokenStream {
92    let ItemFn {
93        attrs,
94        vis,
95        sig,
96        block,
97    } = parse_macro_input!(input as ItemFn);
98    let context = sig
99        .inputs
100        .iter()
101        .find_map(is_arg_context)
102        .unwrap_or_else(|| panic!("Could not find function context argument!"));
103    let list = parse_macro_input!(attr as IdentList);
104    let hooks = list
105        .values
106        .into_iter()
107        .map(|v| quote! { #context.use_hook(#v); });
108
109    let tokens = quote! {
110        #(#attrs)*
111        #vis #sig {
112            let result = {
113                #block
114            };
115            #({#hooks})*
116            result
117        }
118    };
119    tokens.into()
120}
121
122// The links won't be broken when built in the context of the `raui` crate
123/// Derive macro for the [`PropsData`][raui_core::props::PropsData] trait
124///
125/// # Example
126///
127/// ```ignore
128/// #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)]
129/// #[props_data(crate::props::PropsData)]
130/// #[prefab(crate::Prefab)]
131/// pub struct ButtonProps {
132///     #[serde(default)]
133///     pub selected: bool,
134///     #[serde(default)]
135///     pub trigger: bool,
136///     #[serde(default)]
137///     pub context: bool,
138///     #[serde(default)]
139///     pub pointer: Vec2,
140/// }
141/// ```
142#[proc_macro_derive(PropsData, attributes(remote, props_data, prefab))]
143pub fn derive_props(input: TokenStream) -> TokenStream {
144    let DeriveInput { ident, attrs, .. } = parse_macro_input!(input as DeriveInput);
145
146    let mut path = Path::from(ident);
147    let mut props_data = parse_str::<Path>("PropsData").unwrap();
148    let mut prefab = parse_str::<Path>("Prefab").unwrap();
149    for attr in attrs {
150        if let Some(ident) = attr.path.get_ident() {
151            if ident == "remote" {
152                path = attr.parse_args::<Path>().unwrap();
153            } else if ident == "props_data" {
154                props_data = attr.parse_args::<Path>().unwrap();
155            } else if ident == "prefab" {
156                prefab = attr.parse_args::<Path>().unwrap();
157            }
158        }
159    }
160
161    let tokens = quote! {
162        impl #props_data for #path
163        where
164            Self: Clone,
165        {
166            fn clone_props(&self) -> Box<dyn #props_data> {
167                Box::new(self.clone())
168            }
169
170            fn as_any(&self) -> &dyn std::any::Any {
171                self
172            }
173        }
174
175        impl #prefab for #path {}
176    };
177    tokens.into()
178}
179
180// The links won't be broken when built in the context of the `raui` crate
181/// Derive macro for the [`MessageData`][raui_core::messenger::MessageData] trait
182///
183/// # Example
184///
185/// ```ignore
186/// #[derive(MessageData, Debug, Clone)]
187/// pub enum AppMessage {
188///     ShowPopup(usize),
189///     ClosePopup,
190/// }
191/// ```
192#[proc_macro_derive(MessageData, attributes(remote, message_data))]
193pub fn derive_message(input: TokenStream) -> TokenStream {
194    let DeriveInput { ident, attrs, .. } = parse_macro_input!(input as DeriveInput);
195
196    let mut path = Path::from(ident);
197    let mut message_data = parse_str::<Path>("MessageData").unwrap();
198    for attr in attrs {
199        if let Some(ident) = attr.path.get_ident() {
200            if ident == "remote" {
201                path = attr.parse_args::<Path>().unwrap();
202            } else if ident == "message_data" {
203                message_data = attr.parse_args::<Path>().unwrap();
204            }
205        }
206    }
207
208    let tokens = quote! {
209        impl #message_data for #path
210        where
211            Self: Clone,
212        {
213            fn clone_message(&self) -> Box<dyn #message_data> {
214                Box::new(self.clone())
215            }
216
217            fn as_any(&self) -> &dyn std::any::Any {
218                self
219            }
220        }
221    };
222    tokens.into()
223}