raui_derive/
lib.rs

1extern crate proc_macro;
2use proc_macro::TokenStream;
3use quote::quote;
4use syn::{
5    parse::{Parse, ParseStream},
6    parse_macro_input, parse_str,
7    punctuated::Punctuated,
8    DeriveInput, FnArg, Ident, ItemFn, Pat, PatIdent, Path, Result, Token, Type, TypePath,
9    TypeReference,
10};
11
12#[derive(Debug, Clone)]
13struct IdentList {
14    values: Punctuated<Ident, Token![,]>,
15}
16
17impl Parse for IdentList {
18    fn parse(input: ParseStream) -> Result<Self> {
19        Ok(Self {
20            values: input.parse_terminated(Ident::parse)?,
21        })
22    }
23}
24
25fn unpack_context(ty: &Type, pat: &Pat) -> Option<Ident> {
26    match ty {
27        Type::Path(TypePath { path, .. }) => {
28            if let Some(segment) = path.segments.iter().last() {
29                if segment.ident == "WidgetContext" {
30                    if let Pat::Ident(PatIdent { ident, .. }) = pat {
31                        return Some(ident.to_owned());
32                    }
33                }
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///
59/// # Usage Example
60///
61/// ```rust,ignore
62/// # use raui::prelude::*;
63/// #[pre_hooks(use_button_notified_state)]
64/// pub fn image_button(mut context: WidgetContext) -> WidgetNode {
65///    // Do stuff and potentially use state added by the `use_button_notified_state` hook
66/// }
67/// ```
68///
69/// # Creating a Hook
70///
71/// Hooks are simply functions that take a mutable reference to the component's
72/// [`WidgetContext`][raui_core::widget::context::WidgetContext].
73///
74/// ```ignore
75/// pub fn use_button_notified_state(context: &mut WidgetContext) {
76///     // hook into the lifecycle of whatever widget this hook is applied to
77///     context.life_cycle.change(|context| {
78///         for msg in context.messenger.messages {
79///             // listen for button messages
80///             if let Some(msg) = msg.as_any().downcast_ref::<ButtonNotifyMessage>() {
81///                 // And modify the context state with the button info
82///                 let _ = context.state.write_with(msg.state);
83///             }
84///         }
85///     });
86/// }
87/// ```
88#[proc_macro_attribute]
89pub fn pre_hooks(attr: TokenStream, input: TokenStream) -> TokenStream {
90    let ItemFn {
91        attrs,
92        vis,
93        sig,
94        block,
95    } = parse_macro_input!(input as ItemFn);
96    let context = sig
97        .inputs
98        .iter()
99        .find_map(is_arg_context)
100        .unwrap_or_else(|| panic!("Could not find function context argument!"));
101    let list = parse_macro_input!(attr as IdentList);
102    let hooks = list
103        .values
104        .into_iter()
105        .map(|v| quote! { #context.use_hook(#v); });
106
107    let tokens = quote! {
108        #(#attrs)*
109        #vis #sig {
110            #({#hooks})*
111            #block
112        }
113    };
114    tokens.into()
115}
116
117/// Allows you to execute re-usable logic after your component body
118///
119/// See [`macro@pre_hooks`]
120#[proc_macro_attribute]
121pub fn post_hooks(attr: TokenStream, input: TokenStream) -> TokenStream {
122    let ItemFn {
123        attrs,
124        vis,
125        sig,
126        block,
127    } = parse_macro_input!(input as ItemFn);
128    let context = sig
129        .inputs
130        .iter()
131        .find_map(is_arg_context)
132        .unwrap_or_else(|| panic!("Could not find function context argument!"));
133    let list = parse_macro_input!(attr as IdentList);
134    let hooks = list
135        .values
136        .into_iter()
137        .map(|v| quote! { #context.use_hook(#v); });
138
139    let tokens = quote! {
140        #(#attrs)*
141        #vis #sig {
142            let result = {
143                #block
144            };
145            #({#hooks})*
146            result
147        }
148    };
149    tokens.into()
150}
151
152// The links won't be broken when built in the context of the `raui` crate
153/// Derive macro for the [`PropsData`][raui_core::props::PropsData] trait
154///
155/// # Example
156///
157/// ```ignore
158/// #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)]
159/// #[props_data(crate::props::PropsData)]
160/// #[prefab(crate::Prefab)]
161/// pub struct ButtonProps {
162///     #[serde(default)]
163///     pub selected: bool,
164///     #[serde(default)]
165///     pub trigger: bool,
166///     #[serde(default)]
167///     pub context: bool,
168///     #[serde(default)]
169///     pub pointer: Vec2,
170/// }
171/// ```
172#[proc_macro_derive(PropsData, attributes(remote, props_data, prefab))]
173pub fn derive_props(input: TokenStream) -> TokenStream {
174    let DeriveInput { ident, attrs, .. } = parse_macro_input!(input as DeriveInput);
175
176    let mut path = Path::from(ident);
177    let mut props_data = parse_str::<Path>("PropsData").unwrap();
178    let mut prefab = parse_str::<Path>("Prefab").unwrap();
179    for attr in attrs {
180        if let Some(ident) = attr.path.get_ident() {
181            if ident == "remote" {
182                path = attr.parse_args::<Path>().unwrap();
183            } else if ident == "props_data" {
184                props_data = attr.parse_args::<Path>().unwrap();
185            } else if ident == "prefab" {
186                prefab = attr.parse_args::<Path>().unwrap();
187            }
188        }
189    }
190
191    let tokens = quote! {
192        impl #props_data for #path
193        where
194            Self: Clone,
195        {
196            fn clone_props(&self) -> Box<dyn #props_data> {
197                Box::new(self.clone())
198            }
199
200            fn as_any(&self) -> &dyn std::any::Any {
201                self
202            }
203        }
204
205        impl #prefab for #path {}
206    };
207    tokens.into()
208}
209
210// The links won't be broken when built in the context of the `raui` crate
211/// Derive macro for the [`MessageData`][raui_core::messenger::MessageData] trait
212///
213/// # Example
214///
215/// ```ignore
216/// #[derive(MessageData, Debug, Clone)]
217/// pub enum AppMessage {
218///     ShowPopup(usize),
219///     ClosePopup,
220/// }
221/// ```
222#[proc_macro_derive(MessageData, attributes(remote, message_data))]
223pub fn derive_message(input: TokenStream) -> TokenStream {
224    let DeriveInput { ident, attrs, .. } = parse_macro_input!(input as DeriveInput);
225
226    let mut path = Path::from(ident);
227    let mut message_data = parse_str::<Path>("MessageData").unwrap();
228    for attr in attrs {
229        if let Some(ident) = attr.path.get_ident() {
230            if ident == "remote" {
231                path = attr.parse_args::<Path>().unwrap();
232            } else if ident == "message_data" {
233                message_data = attr.parse_args::<Path>().unwrap();
234            }
235        }
236    }
237
238    let tokens = quote! {
239        impl #message_data for #path
240        where
241            Self: Clone,
242        {
243            fn clone_message(&self) -> Box<dyn #message_data> {
244                Box::new(self.clone())
245            }
246
247            fn as_any(&self) -> &dyn std::any::Any {
248                self
249            }
250        }
251    };
252    tokens.into()
253}