bevy_yoleck_macros/
lib.rs

1use proc_macro2::TokenStream;
2
3use quote::quote;
4use syn::{Data, DeriveInput, Error, Field, Fields, LitStr, Token, Type};
5
6#[proc_macro_derive(YoleckComponent)]
7pub fn derive_yoleck_component(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
8    let input = syn::parse_macro_input!(input as DeriveInput);
9    match impl_yoleck_component_derive(input) {
10        Ok(output) => output.into(),
11        Err(error) => error.to_compile_error().into(),
12    }
13}
14
15fn impl_yoleck_component_derive(input: DeriveInput) -> Result<TokenStream, Error> {
16    let name = input.ident;
17    let key = name.to_string();
18    let result = quote!(
19        impl YoleckComponent for #name {
20            const KEY: &'static str = #key;
21        }
22    );
23    Ok(result)
24}
25
26#[derive(Default, Debug)]
27struct YoleckFieldAttrs {
28    range: Option<(f64, f64)>,
29    step: Option<f64>,
30    label: Option<String>,
31    tooltip: Option<String>,
32    readonly: bool,
33    hidden: bool,
34    multiline: bool,
35    color_picker: bool,
36    asset_extensions: Option<Vec<String>>,
37    entity_filter: Option<String>,
38    speed: Option<f64>,
39}
40
41fn parse_number(expr: &syn::Expr) -> syn::Result<f64> {
42    match expr {
43        syn::Expr::Lit(syn::ExprLit {
44            lit: syn::Lit::Int(i),
45            ..
46        }) => Ok(i.base10_parse::<f64>()?),
47        syn::Expr::Lit(syn::ExprLit {
48            lit: syn::Lit::Float(f),
49            ..
50        }) => Ok(f.base10_parse::<f64>()?),
51        syn::Expr::Unary(syn::ExprUnary {
52            op: syn::UnOp::Neg(_),
53            expr: inner,
54            ..
55        }) => Ok(-parse_number(inner)?),
56        _ => Err(syn::Error::new_spanned(expr, "Expected numeric literal")),
57    }
58}
59
60fn parse_field_attrs(field: &Field) -> Result<YoleckFieldAttrs, Error> {
61    let mut attrs = YoleckFieldAttrs::default();
62
63    for attr in &field.attrs {
64        if !attr.path().is_ident("yoleck") {
65            continue;
66        }
67
68        attr.parse_nested_meta(|meta| {
69            if meta.path.is_ident("readonly") {
70                attrs.readonly = true;
71                return Ok(());
72            }
73            if meta.path.is_ident("hidden") {
74                attrs.hidden = true;
75                return Ok(());
76            }
77            if meta.path.is_ident("multiline") {
78                attrs.multiline = true;
79                return Ok(());
80            }
81            if meta.path.is_ident("color_picker") {
82                attrs.color_picker = true;
83                return Ok(());
84            }
85
86            if meta.path.is_ident("label") {
87                let value: syn::LitStr = meta.value()?.parse()?;
88                attrs.label = Some(value.value());
89                return Ok(());
90            }
91            if meta.path.is_ident("tooltip") {
92                let value: syn::LitStr = meta.value()?.parse()?;
93                attrs.tooltip = Some(value.value());
94                return Ok(());
95            }
96            if meta.path.is_ident("step") {
97                let value: syn::LitFloat = meta.value()?.parse()?;
98                attrs.step = Some(value.base10_parse()?);
99                return Ok(());
100            }
101            if meta.path.is_ident("speed") {
102                let value: syn::LitFloat = meta.value()?.parse()?;
103                attrs.speed = Some(value.base10_parse()?);
104                return Ok(());
105            }
106            if meta.path.is_ident("asset") {
107                let value: syn::LitStr = meta.value()?.parse()?;
108                attrs.asset_extensions = Some(
109                    value
110                        .value()
111                        .split(',')
112                        .map(|s| s.trim().to_string())
113                        .collect(),
114                );
115                return Ok(());
116            }
117            if meta.path.is_ident("entity_ref") {
118                let value: syn::LitStr = meta.value()?.parse()?;
119                attrs.entity_filter = Some(value.value());
120                return Ok(());
121            }
122            if meta.path.is_ident("range") {
123                let content;
124                syn::parenthesized!(content in meta.input);
125
126                let expr: syn::Expr = content.parse()?;
127                match expr {
128                    syn::Expr::Range(syn::ExprRange {
129                        start: Some(start),
130                        end: Some(end),
131                        limits: syn::RangeLimits::Closed(_),
132                        ..
133                    }) => {
134                        let start_val = parse_number(&start)?;
135                        let end_val = parse_number(&end)?;
136                        attrs.range = Some((start_val, end_val));
137                        return Ok(());
138                    }
139                    _ => {
140                        return Err(syn::Error::new_spanned(
141                            expr,
142                            "Expected closed numeric range, e.g., `0.5..=10.0`",
143                        ));
144                    }
145                }
146            }
147
148            Err(meta.error("unknown yoleck attribute"))
149        })?;
150    }
151
152    Ok(attrs)
153}
154
155fn get_type_name(ty: &Type) -> String {
156    match ty {
157        Type::Path(type_path) => type_path
158            .path
159            .segments
160            .last()
161            .map(|s| s.ident.to_string())
162            .unwrap_or_default(),
163        _ => String::new(),
164    }
165}
166
167fn quote_option<T, F>(opt: &Option<T>, f: F) -> TokenStream
168where
169    F: FnOnce(&T) -> TokenStream,
170{
171    match opt {
172        Some(value) => {
173            let inner = f(value);
174            quote! { Some(#inner) }
175        }
176        None => quote! { None },
177    }
178}
179
180fn generate_field_ui(field: &Field, attrs: &YoleckFieldAttrs) -> TokenStream {
181    let field_name = field.ident.as_ref().unwrap();
182    let field_name_str = attrs
183        .label
184        .clone()
185        .unwrap_or_else(|| field_name.to_string().replace('_', " "));
186
187    let range = quote_option(&attrs.range, |(min, max)| quote! { (#min, #max) });
188    let speed = quote_option(&attrs.speed, |s| quote! { #s });
189    let label_opt = quote_option(&attrs.label, |l| quote! { #l.to_string() });
190    let tooltip = quote_option(&attrs.tooltip, |t| quote! { #t.to_string() });
191    let entity_filter = quote_option(&attrs.entity_filter, |f| quote! { #f.to_string() });
192
193    let readonly = attrs.readonly;
194    let multiline = attrs.multiline;
195
196    quote! {
197        {
198            use bevy_yoleck::auto_edit::{YoleckAutoEdit, FieldAttrs};
199            let attrs = FieldAttrs {
200                label: #label_opt,
201                tooltip: #tooltip,
202                range: #range,
203                speed: #speed,
204                readonly: #readonly,
205                multiline: #multiline,
206                entity_filter: #entity_filter,
207            };
208            YoleckAutoEdit::auto_edit_with_label_and_attrs(
209                &mut value.#field_name,
210                ui,
211                #field_name_str,
212                &attrs,
213            );
214        }
215    }
216}
217
218#[proc_macro_derive(YoleckAutoEdit, attributes(yoleck))]
219pub fn derive_yoleck_auto_edit(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
220    let input = syn::parse_macro_input!(input as DeriveInput);
221    match impl_yoleck_auto_edit_derive(input) {
222        Ok(output) => output.into(),
223        Err(error) => error.to_compile_error().into(),
224    }
225}
226
227fn impl_yoleck_auto_edit_derive(input: DeriveInput) -> Result<TokenStream, Error> {
228    let name = &input.ident;
229    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
230
231    let fields = if let Data::Struct(data) = &input.data {
232        if let Fields::Named(fields) = &data.fields {
233            &fields.named
234        } else {
235            return Err(Error::new_spanned(
236                &input,
237                "YoleckAutoEdit only supports structs with named fields",
238            ));
239        }
240    } else {
241        return Err(Error::new_spanned(
242            &input,
243            "YoleckAutoEdit only supports structs",
244        ));
245    };
246
247    let mut field_uis = Vec::new();
248    for field in fields {
249        let attrs = parse_field_attrs(field)?;
250        if attrs.hidden {
251            continue;
252        }
253        field_uis.push(generate_field_ui(field, &attrs));
254    }
255
256    let mut entity_ref_fields = Vec::new();
257    let mut entity_ref_field_names = Vec::new();
258    for field in fields {
259        if let Some(info) = parse_entity_ref_attrs(field)? {
260            entity_ref_fields.push(info);
261            entity_ref_field_names.push(
262                field
263                    .ident
264                    .as_ref()
265                    .expect("fields are taken from a named struct variant"),
266            );
267        }
268    }
269
270    let fields_array: Vec<TokenStream> = entity_ref_fields
271        .iter()
272        .map(|info| {
273            let field_ident = &info.field_ident;
274            let field_ident_str = LitStr::new(&field_ident.to_string(), field_ident.span());
275            let filter = match &info.filter {
276                Some(f) => quote! { Some(#f) },
277                None => quote! { None },
278            };
279
280            quote! { (#field_ident_str, #filter) }
281        })
282        .collect();
283
284    let match_arms: Vec<TokenStream> = entity_ref_fields
285        .iter()
286        .map(|info| {
287            let field_ident = &info.field_ident;
288            let field_ident_str = LitStr::new(&field_ident.to_string(), field_ident.span());
289
290            quote! {
291                #field_ident_str => &mut self.#field_ident
292            }
293        })
294        .collect();
295
296    let fields_count = entity_ref_fields.len();
297
298    let get_entity_ref_mut_body = if entity_ref_fields.is_empty() {
299        quote! {
300            panic!("No entity ref fields in {}", stringify!(#name))
301        }
302    } else {
303        quote! {
304            match field_name {
305                #(#match_arms,)*
306                _ => panic!("Unknown entity ref field: {}", field_name),
307            }
308        }
309    };
310
311    let result = quote! {
312        impl #impl_generics bevy_yoleck::auto_edit::YoleckAutoEdit for #name #ty_generics #where_clause {
313            fn auto_edit(value: &mut Self, ui: &mut bevy_yoleck::egui::Ui) {
314                use bevy_yoleck::egui;
315                #(#field_uis)*
316            }
317        }
318
319        impl #impl_generics bevy_yoleck::entity_ref::YoleckEntityRefAccessor for #name #ty_generics #where_clause {
320            fn entity_ref_fields() -> &'static [(&'static str, Option<&'static str>)] {
321                static FIELDS: [(&'static str, Option<&'static str>); #fields_count] = [
322                    #(#fields_array),*
323                ];
324                &FIELDS
325            }
326
327            fn get_entity_ref_mut(&mut self, field_name: &str) -> &mut bevy_yoleck::entity_ref::YoleckEntityRef {
328                #get_entity_ref_mut_body
329            }
330
331            fn resolve_entity_refs(&mut self, registry: &bevy_yoleck::prelude::YoleckUuidRegistry) {
332                #(
333                    let _ = self.#entity_ref_field_names.resolve(registry);
334                )*
335            }
336        }
337    };
338
339    Ok(result)
340}
341
342#[derive(Debug)]
343struct EntityRefFieldInfo {
344    field_ident: syn::Ident,
345    filter: Option<String>,
346}
347
348fn parse_entity_ref_attrs(field: &Field) -> Result<Option<EntityRefFieldInfo>, Error> {
349    let type_name = get_type_name(&field.ty);
350
351    if type_name != "YoleckEntityRef" {
352        return Ok(None);
353    }
354
355    let field_ident = field
356        .ident
357        .as_ref()
358        .ok_or_else(|| Error::new_spanned(field, "Expected named field"))?
359        .clone();
360
361    let mut info = EntityRefFieldInfo {
362        field_ident,
363        filter: None,
364    };
365
366    for attr in &field.attrs {
367        if !attr.path().is_ident("yoleck") {
368            continue;
369        }
370
371        attr.parse_nested_meta(|meta| {
372            if meta.path.is_ident("entity_ref") {
373                if meta.input.peek(Token![=]) {
374                    let value: syn::LitStr = meta.value()?.parse()?;
375                    info.filter = Some(value.value());
376                }
377                return Ok(());
378            }
379            Ok(())
380        })?;
381    }
382
383    Ok(Some(info))
384}