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}