feather_macro/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 Fundament Research Institute <https://fundament.institute>
3
4use core::panic;
5
6use proc_macro::TokenStream;
7use proc_macro2::Span;
8use quote::{format_ident, quote};
9use syn::{Data, DataEnum, DeriveInput, Meta, parse_macro_input};
10
11fn derive_base_prop(input: TokenStream, prop: &str, source: &str, result: &str) -> TokenStream {
12    let ast = parse_macro_input!(input as DeriveInput);
13
14    let result: syn::Path = syn::parse_str(result).unwrap();
15    let source: syn::Path = syn::parse_str(source).unwrap();
16    let prop = format_ident!("{}", prop);
17    let name = ast.ident;
18    quote! {
19        impl #source for #name {
20            fn #prop(&self) -> &#result {
21                &self.#prop
22            }
23        }
24    }
25    .into()
26}
27
28#[proc_macro_derive(Empty)]
29pub fn derive_empty(input: TokenStream) -> TokenStream {
30    let ast = parse_macro_input!(input as DeriveInput);
31
32    let sname = ast.ident;
33    quote! {
34        impl feather_ui::layout::base::Empty for #sname {}
35    }
36    .into()
37}
38
39#[proc_macro_derive(Area)]
40pub fn derive_area(input: TokenStream) -> TokenStream {
41    derive_base_prop(
42        input,
43        "area",
44        "feather_ui::layout::base::Area",
45        "feather_ui::DRect",
46    )
47}
48
49#[proc_macro_derive(Padding)]
50pub fn derive_padding(input: TokenStream) -> TokenStream {
51    derive_base_prop(
52        input,
53        "padding",
54        "feather_ui::layout::base::Padding",
55        "feather_ui::DAbsRect",
56    )
57}
58
59#[proc_macro_derive(Margin)]
60pub fn derive_margin(input: TokenStream) -> TokenStream {
61    derive_base_prop(
62        input,
63        "margin",
64        "feather_ui::layout::base::Margin",
65        "feather_ui::DRect",
66    )
67}
68
69#[proc_macro_derive(Limits)]
70pub fn derive_limits(input: TokenStream) -> TokenStream {
71    derive_base_prop(
72        input,
73        "limits",
74        "feather_ui::layout::base::Limits",
75        "feather_ui::DLimits",
76    )
77}
78
79#[proc_macro_derive(RLimits)]
80pub fn derive_rlimits(input: TokenStream) -> TokenStream {
81    derive_base_prop(
82        input,
83        "rlimits",
84        "feather_ui::layout::base::RLimits",
85        "feather_ui::RelLimits",
86    )
87}
88
89#[proc_macro_derive(Anchor)]
90pub fn derive_anchor(input: TokenStream) -> TokenStream {
91    derive_base_prop(
92        input,
93        "anchor",
94        "feather_ui::layout::base::Anchor",
95        "feather_ui::DPoint",
96    )
97}
98
99#[proc_macro_derive(TextEdit)]
100pub fn derive_textedit(input: TokenStream) -> TokenStream {
101    derive_base_prop(
102        input,
103        "textedit",
104        "feather_ui::layout::base::TextEdit",
105        "feather_ui::text::EditView",
106    )
107}
108
109#[proc_macro_derive(FlexProp)]
110pub fn derive_flex_prop(input: TokenStream) -> TokenStream {
111    let ast = parse_macro_input!(input as DeriveInput);
112
113    let name = ast.ident;
114    quote! {
115        impl feather_ui::layout::flex::Prop for #name {
116        fn wrap(&self) -> bool { self.wrap }
117        fn justify(&self) -> feather_ui::layout::flex::FlexJustify { self.justify }
118        fn align(&self) -> feather_ui::layout::flex::FlexJustify { self.align }
119        }
120    }
121    .into()
122}
123
124#[proc_macro_derive(FlexChild)]
125pub fn derive_flex_child(input: TokenStream) -> TokenStream {
126    let ast = parse_macro_input!(input as DeriveInput);
127
128    let name = ast.ident;
129    quote! {
130        impl feather_ui::layout::flex::Child for #name {
131            fn grow(&self) -> f32 { self.grow }
132            fn shrink(&self) -> f32 { self.shrink }
133            fn basis(&self) -> feather_ui::DValue { self.basis }
134        }
135    }
136    .into()
137}
138
139#[proc_macro_derive(ZIndex)]
140pub fn derive_zindex(input: TokenStream) -> TokenStream {
141    let ast = parse_macro_input!(input as DeriveInput);
142
143    let sname = ast.ident;
144    quote! {
145        impl feather_ui::layout::base::ZIndex for #sname {
146            fn zindex(&self) -> i32 {
147                self.zindex
148            }
149        }
150    }
151    .into()
152}
153
154#[proc_macro_derive(Direction)]
155pub fn derive_direction(input: TokenStream) -> TokenStream {
156    let ast = parse_macro_input!(input as DeriveInput);
157
158    let sname = ast.ident;
159    quote! {
160        impl feather_ui::layout::base::Direction for #sname {
161            fn direction(&self) -> feather_ui::RowDirection {
162                self.direction
163            }
164        }
165    }
166    .into()
167}
168
169#[proc_macro_derive(RootProp)]
170pub fn derive_root_prop(input: TokenStream) -> TokenStream {
171    derive_base_prop(
172        input,
173        "dim",
174        "feather_ui::layout::root::Prop",
175        "feather_ui::AbsDim",
176    )
177}
178
179fn data_enum(ast: &DeriveInput) -> &DataEnum {
180    if let Data::Enum(data_enum) = &ast.data {
181        data_enum
182    } else {
183        panic!("`Dispatch` derive can only be used on an enum.");
184    }
185}
186
187fn find_enum_module(attrs: &[syn::Attribute]) -> syn::Result<String> {
188    // Extract EnumVariantType's module, since this has to be used in conjunction with our derive
189    for attr in attrs.iter() {
190        if attr.path().is_ident("evt") {
191            let nested = attr
192                .parse_args_with(
193                    syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
194                )
195                .unwrap();
196
197            for meta in nested {
198                if let Meta::NameValue(name_value) = meta {
199                    if let (true, syn::Expr::Lit(lit_str)) =
200                        (name_value.path.is_ident("module"), name_value.value)
201                    {
202                        if let syn::Lit::Str(s) = lit_str.lit {
203                            return Ok(s.value());
204                        } else {
205                            return Err(syn::Error::new(Span::call_site(), ""));
206                        }
207                    } else {
208                        return Err(syn::Error::new(Span::call_site(), ""));
209                    }
210                }
211            }
212
213            // This would be a lot easier but it doesn't seem to work for #[evt(derive(Clone), module = "mouse_area_event")]
214            /*let _ = attr.parse_nested_meta(|meta| {
215                if meta.path.is_ident("module") {
216                    let value = meta.value()?;
217                    let s: LitStr = value.parse()?;
218                    enum_module = Some(s.value());
219                }
220
221                Ok(())
222            });*/
223        }
224    }
225
226    // Error here doesn't matter, we transform it into another error message upon return
227    Err(syn::Error::new(Span::call_site(), ""))
228}
229
230#[proc_macro_derive(Dispatch)]
231pub fn dispatchable(input: TokenStream) -> TokenStream {
232    let crate_name = std::env::var("CARGO_PKG_NAME").unwrap();
233
234    let crate_name = format_ident!(
235        "{}",
236        if crate_name == "feather-ui" {
237            "crate"
238        } else {
239            "feather_ui"
240        }
241    );
242
243    let ast = parse_macro_input!(input as DeriveInput);
244    let enum_module = format_ident!(
245        "{}",
246        find_enum_module(&ast.attrs).expect(
247        "Expected `evt` attribute argument in the form: `#[evt(module = \"some_module_name\")]`",
248    ));
249
250    let enum_name = &ast.ident;
251    let data_enum = data_enum(&ast);
252    let variants = &data_enum.variants;
253
254    let mut extract_declarations = proc_macro2::TokenStream::new();
255    let mut restore_declarations = proc_macro2::TokenStream::new();
256
257    for (counter, variant) in variants.iter().enumerate() {
258        let variant_name = &variant.ident;
259
260        let idx = (1_u64)
261            .checked_shl(counter as u32)
262            .expect("Too many variants! Can't handle more than 64!");
263
264        if variant.fields.is_empty() {
265            extract_declarations.extend(quote! {
266                #enum_name::#variant_name => (
267                    #idx,
268                    Box::new(#enum_module::#variant_name::try_from(self).unwrap()),
269                ),
270            });
271        } else if variant.fields.iter().next().unwrap().ident.is_none() {
272            let underscores = variant.fields.iter().map(|_| format_ident!("_"));
273            extract_declarations.extend(quote! {
274                #enum_name::#variant_name(#(#underscores),*) => (
275                    #idx,
276                    Box::new(#enum_module::#variant_name::try_from(self).unwrap()),
277                ),
278            });
279        } else {
280            extract_declarations.extend(quote! {
281                #enum_name::#variant_name { .. } => (
282                    #idx,
283                    Box::new(#enum_module::#variant_name::try_from(self).unwrap()),
284                ),
285            });
286        }
287
288        restore_declarations.extend(quote! {
289            #idx => Ok(#enum_name::from(
290                *pair
291                    .1
292                    .downcast::<#enum_module::#variant_name>()
293                    .map_err(|_| {
294                        #crate_name::Error::MismatchedEnumTag(
295                            pair.0,
296                            std::any::TypeId::of::<#enum_module::#variant_name>(),
297                            typeid,
298                        )
299                    })?,
300            )),
301        });
302    }
303
304    let counter = variants.len();
305    quote! {
306        impl #crate_name::Dispatchable for #enum_name {
307            const SIZE: usize = #counter;
308
309            fn extract(self) -> #crate_name::DispatchPair {
310                match self {
311                    #extract_declarations
312                }
313            }
314
315            fn restore(pair: #crate_name::DispatchPair) -> Result<Self, #crate_name::Error> {
316                let typeid = (*pair.1).type_id();
317                match pair.0 {
318                    #restore_declarations
319                    _ => Err(#crate_name::Error::InvalidEnumTag(pair.0)),
320                }
321            }
322        }
323    }
324    .into()
325}
326
327#[proc_macro_derive(StateMachineChild)]
328pub fn state_machine_child(input: TokenStream) -> TokenStream {
329    let crate_name = std::env::var("CARGO_PKG_NAME").unwrap();
330
331    let crate_name = format_ident!(
332        "{}",
333        if crate_name == "feather-ui" {
334            "crate"
335        } else {
336            "feather_ui"
337        }
338    );
339
340    let ast = parse_macro_input!(input as DeriveInput);
341    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
342
343    let data = if let Data::Struct(data_enum) = &ast.data {
344        data_enum
345    } else {
346        panic!("`StateMachineChild` derive can only be used on a struct.");
347    };
348
349    let has_children = data.fields.members().any(|x| {
350        if let syn::Member::Named(f) = x {
351            f == "children"
352        } else {
353            false
354        }
355    });
356
357    let apply_children = if has_children {
358        quote! {
359            fn apply_children(
360                    &self,
361                    f: &mut dyn FnMut(&dyn #crate_name::StateMachineChild) -> eyre::Result<()>,
362                ) -> eyre::Result<()> {
363                    self.children
364                        .iter()
365                        .try_for_each(|x| f(x.as_ref().unwrap().as_ref()))
366                }
367        }
368    } else {
369        quote! {}
370    };
371
372    let sname = ast.ident;
373    quote! {
374        impl #impl_generics #crate_name::StateMachineChild for #sname #ty_generics #where_clause {
375            fn id(&self) -> std::sync::Arc<SourceID> {
376                self.id.clone()
377            }
378
379            #apply_children
380        }
381    }
382    .into()
383}
384
385#[proc_macro_derive(UserData)]
386pub fn lua_user_data(input: TokenStream) -> TokenStream {
387    /*let crate_name = std::env::var("CARGO_PKG_NAME").unwrap();
388
389    let crate_name = format_ident!(
390        "{}",
391        if crate_name == "feather-ui" {
392            "crate"
393        } else {
394            "feather_ui"
395        }
396    );*/
397    let crate_name = format_ident!("feather_ui");
398
399    let ast = parse_macro_input!(input as DeriveInput);
400    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
401
402    let data = if let Data::Struct(data_enum) = &ast.data {
403        data_enum
404    } else {
405        panic!("`UserData` derive can only be used on a struct.");
406    };
407
408    let mut field_methods = proc_macro2::TokenStream::new();
409    for m in data.fields.members() {
410        match m {
411            syn::Member::Named(ident) => {
412                field_methods.extend(quote! {
413                    f.add_field_method_get(stringify!(#ident), |_, this| Ok(this.#ident.clone()));
414                    f.add_field_method_set(stringify!(#ident), |_, this, v| Ok(this.#ident = v));
415                });
416            }
417            syn::Member::Unnamed(_) => panic!(
418                "You can't use a UserData derive on a tuple, because mlua knows how to parse tuples already!"
419            ),
420        }
421    }
422
423    let sname = ast.ident;
424    quote! {
425        impl #impl_generics #crate_name::mlua::UserData for #sname #ty_generics #where_clause {
426            fn add_fields<F: #crate_name::mlua::UserDataFields<Self>>(f: &mut F) {
427                #field_methods
428            }
429        }
430
431        impl #impl_generics #crate_name::mlua::FromLua for #sname #ty_generics #where_clause {
432            #[inline]
433            fn from_lua(value: #crate_name::mlua::Value, _: &#crate_name::mlua::Lua) -> #crate_name::mlua::Result<Self> {
434                match value {
435                    #crate_name::mlua::Value::UserData(ud) => Ok(ud.borrow::<Self>()?.clone()),
436                    _ => Err(#crate_name::mlua::Error::FromLuaConversionError {
437                        from: value.type_name(),
438                        to: stringify!(#sname).to_string(),
439                        message: None,
440                    }),
441                }
442            }
443        }
444    }
445    .into()
446}