1#![cfg_attr(docsrs, feature(doc_cfg))]
2use proc_macro::TokenStream;
14use quote::quote;
15use syn::{DeriveInput, LitStr, Type, parse_macro_input};
16
17#[proc_macro_attribute]
37pub fn react_message(attr: TokenStream, item: TokenStream) -> TokenStream {
38 let name_override = match parse_name_only_attr(attr, "react_message") {
39 Ok(name) => name,
40 Err(e) => return e.to_compile_error().into(),
41 };
42
43 let input = parse_macro_input!(item as DeriveInput);
44 let PayloadParts {
45 ident,
46 impl_generics,
47 ty_generics,
48 where_clause,
49 name,
50 } = payload_parts(&input, name_override);
51
52 quote! {
53 #[derive(::serde::Deserialize, ::ts_rs::TS)]
54 #input
55
56 impl #impl_generics ::bevy::ecs::event::Event for #ident #ty_generics #where_clause {
57 type Trigger<'a> = ::bevy::ecs::event::GlobalTrigger;
58 }
59
60 impl #impl_generics ::bevy_react::ReactPayload for #ident #ty_generics #where_clause {
61 const NAME: &'static str = #name;
62 }
63 }
64 .into()
65}
66
67#[proc_macro_attribute]
88pub fn react_request(attr: TokenStream, item: TokenStream) -> TokenStream {
89 let mut name_override: Option<String> = None;
90 let mut response: Option<Type> = None;
91 let arg_parser = syn::meta::parser(|meta| {
92 if try_parse_name_arg(&meta, &mut name_override)? {
93 Ok(())
94 } else if meta.path.is_ident("response") {
95 response = Some(meta.value()?.parse::<Type>()?);
96 Ok(())
97 } else {
98 Err(meta.error(
99 "unsupported `react_request` argument; expected `name = \"...\"` or `response = Type`",
100 ))
101 }
102 });
103 parse_macro_input!(attr with arg_parser);
104
105 let response = match response {
106 Some(ty) => ty,
107 None => {
108 return syn::Error::new(
109 proc_macro2::Span::call_site(),
110 "`react_request` requires a `response = Type` argument",
111 )
112 .to_compile_error()
113 .into();
114 }
115 };
116
117 let input = parse_macro_input!(item as DeriveInput);
118 let PayloadParts {
119 ident,
120 impl_generics,
121 ty_generics,
122 where_clause,
123 name,
124 } = payload_parts(&input, name_override);
125
126 quote! {
127 #[derive(::serde::Deserialize, ::ts_rs::TS)]
128 #input
129
130 impl #impl_generics ::bevy_react::ReactRequest for #ident #ty_generics #where_clause {
131 const NAME: &'static str = #name;
132 type Response = #response;
133 }
134 }
135 .into()
136}
137
138#[proc_macro_attribute]
152pub fn react_event(attr: TokenStream, item: TokenStream) -> TokenStream {
153 let name_override = match parse_name_only_attr(attr, "react_event") {
154 Ok(name) => name,
155 Err(e) => return e.to_compile_error().into(),
156 };
157
158 let input = parse_macro_input!(item as DeriveInput);
159 let PayloadParts {
160 ident,
161 impl_generics,
162 ty_generics,
163 where_clause,
164 name,
165 } = payload_parts(&input, name_override);
166
167 quote! {
168 #[derive(::serde::Serialize, ::ts_rs::TS)]
169 #input
170
171 impl #impl_generics ::bevy_react::ReactEvent for #ident #ty_generics #where_clause {
172 const NAME: &'static str = #name;
173 }
174 }
175 .into()
176}
177
178#[proc_macro_attribute]
232pub fn react_filter(attr: TokenStream, item: TokenStream) -> TokenStream {
233 let mut name_override: Option<String> = None;
234 let mut shader: Option<LitStr> = None;
235 let mut outset: f32 = 0.0;
236 let mut time = false;
237 let arg_parser = syn::meta::parser(|meta| {
238 if try_parse_name_arg(&meta, &mut name_override)? {
239 Ok(())
240 } else if meta.path.is_ident("shader") {
241 shader = Some(meta.value()?.parse::<LitStr>()?);
242 Ok(())
243 } else if meta.path.is_ident("outset") {
244 outset = match meta.value()?.parse::<syn::Lit>()? {
245 syn::Lit::Float(f) => f.base10_parse()?,
246 syn::Lit::Int(i) => i.base10_parse()?,
247 other => {
248 return Err(syn::Error::new_spanned(
249 other,
250 "`outset` must be a number literal (logical px)",
251 ));
252 }
253 };
254 Ok(())
255 } else if meta.path.is_ident("time") {
256 time = meta.value()?.parse::<syn::LitBool>()?.value;
257 Ok(())
258 } else {
259 Err(meta.error(
260 "unsupported `react_filter` argument; expected `name = \"...\"`, \
261 `shader = \"...\"`, `outset = <number>`, or `time = <bool>`",
262 ))
263 }
264 });
265 parse_macro_input!(attr with arg_parser);
266
267 let Some(shader) = shader else {
268 return syn::Error::new(
269 proc_macro2::Span::call_site(),
270 "`react_filter` requires a `shader = \"path/to.wgsl\"` argument",
271 )
272 .to_compile_error()
273 .into();
274 };
275
276 let mut input = parse_macro_input!(item as DeriveInput);
277 let name = name_override.unwrap_or_else(|| lower_first(&input.ident.to_string()));
278
279 let syn::Data::Struct(data) = &mut input.data else {
280 return syn::Error::new_spanned(&input.ident, "`react_filter` requires a struct")
281 .to_compile_error()
282 .into();
283 };
284 let syn::Fields::Named(fields) = &mut data.fields else {
285 return syn::Error::new_spanned(
286 &input.ident,
287 "`react_filter` requires named fields (each field becomes a shader param)",
288 )
289 .to_compile_error()
290 .into();
291 };
292
293 let mut ts_overrides: Vec<(usize, &'static str)> = Vec::new();
301 let mut errors: Vec<proc_macro2::TokenStream> = Vec::new();
302 let mut slots: Vec<proc_macro2::TokenStream> = Vec::new();
303 let mut writes: Vec<proc_macro2::TokenStream> = Vec::new();
304 let mut length_checks: Vec<proc_macro2::TokenStream> = Vec::new();
305 let mut vec_i = 0usize;
306 let mut comp = 0usize;
307 for (field_i, field) in fields.named.iter().enumerate() {
308 let ident = field.ident.clone().expect("named field");
309 let field_name = ident.to_string();
310 let Some(param) = classify_filter_field(&field.ty) else {
311 errors.push(
312 syn::Error::new_spanned(
313 &field.ty,
314 format!(
315 "`react_filter` cannot pack field `{field_name}`: supported param types \
316 are f32, Vec2, Vec3, Vec4, [f32; 2..=4], Angle, Length, and FilterColor"
317 ),
318 )
319 .to_compile_error(),
320 );
321 continue;
322 };
323 let len = param.len();
324 if comp + len > 4 {
327 vec_i += 1;
328 comp = 0;
329 }
330 let (v, c) = (vec_i, comp);
331 let kind = param.value_kind();
332 slots.push(quote! {
333 ::bevy_react::filters::ParamSlot {
334 name: #field_name,
335 kind: #kind,
336 vec: #v,
337 comp: #c,
338 len: #len,
339 }
340 });
341 match ¶m {
342 FilterField::Scalar => writes.push(quote! { params[#v][#c] = self.#ident; }),
343 FilterField::Vector(n) => {
344 for (i, axis) in ["x", "y", "z", "w"].iter().take(*n).enumerate() {
345 let axis = syn::Ident::new(axis, proc_macro2::Span::call_site());
346 let ci = c + i;
347 writes.push(quote! { params[#v][#ci] = self.#ident.#axis; });
348 }
349 }
350 FilterField::Array(n) => {
351 for i in 0..*n {
352 let ci = c + i;
353 writes.push(quote! { params[#v][#ci] = self.#ident[#i]; });
354 }
355 }
356 FilterField::Angle => {
357 writes.push(quote! { params[#v][#c] = self.#ident.radians(); });
358 }
359 FilterField::Length => {
360 writes.push(quote! {
365 params[#v][#c] = ::bevy_react::filters::length_logical_px(
366 #name, #field_name, self.#ident,
367 )
368 .unwrap_or(0.0);
369 });
370 length_checks.push(quote! {
371 ::bevy_react::filters::length_logical_px(#name, #field_name, self.#ident)?;
372 });
373 }
374 FilterField::Color => {
375 for i in 0..4usize {
376 let ci = c + i;
377 writes.push(quote! { params[#v][#ci] = self.#ident.0[#i]; });
378 }
379 }
380 }
381 comp += len;
382 if comp == 4 {
383 vec_i += 1;
384 comp = 0;
385 }
386 if let Some(ts) = param.ts_override() {
387 ts_overrides.push((field_i, ts));
388 }
389 }
390 if errors.is_empty() {
391 for (field_i, ts) in ts_overrides {
394 fields.named[field_i]
395 .attrs
396 .push(syn::parse_quote!(#[ts(type = #ts)]));
397 }
398 } else {
399 return quote! { #input #(#errors)* }.into();
403 }
404 let total_vecs = if comp == 0 { vec_i } else { vec_i + 1 };
405
406 let ident = &input.ident;
407 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
408 let resolve_override = (!length_checks.is_empty()).then(|| {
411 quote! {
412 fn resolve(
413 &self,
414 assets: &::bevy::asset::AssetServer,
415 ) -> ::std::result::Result<
416 ::std::vec::Vec<::bevy_react::filters::ResolvedFilterPass>,
417 ::std::string::String,
418 > {
419 #(#length_checks)*
420 ::bevy_react::filters::resolve_single_pass(self, assets)
421 }
422 }
423 });
424
425 quote! {
426 #[derive(::serde::Deserialize, ::ts_rs::TS)]
427 #[serde(deny_unknown_fields)]
428 #input
429
430 impl #impl_generics ::bevy_react::filters::ReactFilter for #ident #ty_generics #where_clause {
431 const NAME: &'static str = #name;
432 const USES_TIME: bool = #time;
433
434 fn shader(
435 assets: &::bevy::asset::AssetServer,
436 ) -> ::bevy::asset::Handle<::bevy::shader::Shader> {
437 assets.load(#shader)
438 }
439
440 fn outset(&self) -> ::std::result::Result<f32, ::std::string::String> {
441 #(#length_checks)*
442 ::std::result::Result::Ok(#outset)
443 }
444
445 fn pack(
446 &self,
447 ) -> (
448 ::std::vec::Vec<::bevy::math::Vec4>,
449 ::std::sync::Arc<[::bevy_react::filters::ParamSlot]>,
450 ) {
451 static LAYOUT: ::std::sync::LazyLock<
452 ::std::sync::Arc<[::bevy_react::filters::ParamSlot]>,
453 > = ::std::sync::LazyLock::new(|| {
454 ::std::sync::Arc::from(::std::vec![#(#slots),*])
455 });
456 #[allow(unused_mut)]
457 let mut params = ::std::vec![::bevy::math::Vec4::ZERO; #total_vecs];
458 #(#writes)*
459 (params, LAYOUT.clone())
460 }
461
462 #resolve_override
463 }
464 }
465 .into()
466}
467
468enum FilterField {
471 Scalar,
473 Vector(usize),
475 Array(usize),
477 Angle,
479 Length,
481 Color,
483}
484
485impl FilterField {
486 fn len(&self) -> usize {
488 match self {
489 Self::Scalar | Self::Angle | Self::Length => 1,
490 Self::Vector(n) | Self::Array(n) => *n,
491 Self::Color => 4,
492 }
493 }
494
495 fn value_kind(&self) -> proc_macro2::TokenStream {
497 match self {
498 Self::Scalar | Self::Vector(_) | Self::Array(_) => {
499 quote!(::bevy_react::animations::ValueKind::Scalar)
500 }
501 Self::Angle => quote!(::bevy_react::animations::ValueKind::Angle),
502 Self::Length => quote!(::bevy_react::animations::ValueKind::Length),
503 Self::Color => quote!(::bevy_react::animations::ValueKind::Color),
504 }
505 }
506
507 fn ts_override(&self) -> Option<&'static str> {
511 match self {
512 Self::Vector(2) => Some("[number, number]"),
513 Self::Vector(3) => Some("[number, number, number]"),
514 Self::Vector(4) => Some("[number, number, number, number]"),
515 Self::Angle | Self::Length => Some("number | string"),
516 _ => None,
517 }
518 }
519}
520
521fn classify_filter_field(ty: &Type) -> Option<FilterField> {
525 match ty {
526 Type::Path(p) => {
527 let seg = p.path.segments.last()?;
528 if !seg.arguments.is_empty() {
529 return None;
530 }
531 match seg.ident.to_string().as_str() {
532 "f32" => Some(FilterField::Scalar),
533 "Vec2" => Some(FilterField::Vector(2)),
534 "Vec3" => Some(FilterField::Vector(3)),
535 "Vec4" => Some(FilterField::Vector(4)),
536 "Angle" => Some(FilterField::Angle),
537 "Length" => Some(FilterField::Length),
538 "FilterColor" => Some(FilterField::Color),
539 _ => None,
540 }
541 }
542 Type::Array(a) => {
543 let is_f32 = matches!(&*a.elem, Type::Path(p) if p.path.is_ident("f32"));
544 let syn::Expr::Lit(lit) = &a.len else {
545 return None;
546 };
547 let syn::Lit::Int(n) = &lit.lit else {
548 return None;
549 };
550 let n = n.base10_parse::<usize>().ok()?;
551 (is_f32 && (2..=4).contains(&n)).then_some(FilterField::Array(n))
552 }
553 _ => None,
554 }
555}
556
557fn try_parse_name_arg(
560 meta: &syn::meta::ParseNestedMeta,
561 out: &mut Option<String>,
562) -> syn::Result<bool> {
563 if meta.path.is_ident("name") {
564 *out = Some(meta.value()?.parse::<LitStr>()?.value());
565 Ok(true)
566 } else {
567 Ok(false)
568 }
569}
570
571fn parse_name_only_attr(attr: TokenStream, macro_name: &str) -> syn::Result<Option<String>> {
574 let mut name_override: Option<String> = None;
575 let parser = syn::meta::parser(|meta| {
576 if try_parse_name_arg(&meta, &mut name_override)? {
577 Ok(())
578 } else {
579 Err(meta.error(format!(
580 "unsupported `{macro_name}` argument; expected `name = \"...\"`"
581 )))
582 }
583 });
584 syn::parse::Parser::parse(parser, attr)?;
585 Ok(name_override)
586}
587
588struct PayloadParts<'a> {
590 ident: &'a syn::Ident,
591 impl_generics: syn::ImplGenerics<'a>,
592 ty_generics: syn::TypeGenerics<'a>,
593 where_clause: Option<&'a syn::WhereClause>,
594 name: String,
597}
598
599fn payload_parts<'a>(input: &'a DeriveInput, name_override: Option<String>) -> PayloadParts<'a> {
600 let ident = &input.ident;
601 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
602 PayloadParts {
603 ident,
604 impl_generics,
605 ty_generics,
606 where_clause,
607 name: name_override.unwrap_or_else(|| lower_first(&ident.to_string())),
608 }
609}
610
611fn lower_first(s: &str) -> String {
613 let mut chars = s.chars();
614 match chars.next() {
615 Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
616 None => String::new(),
617 }
618}