diff --git a/Cargo.toml b/Cargo.toml index c219ccdd41..f5f95239f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2067,6 +2067,17 @@ description = "Demonstrates how reflection in Bevy provides a way to dynamically category = "Reflection" wasm = false +[[example]] +name = "custom_attributes" +path = "examples/reflection/custom_attributes.rs" +doc-scrape-examples = true + +[package.metadata.example.custom_attributes] +name = "Custom Attributes" +description = "Registering and accessing custom attributes on reflected types" +category = "Reflection" +wasm = false + [[example]] name = "dynamic_types" path = "examples/reflection/dynamic_types.rs" diff --git a/crates/bevy_reflect/derive/src/container_attributes.rs b/crates/bevy_reflect/derive/src/container_attributes.rs index e87e565940..7b7a12752d 100644 --- a/crates/bevy_reflect/derive/src/container_attributes.rs +++ b/crates/bevy_reflect/derive/src/container_attributes.rs @@ -5,6 +5,7 @@ //! the derive helper attribute for `Reflect`, which looks like: //! `#[reflect(PartialEq, Default, ...)]` and `#[reflect_value(PartialEq, Default, ...)]`. +use crate::custom_attributes::CustomAttributes; use crate::derive_data::ReflectTraitToImpl; use crate::utility; use crate::utility::terminated_parser; @@ -187,6 +188,7 @@ pub(crate) struct ContainerAttributes { type_path_attrs: TypePathAttrs, custom_where: Option, no_field_bounds: bool, + custom_attributes: CustomAttributes, idents: Vec, } @@ -227,7 +229,9 @@ impl ContainerAttributes { trait_: ReflectTraitToImpl, ) -> syn::Result<()> { let lookahead = input.lookahead1(); - if lookahead.peek(Token![where]) { + if lookahead.peek(Token![@]) { + self.custom_attributes.parse_custom_attribute(input) + } else if lookahead.peek(Token![where]) { self.parse_custom_where(input) } else if lookahead.peek(kw::from_reflect) { self.parse_from_reflect(input, trait_) @@ -509,6 +513,10 @@ impl ContainerAttributes { } } + pub fn custom_attributes(&self) -> &CustomAttributes { + &self.custom_attributes + } + /// The custom where configuration found within `#[reflect(...)]` attributes on this type. pub fn custom_where(&self) -> Option<&WhereClause> { self.custom_where.as_ref() diff --git a/crates/bevy_reflect/derive/src/custom_attributes.rs b/crates/bevy_reflect/derive/src/custom_attributes.rs new file mode 100644 index 0000000000..68bb7d1843 --- /dev/null +++ b/crates/bevy_reflect/derive/src/custom_attributes.rs @@ -0,0 +1,42 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::ParseStream; +use syn::{Expr, Path, Token}; + +#[derive(Default, Clone)] +pub(crate) struct CustomAttributes { + attributes: Vec, +} + +impl CustomAttributes { + /// Generates a `TokenStream` for `CustomAttributes` construction. + pub fn to_tokens(&self, bevy_reflect_path: &Path) -> TokenStream { + let attributes = self.attributes.iter().map(|value| { + quote! { + .with_attribute(#value) + } + }); + + quote! { + #bevy_reflect_path::attributes::CustomAttributes::default() + #(#attributes)* + } + } + + /// Inserts a custom attribute into the list. + pub fn push(&mut self, value: Expr) -> syn::Result<()> { + self.attributes.push(value); + Ok(()) + } + + /// Parse `@` (custom attribute) attribute. + /// + /// Examples: + /// - `#[reflect(@Foo))]` + /// - `#[reflect(@Bar::baz("qux"))]` + /// - `#[reflect(@0..256u8)]` + pub fn parse_custom_attribute(&mut self, input: ParseStream) -> syn::Result<()> { + input.parse::()?; + self.push(input.parse()?) + } +} diff --git a/crates/bevy_reflect/derive/src/derive_data.rs b/crates/bevy_reflect/derive/src/derive_data.rs index 3c45b5ee3b..77b3339651 100644 --- a/crates/bevy_reflect/derive/src/derive_data.rs +++ b/crates/bevy_reflect/derive/src/derive_data.rs @@ -1,4 +1,5 @@ use core::fmt; +use proc_macro2::Span; use crate::container_attributes::{ContainerAttributes, FromReflectAttrs, TypePathAttrs}; use crate::field_attributes::FieldAttributes; @@ -481,6 +482,44 @@ impl<'a> ReflectMeta<'a> { } } +impl<'a> StructField<'a> { + /// Generates a `TokenStream` for `NamedField` or `UnnamedField` construction. + pub fn to_info_tokens(&self, bevy_reflect_path: &Path) -> proc_macro2::TokenStream { + let name = match &self.data.ident { + Some(ident) => ident.to_string().to_token_stream(), + None => self.reflection_index.to_token_stream(), + }; + + let field_info = if self.data.ident.is_some() { + quote! { + #bevy_reflect_path::NamedField + } + } else { + quote! { + #bevy_reflect_path::UnnamedField + } + }; + + let ty = &self.data.ty; + let custom_attributes = self.attrs.custom_attributes.to_tokens(bevy_reflect_path); + + #[allow(unused_mut)] // Needs mutability for the feature gate + let mut info = quote! { + #field_info::new::<#ty>(#name).with_custom_attributes(#custom_attributes) + }; + + #[cfg(feature = "documentation")] + { + let docs = &self.doc; + info.extend(quote! { + .with_docs(#docs) + }); + } + + info + } +} + impl<'a> ReflectStruct<'a> { /// Access the metadata associated with this struct definition. pub fn meta(&self) -> &ReflectMeta<'a> { @@ -536,6 +575,53 @@ impl<'a> ReflectStruct<'a> { pub fn where_clause_options(&self) -> WhereClauseOptions { WhereClauseOptions::new_with_fields(self.meta(), self.active_types().into_boxed_slice()) } + + /// Generates a `TokenStream` for `TypeInfo::Struct` or `TypeInfo::TupleStruct` construction. + pub fn to_info_tokens(&self, is_tuple: bool) -> proc_macro2::TokenStream { + let bevy_reflect_path = self.meta().bevy_reflect_path(); + + let (info_variant, info_struct) = if is_tuple { + ( + Ident::new("TupleStruct", Span::call_site()), + Ident::new("TupleStructInfo", Span::call_site()), + ) + } else { + ( + Ident::new("Struct", Span::call_site()), + Ident::new("StructInfo", Span::call_site()), + ) + }; + + let field_infos = self + .active_fields() + .map(|field| field.to_info_tokens(bevy_reflect_path)); + + let custom_attributes = self + .meta + .attrs + .custom_attributes() + .to_tokens(bevy_reflect_path); + + #[allow(unused_mut)] // Needs mutability for the feature gate + let mut info = quote! { + #bevy_reflect_path::#info_struct::new::(&[ + #(#field_infos),* + ]) + .with_custom_attributes(#custom_attributes) + }; + + #[cfg(feature = "documentation")] + { + let docs = self.meta().doc(); + info.extend(quote! { + .with_docs(#docs) + }); + } + + quote! { + #bevy_reflect_path::TypeInfo::#info_variant(#info) + } + } } impl<'a> ReflectEnum<'a> { @@ -589,6 +675,42 @@ impl<'a> ReflectEnum<'a> { Some(self.active_fields().map(|field| &field.data.ty)), ) } + + /// Generates a `TokenStream` for `TypeInfo::Enum` construction. + pub fn to_info_tokens(&self) -> proc_macro2::TokenStream { + let bevy_reflect_path = self.meta().bevy_reflect_path(); + + let variants = self + .variants + .iter() + .map(|variant| variant.to_info_tokens(bevy_reflect_path)); + + let custom_attributes = self + .meta + .attrs + .custom_attributes() + .to_tokens(bevy_reflect_path); + + #[allow(unused_mut)] // Needs mutability for the feature gate + let mut info = quote! { + #bevy_reflect_path::EnumInfo::new::(&[ + #(#variants),* + ]) + .with_custom_attributes(#custom_attributes) + }; + + #[cfg(feature = "documentation")] + { + let docs = self.meta().doc(); + info.extend(quote! { + .with_docs(#docs) + }); + } + + quote! { + #bevy_reflect_path::TypeInfo::Enum(#info) + } + } } impl<'a> EnumVariant<'a> { @@ -607,6 +729,57 @@ impl<'a> EnumVariant<'a> { EnumVariantFields::Unit => &[], } } + + /// Generates a `TokenStream` for `VariantInfo` construction. + pub fn to_info_tokens(&self, bevy_reflect_path: &Path) -> proc_macro2::TokenStream { + let variant_name = &self.data.ident.to_string(); + + let (info_variant, info_struct) = match &self.fields { + EnumVariantFields::Unit => ( + Ident::new("Unit", Span::call_site()), + Ident::new("UnitVariantInfo", Span::call_site()), + ), + EnumVariantFields::Unnamed(..) => ( + Ident::new("Tuple", Span::call_site()), + Ident::new("TupleVariantInfo", Span::call_site()), + ), + EnumVariantFields::Named(..) => ( + Ident::new("Struct", Span::call_site()), + Ident::new("StructVariantInfo", Span::call_site()), + ), + }; + + let fields = self + .active_fields() + .map(|field| field.to_info_tokens(bevy_reflect_path)); + + let args = match &self.fields { + EnumVariantFields::Unit => quote!(#variant_name), + _ => { + quote!( #variant_name , &[#(#fields),*] ) + } + }; + + let custom_attributes = self.attrs.custom_attributes.to_tokens(bevy_reflect_path); + + #[allow(unused_mut)] // Needs mutability for the feature gate + let mut info = quote! { + #bevy_reflect_path::#info_struct::new(#args) + .with_custom_attributes(#custom_attributes) + }; + + #[cfg(feature = "documentation")] + { + let docs = &self.doc; + info.extend(quote! { + .with_docs(#docs) + }); + } + + quote! { + #bevy_reflect_path::VariantInfo::#info_variant(#info) + } + } } /// Represents a path to a type. diff --git a/crates/bevy_reflect/derive/src/field_attributes.rs b/crates/bevy_reflect/derive/src/field_attributes.rs index 6ab8826042..d66daf8382 100644 --- a/crates/bevy_reflect/derive/src/field_attributes.rs +++ b/crates/bevy_reflect/derive/src/field_attributes.rs @@ -4,6 +4,7 @@ //! as opposed to an entire struct or enum. An example of such an attribute is //! the derive helper attribute for `Reflect`, which looks like: `#[reflect(ignore)]`. +use crate::custom_attributes::CustomAttributes; use crate::utility::terminated_parser; use crate::REFLECT_ATTRIBUTE_NAME; use syn::parse::ParseStream; @@ -73,6 +74,8 @@ pub(crate) struct FieldAttributes { pub ignore: ReflectIgnoreBehavior, /// Sets the default behavior of this field. pub default: DefaultBehavior, + /// Custom attributes created via `#[reflect(@...)]`. + pub custom_attributes: CustomAttributes, } impl FieldAttributes { @@ -108,7 +111,9 @@ impl FieldAttributes { /// Parses a single field attribute. fn parse_field_attribute(&mut self, input: ParseStream) -> syn::Result<()> { let lookahead = input.lookahead1(); - if lookahead.peek(kw::ignore) { + if lookahead.peek(Token![@]) { + self.parse_custom_attribute(input) + } else if lookahead.peek(kw::ignore) { self.parse_ignore(input) } else if lookahead.peek(kw::skip_serializing) { self.parse_skip_serializing(input) @@ -176,4 +181,13 @@ impl FieldAttributes { Ok(()) } + + /// Parse `@` (custom attribute) attribute. + /// + /// Examples: + /// - `#[reflect(@(foo = "bar"))]` + /// - `#[reflect(@(min = 0.0, max = 1.0))]` + fn parse_custom_attribute(&mut self, input: ParseStream) -> syn::Result<()> { + self.custom_attributes.parse_custom_attribute(input) + } } diff --git a/crates/bevy_reflect/derive/src/impls/enums.rs b/crates/bevy_reflect/derive/src/impls/enums.rs index 8293da7157..ab72539a71 100644 --- a/crates/bevy_reflect/derive/src/impls/enums.rs +++ b/crates/bevy_reflect/derive/src/impls/enums.rs @@ -1,4 +1,4 @@ -use crate::derive_data::{EnumVariant, EnumVariantFields, ReflectEnum, StructField}; +use crate::derive_data::{EnumVariantFields, ReflectEnum, StructField}; use crate::enum_utility::{get_variant_constructors, EnumVariantConstructors}; use crate::impls::{impl_type_path, impl_typed}; use bevy_macro_utils::fq_std::{FQAny, FQBox, FQOption, FQResult}; @@ -17,7 +17,6 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream let where_clause_options = reflect_enum.where_clause_options(); let EnumImpls { - variant_info, enum_field, enum_field_at, enum_index_of, @@ -57,29 +56,10 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream } }); - #[cfg(feature = "documentation")] - let info_generator = { - let doc = reflect_enum.meta().doc(); - quote! { - #bevy_reflect_path::EnumInfo::new::(&variants).with_docs(#doc) - } - }; - - #[cfg(not(feature = "documentation"))] - let info_generator = { - quote! { - #bevy_reflect_path::EnumInfo::new::(&variants) - } - }; - let typed_impl = impl_typed( reflect_enum.meta(), &where_clause_options, - quote! { - let variants = [#(#variant_info),*]; - let info = #info_generator; - #bevy_reflect_path::TypeInfo::Enum(info) - }, + reflect_enum.to_info_tokens(), ); let type_path_impl = impl_type_path(reflect_enum.meta()); @@ -305,7 +285,6 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream } struct EnumImpls { - variant_info: Vec, enum_field: Vec, enum_field_at: Vec, enum_index_of: Vec, @@ -319,7 +298,6 @@ struct EnumImpls { fn generate_impls(reflect_enum: &ReflectEnum, ref_index: &Ident, ref_name: &Ident) -> EnumImpls { let bevy_reflect_path = reflect_enum.meta().bevy_reflect_path(); - let mut variant_info = Vec::new(); let mut enum_field = Vec::new(); let mut enum_field_at = Vec::new(); let mut enum_index_of = Vec::new(); @@ -340,133 +318,89 @@ fn generate_impls(reflect_enum: &ReflectEnum, ref_index: &Ident, ref_name: &Iden Fields::Named(..) => Ident::new("Struct", Span::call_site()), }; - let variant_info_ident = match variant.data.fields { - Fields::Unit => Ident::new("UnitVariantInfo", Span::call_site()), - Fields::Unnamed(..) => Ident::new("TupleVariantInfo", Span::call_site()), - Fields::Named(..) => Ident::new("StructVariantInfo", Span::call_site()), - }; - enum_variant_name.push(quote! { #unit{..} => #name }); enum_variant_index.push(quote! { #unit{..} => #variant_index }); + enum_variant_type.push(quote! { + #unit{..} => #bevy_reflect_path::VariantType::#variant_type_ident + }); - fn get_field_args( + fn process_fields( fields: &[StructField], - mut generate_for_field: impl FnMut(usize, usize, &StructField) -> proc_macro2::TokenStream, - ) -> Vec { - let mut constructor_argument = Vec::new(); - let mut reflect_idx = 0; - for field in fields { + mut f: impl FnMut(&StructField) + Sized, + ) -> usize { + let mut field_len = 0; + for field in fields.iter() { if field.attrs.ignore.is_ignored() { // Ignored field continue; - } - constructor_argument.push(generate_for_field( - reflect_idx, - field.declaration_index, - field, - )); - reflect_idx += 1; - } - constructor_argument - } - - let mut push_variant = - |_variant: &EnumVariant, arguments: proc_macro2::TokenStream, field_len: usize| { - #[cfg(feature = "documentation")] - let with_docs = { - let doc = quote::ToTokens::to_token_stream(&_variant.doc); - Some(quote!(.with_docs(#doc))) }; - #[cfg(not(feature = "documentation"))] - let with_docs: Option = None; - variant_info.push(quote! { - #bevy_reflect_path::VariantInfo::#variant_type_ident( - #bevy_reflect_path::#variant_info_ident::new(#arguments) - #with_docs - ) - }); - enum_field_len.push(quote! { - #unit{..} => #field_len - }); - enum_variant_type.push(quote! { - #unit{..} => #bevy_reflect_path::VariantType::#variant_type_ident - }); - }; + f(field); + + field_len += 1; + } + + field_len + } match &variant.fields { EnumVariantFields::Unit => { - push_variant(variant, quote!(#name), 0); + let field_len = process_fields(&[], |_| {}); + + enum_field_len.push(quote! { + #unit{..} => #field_len + }); } EnumVariantFields::Unnamed(fields) => { - let args = get_field_args(fields, |reflect_idx, declaration_index, field| { - let declare_field = syn::Index::from(declaration_index); + let field_len = process_fields(fields, |field: &StructField| { + let reflection_index = field + .reflection_index + .expect("reflection index should exist for active field"); + + let declare_field = syn::Index::from(field.declaration_index); enum_field_at.push(quote! { - #unit { #declare_field : value, .. } if #ref_index == #reflect_idx => #FQOption::Some(value) + #unit { #declare_field : value, .. } if #ref_index == #reflection_index => #FQOption::Some(value) }); - - #[cfg(feature = "documentation")] - let with_docs = { - let doc = quote::ToTokens::to_token_stream(&field.doc); - Some(quote!(.with_docs(#doc))) - }; - #[cfg(not(feature = "documentation"))] - let with_docs: Option = None; - - let field_ty = &field.data.ty; - quote! { - #bevy_reflect_path::UnnamedField::new::<#field_ty>(#reflect_idx) - #with_docs - } }); - let field_len = args.len(); - push_variant(variant, quote!(#name, &[ #(#args),* ]), field_len); + enum_field_len.push(quote! { + #unit{..} => #field_len + }); } EnumVariantFields::Named(fields) => { - let args = get_field_args(fields, |reflect_idx, _, field| { + let field_len = process_fields(fields, |field: &StructField| { let field_ident = field.data.ident.as_ref().unwrap(); let field_name = field_ident.to_string(); + let reflection_index = field + .reflection_index + .expect("reflection index should exist for active field"); + enum_field.push(quote! { #unit{ #field_ident, .. } if #ref_name == #field_name => #FQOption::Some(#field_ident) }); enum_field_at.push(quote! { - #unit{ #field_ident, .. } if #ref_index == #reflect_idx => #FQOption::Some(#field_ident) + #unit{ #field_ident, .. } if #ref_index == #reflection_index => #FQOption::Some(#field_ident) }); enum_index_of.push(quote! { - #unit{ .. } if #ref_name == #field_name => #FQOption::Some(#reflect_idx) + #unit{ .. } if #ref_name == #field_name => #FQOption::Some(#reflection_index) }); enum_name_at.push(quote! { - #unit{ .. } if #ref_index == #reflect_idx => #FQOption::Some(#field_name) + #unit{ .. } if #ref_index == #reflection_index => #FQOption::Some(#field_name) }); - - #[cfg(feature = "documentation")] - let with_docs = { - let doc = quote::ToTokens::to_token_stream(&field.doc); - Some(quote!(.with_docs(#doc))) - }; - #[cfg(not(feature = "documentation"))] - let with_docs: Option = None; - - let field_ty = &field.data.ty; - quote! { - #bevy_reflect_path::NamedField::new::<#field_ty>(#field_name) - #with_docs - } }); - let field_len = args.len(); - push_variant(variant, quote!(#name, &[ #(#args),* ]), field_len); + enum_field_len.push(quote! { + #unit{..} => #field_len + }); } }; } EnumImpls { - variant_info, enum_field, enum_field_at, enum_index_of, diff --git a/crates/bevy_reflect/derive/src/impls/structs.rs b/crates/bevy_reflect/derive/src/impls/structs.rs index f51f0b2de4..249ac22745 100644 --- a/crates/bevy_reflect/derive/src/impls/structs.rs +++ b/crates/bevy_reflect/derive/src/impls/structs.rs @@ -26,7 +26,6 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS .active_fields() .map(|field| ident_or_index(field.data.ident.as_ref(), field.declaration_index)) .collect::>(); - let field_types = reflect_struct.active_types(); let field_count = field_idents.len(); let field_indices = (0..field_count).collect::>(); @@ -46,47 +45,11 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS } }); - #[cfg(feature = "documentation")] - let field_generator = { - let docs = reflect_struct - .active_fields() - .map(|field| ToTokens::to_token_stream(&field.doc)); - quote! { - #(#bevy_reflect_path::NamedField::new::<#field_types>(#field_names).with_docs(#docs) ,)* - } - }; - - #[cfg(not(feature = "documentation"))] - let field_generator = { - quote! { - #(#bevy_reflect_path::NamedField::new::<#field_types>(#field_names) ,)* - } - }; - - #[cfg(feature = "documentation")] - let info_generator = { - let doc = reflect_struct.meta().doc(); - quote! { - #bevy_reflect_path::StructInfo::new::(&fields).with_docs(#doc) - } - }; - - #[cfg(not(feature = "documentation"))] - let info_generator = { - quote! { - #bevy_reflect_path::StructInfo::new::(&fields) - } - }; - let where_clause_options = reflect_struct.where_clause_options(); let typed_impl = impl_typed( reflect_struct.meta(), &where_clause_options, - quote! { - let fields = [#field_generator]; - let info = #info_generator; - #bevy_reflect_path::TypeInfo::Struct(info) - }, + reflect_struct.to_info_tokens(false), ); let type_path_impl = impl_type_path(reflect_struct.meta()); diff --git a/crates/bevy_reflect/derive/src/impls/tuple_structs.rs b/crates/bevy_reflect/derive/src/impls/tuple_structs.rs index 255928cf97..cf43e5fe98 100644 --- a/crates/bevy_reflect/derive/src/impls/tuple_structs.rs +++ b/crates/bevy_reflect/derive/src/impls/tuple_structs.rs @@ -15,7 +15,6 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2:: .active_fields() .map(|field| Member::Unnamed(Index::from(field.declaration_index))) .collect::>(); - let field_types = reflect_struct.active_types(); let field_count = field_idents.len(); let field_indices = (0..field_count).collect::>(); @@ -39,46 +38,10 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2:: } }); - #[cfg(feature = "documentation")] - let field_generator = { - let docs = reflect_struct - .active_fields() - .map(|field| ToTokens::to_token_stream(&field.doc)); - quote! { - #(#bevy_reflect_path::UnnamedField::new::<#field_types>(#field_idents).with_docs(#docs) ,)* - } - }; - - #[cfg(not(feature = "documentation"))] - let field_generator = { - quote! { - #(#bevy_reflect_path::UnnamedField::new::<#field_types>(#field_idents) ,)* - } - }; - - #[cfg(feature = "documentation")] - let info_generator = { - let doc = reflect_struct.meta().doc(); - quote! { - #bevy_reflect_path::TupleStructInfo::new::(&fields).with_docs(#doc) - } - }; - - #[cfg(not(feature = "documentation"))] - let info_generator = { - quote! { - #bevy_reflect_path::TupleStructInfo::new::(&fields) - } - }; - let typed_impl = impl_typed( reflect_struct.meta(), &where_clause_options, - quote! { - let fields = [#field_generator]; - let info = #info_generator; - #bevy_reflect_path::TypeInfo::TupleStruct(info) - }, + reflect_struct.to_info_tokens(true), ); let type_path_impl = impl_type_path(reflect_struct.meta()); diff --git a/crates/bevy_reflect/derive/src/lib.rs b/crates/bevy_reflect/derive/src/lib.rs index 743c558dde..86e0069a88 100644 --- a/crates/bevy_reflect/derive/src/lib.rs +++ b/crates/bevy_reflect/derive/src/lib.rs @@ -17,6 +17,7 @@ extern crate proc_macro; mod container_attributes; +mod custom_attributes; mod derive_data; #[cfg(feature = "documentation")] mod documentation; @@ -273,6 +274,36 @@ fn match_reflect_impls(ast: DeriveInput, source: ReflectImplSource) -> TokenStre /// // {/* ... */} /// ``` /// +/// ## `#[reflect(@...)]` +/// +/// This attribute can be used to register custom attributes to the type's `TypeInfo`. +/// +/// It accepts any expression after the `@` symbol that resolves to a value which implements `Reflect`. +/// +/// Any number of custom attributes may be registered, however, each the type of each attribute must be unique. +/// If two attributes of the same type are registered, the last one will overwrite the first. +/// +/// ### Example +/// +/// ```ignore +/// #[derive(Reflect)] +/// struct Required; +/// +/// #[derive(Reflect)] +/// struct EditorTooltip(String); +/// +/// impl EditorTooltip { +/// fn new(text: &str) -> Self { +/// Self(text.to_string()) +/// } +/// } +/// +/// #[derive(Reflect)] +/// // Specify a "required" status and tooltip: +/// #[reflect(@Required, @EditorTooltip::new("An ID is required!"))] +/// struct Id(u8); +/// ``` +/// /// # Field Attributes /// /// Along with the container attributes, this macro comes with some attributes that may be applied @@ -296,6 +327,35 @@ fn match_reflect_impls(ast: DeriveInput, source: ReflectImplSource) -> TokenStre /// What this does is register the `SerializationData` type within the `GetTypeRegistration` implementation, /// which will be used by the reflection serializers to determine whether or not the field is serializable. /// +/// ## `#[reflect(@...)]` +/// +/// This attribute can be used to register custom attributes to the field's `TypeInfo`. +/// +/// It accepts any expression after the `@` symbol that resolves to a value which implements `Reflect`. +/// +/// Any number of custom attributes may be registered, however, each the type of each attribute must be unique. +/// If two attributes of the same type are registered, the last one will overwrite the first. +/// +/// ### Example +/// +/// ```ignore +/// #[derive(Reflect)] +/// struct EditorTooltip(String); +/// +/// impl EditorTooltip { +/// fn new(text: &str) -> Self { +/// Self(text.to_string()) +/// } +/// } +/// +/// #[derive(Reflect)] +/// struct Slider { +/// // Specify a custom range and tooltip: +/// #[reflect(@0.0..=1.0, @EditorTooltip::new("Must be between 0 and 1"))] +/// value: f32, +/// } +/// ``` +/// /// [`reflect_trait`]: macro@reflect_trait #[proc_macro_derive(Reflect, attributes(reflect, reflect_value, type_path, type_name))] pub fn derive_reflect(input: TokenStream) -> TokenStream { diff --git a/crates/bevy_reflect/src/attributes.rs b/crates/bevy_reflect/src/attributes.rs new file mode 100644 index 0000000000..9fb2e694e4 --- /dev/null +++ b/crates/bevy_reflect/src/attributes.rs @@ -0,0 +1,445 @@ +use crate::Reflect; +use bevy_utils::TypeIdMap; +use core::fmt::{Debug, Formatter}; +use std::any::TypeId; + +/// A collection of custom attributes for a type, field, or variant. +/// +/// These attributes can be created with the [`Reflect` derive macro]. +/// +/// Attributes are stored by their [`TypeId`](std::any::TypeId). +/// Because of this, there can only be one attribute per type. +/// +/// # Example +/// +/// ``` +/// # use bevy_reflect::{Reflect, Typed, TypeInfo}; +/// use core::ops::RangeInclusive; +/// #[derive(Reflect)] +/// struct Slider { +/// #[reflect(@RangeInclusive::::new(0.0, 1.0))] +/// value: f32 +/// } +/// +/// let TypeInfo::Struct(info) = ::type_info() else { +/// panic!("expected struct info"); +/// }; +/// +/// let range = info.field("value").unwrap().get_attribute::>().unwrap(); +/// assert_eq!(0.0..=1.0, *range); +/// ``` +/// +/// [`Reflect` derive macro]: derive@crate::Reflect +#[derive(Default)] +pub struct CustomAttributes { + attributes: TypeIdMap, +} + +impl CustomAttributes { + /// Inserts a custom attribute into the collection. + /// + /// Note that this will overwrite any existing attribute of the same type. + pub fn with_attribute(mut self, value: T) -> Self { + self.attributes + .insert(TypeId::of::(), CustomAttribute::new(value)); + + self + } + + /// Returns `true` if this collection contains a custom attribute of the specified type. + pub fn contains(&self) -> bool { + self.attributes.contains_key(&TypeId::of::()) + } + + /// Returns `true` if this collection contains a custom attribute with the specified [`TypeId`]. + pub fn contains_by_id(&self, id: TypeId) -> bool { + self.attributes.contains_key(&id) + } + + /// Gets a custom attribute by type. + pub fn get(&self) -> Option<&T> { + self.attributes.get(&TypeId::of::())?.value::() + } + + /// Gets a custom attribute by its [`TypeId`]. + pub fn get_by_id(&self, id: TypeId) -> Option<&dyn Reflect> { + Some(self.attributes.get(&id)?.reflect_value()) + } + + /// Returns an iterator over all custom attributes. + pub fn iter(&self) -> impl ExactSizeIterator { + self.attributes + .iter() + .map(|(key, value)| (key, value.reflect_value())) + } + + /// Returns the number of custom attributes in this collection. + pub fn len(&self) -> usize { + self.attributes.len() + } + + /// Returns `true` if this collection is empty. + pub fn is_empty(&self) -> bool { + self.attributes.is_empty() + } +} + +impl Debug for CustomAttributes { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.debug_set().entries(self.attributes.values()).finish() + } +} + +struct CustomAttribute { + value: Box, +} + +impl CustomAttribute { + pub fn new(value: T) -> Self { + Self { + value: Box::new(value), + } + } + + pub fn value(&self) -> Option<&T> { + self.value.downcast_ref() + } + + pub fn reflect_value(&self) -> &dyn Reflect { + &*self.value + } +} + +impl Debug for CustomAttribute { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + self.value.debug(f) + } +} + +/// Implements methods for accessing custom attributes. +/// +/// Implements the following methods: +/// +/// * `fn custom_attributes(&self) -> &CustomAttributes` +/// * `fn get_attribute(&self) -> Option<&T>` +/// * `fn get_attribute_by_id(&self, id: TypeId) -> Option<&dyn Reflect>` +/// * `fn has_attribute(&self) -> bool` +/// * `fn has_attribute_by_id(&self, id: TypeId) -> bool` +/// +/// # Params +/// +/// * `$self` - The name of the variable containing the custom attributes (usually `self`). +/// * `$attributes` - The name of the field containing the [`CustomAttributes`]. +/// * `$term` - (Optional) The term used to describe the type containing the custom attributes. +/// This is purely used to generate better documentation. Defaults to `"item"`. +/// +macro_rules! impl_custom_attribute_methods { + ($self:ident . $attributes:ident, $term:literal) => { + $crate::attributes::impl_custom_attribute_methods!($self, &$self.$attributes, "item"); + }; + ($self:ident, $attributes:expr, $term:literal) => { + #[doc = concat!("Returns the custom attributes for this ", $term, ".")] + pub fn custom_attributes(&$self) -> &$crate::attributes::CustomAttributes { + $attributes + } + + /// Gets a custom attribute by type. + /// + /// For dynamically accessing an attribute, see [`get_attribute_by_id`](Self::get_attribute_by_id). + pub fn get_attribute(&$self) -> Option<&T> { + $self.custom_attributes().get::() + } + + /// Gets a custom attribute by its [`TypeId`](std::any::TypeId). + /// + /// This is the dynamic equivalent of [`get_attribute`](Self::get_attribute). + pub fn get_attribute_by_id(&$self, id: ::std::any::TypeId) -> Option<&dyn $crate::Reflect> { + $self.custom_attributes().get_by_id(id) + } + + #[doc = concat!("Returns `true` if this ", $term, " has a custom attribute of the specified type.")] + #[doc = "\n\nFor dynamically checking if an attribute exists, see [`has_attribute_by_id`](Self::has_attribute_by_id)."] + pub fn has_attribute(&$self) -> bool { + $self.custom_attributes().contains::() + } + + #[doc = concat!("Returns `true` if this ", $term, " has a custom attribute with the specified [`TypeId`](::std::any::TypeId).")] + #[doc = "\n\nThis is the dynamic equivalent of [`has_attribute`](Self::has_attribute)"] + pub fn has_attribute_by_id(&$self, id: ::std::any::TypeId) -> bool { + $self.custom_attributes().contains_by_id(id) + } + }; +} + +pub(crate) use impl_custom_attribute_methods; + +#[cfg(test)] +mod tests { + use super::*; + use crate as bevy_reflect; + use crate::type_info::Typed; + use crate::{TypeInfo, VariantInfo}; + use std::ops::RangeInclusive; + + #[derive(Reflect, PartialEq, Debug)] + struct Tooltip(String); + + impl Tooltip { + fn new(value: impl Into) -> Self { + Self(value.into()) + } + } + + #[test] + fn should_get_custom_attribute() { + let attributes = CustomAttributes::default().with_attribute(0.0..=1.0); + + let value = attributes.get::>().unwrap(); + assert_eq!(&(0.0..=1.0), value); + } + + #[test] + fn should_get_custom_attribute_dynamically() { + let attributes = CustomAttributes::default().with_attribute(String::from("Hello, World!")); + + let value = attributes.get_by_id(TypeId::of::()).unwrap(); + assert!(value + .reflect_partial_eq(&String::from("Hello, World!")) + .unwrap()); + } + + #[test] + fn should_debug_custom_attributes() { + let attributes = CustomAttributes::default().with_attribute("My awesome custom attribute!"); + + let debug = format!("{:?}", attributes); + + assert_eq!(r#"{"My awesome custom attribute!"}"#, debug); + + #[derive(Reflect)] + struct Foo { + value: i32, + } + + let attributes = CustomAttributes::default().with_attribute(Foo { value: 42 }); + + let debug = format!("{:?}", attributes); + + assert_eq!( + r#"{bevy_reflect::attributes::tests::Foo { value: 42 }}"#, + debug + ); + } + + #[test] + fn should_derive_custom_attributes_on_struct_container() { + #[derive(Reflect)] + #[reflect(@Tooltip::new("My awesome custom attribute!"))] + struct Slider { + value: f32, + } + + let TypeInfo::Struct(info) = Slider::type_info() else { + panic!("expected struct info"); + }; + + let tooltip = info.get_attribute::().unwrap(); + assert_eq!(&Tooltip::new("My awesome custom attribute!"), tooltip); + } + + #[test] + fn should_derive_custom_attributes_on_struct_fields() { + #[derive(Reflect)] + struct Slider { + #[reflect(@0.0..=1.0)] + #[reflect(@Tooltip::new("Range: 0.0 to 1.0"))] + value: f32, + } + + let TypeInfo::Struct(info) = Slider::type_info() else { + panic!("expected struct info"); + }; + + let field = info.field("value").unwrap(); + + let range = field.get_attribute::>().unwrap(); + assert_eq!(&(0.0..=1.0), range); + + let tooltip = field.get_attribute::().unwrap(); + assert_eq!(&Tooltip::new("Range: 0.0 to 1.0"), tooltip); + } + + #[test] + fn should_derive_custom_attributes_on_tuple_container() { + #[derive(Reflect)] + #[reflect(@Tooltip::new("My awesome custom attribute!"))] + struct Slider(f32); + + let TypeInfo::TupleStruct(info) = Slider::type_info() else { + panic!("expected tuple struct info"); + }; + + let tooltip = info.get_attribute::().unwrap(); + assert_eq!(&Tooltip::new("My awesome custom attribute!"), tooltip); + } + + #[test] + fn should_derive_custom_attributes_on_tuple_struct_fields() { + #[derive(Reflect)] + struct Slider( + #[reflect(@0.0..=1.0)] + #[reflect(@Tooltip::new("Range: 0.0 to 1.0"))] + f32, + ); + + let TypeInfo::TupleStruct(info) = Slider::type_info() else { + panic!("expected tuple struct info"); + }; + + let field = info.field_at(0).unwrap(); + + let range = field.get_attribute::>().unwrap(); + assert_eq!(&(0.0..=1.0), range); + + let tooltip = field.get_attribute::().unwrap(); + assert_eq!(&Tooltip::new("Range: 0.0 to 1.0"), tooltip); + } + + #[test] + fn should_derive_custom_attributes_on_enum_container() { + #[derive(Reflect)] + #[reflect(@Tooltip::new("My awesome custom attribute!"))] + enum Color { + Transparent, + Grayscale(f32), + Rgb { r: u8, g: u8, b: u8 }, + } + + let TypeInfo::Enum(info) = Color::type_info() else { + panic!("expected enum info"); + }; + + let tooltip = info.get_attribute::().unwrap(); + assert_eq!(&Tooltip::new("My awesome custom attribute!"), tooltip); + } + + #[test] + fn should_derive_custom_attributes_on_enum_variants() { + #[derive(Reflect, Debug, PartialEq)] + enum Display { + Toggle, + Slider, + Picker, + } + + #[derive(Reflect)] + enum Color { + #[reflect(@Display::Toggle)] + Transparent, + #[reflect(@Display::Slider)] + Grayscale(f32), + #[reflect(@Display::Picker)] + Rgb { r: u8, g: u8, b: u8 }, + } + + let TypeInfo::Enum(info) = Color::type_info() else { + panic!("expected enum info"); + }; + + let VariantInfo::Unit(transparent_variant) = info.variant("Transparent").unwrap() else { + panic!("expected unit variant"); + }; + + let display = transparent_variant.get_attribute::().unwrap(); + assert_eq!(&Display::Toggle, display); + + let VariantInfo::Tuple(grayscale_variant) = info.variant("Grayscale").unwrap() else { + panic!("expected tuple variant"); + }; + + let display = grayscale_variant.get_attribute::().unwrap(); + assert_eq!(&Display::Slider, display); + + let VariantInfo::Struct(rgb_variant) = info.variant("Rgb").unwrap() else { + panic!("expected struct variant"); + }; + + let display = rgb_variant.get_attribute::().unwrap(); + assert_eq!(&Display::Picker, display); + } + + #[test] + fn should_derive_custom_attributes_on_enum_variant_fields() { + #[derive(Reflect)] + enum Color { + Transparent, + Grayscale(#[reflect(@0.0..=1.0_f32)] f32), + Rgb { + #[reflect(@0..=255u8)] + r: u8, + #[reflect(@0..=255u8)] + g: u8, + #[reflect(@0..=255u8)] + b: u8, + }, + } + + let TypeInfo::Enum(info) = Color::type_info() else { + panic!("expected enum info"); + }; + + let VariantInfo::Tuple(grayscale_variant) = info.variant("Grayscale").unwrap() else { + panic!("expected tuple variant"); + }; + + let field = grayscale_variant.field_at(0).unwrap(); + + let range = field.get_attribute::>().unwrap(); + assert_eq!(&(0.0..=1.0), range); + + let VariantInfo::Struct(rgb_variant) = info.variant("Rgb").unwrap() else { + panic!("expected struct variant"); + }; + + let field = rgb_variant.field("g").unwrap(); + + let range = field.get_attribute::>().unwrap(); + assert_eq!(&(0..=255), range); + } + + #[test] + fn should_allow_unit_struct_attribute_values() { + #[derive(Reflect)] + struct Required; + + #[derive(Reflect)] + struct Foo { + #[reflect(@Required)] + value: i32, + } + + let TypeInfo::Struct(info) = Foo::type_info() else { + panic!("expected struct info"); + }; + + let field = info.field("value").unwrap(); + assert!(field.has_attribute::()); + } + + #[test] + fn should_accept_last_attribute() { + #[derive(Reflect)] + struct Foo { + #[reflect(@false)] + #[reflect(@true)] + value: i32, + } + + let TypeInfo::Struct(info) = Foo::type_info() else { + panic!("expected struct info"); + }; + + let field = info.field("value").unwrap(); + assert!(field.get_attribute::().unwrap()); + } +} diff --git a/crates/bevy_reflect/src/enums/enum_trait.rs b/crates/bevy_reflect/src/enums/enum_trait.rs index 66029923da..cddc6df97d 100644 --- a/crates/bevy_reflect/src/enums/enum_trait.rs +++ b/crates/bevy_reflect/src/enums/enum_trait.rs @@ -1,7 +1,9 @@ +use crate::attributes::{impl_custom_attribute_methods, CustomAttributes}; use crate::{DynamicEnum, Reflect, TypePath, TypePathTable, VariantInfo, VariantType}; use bevy_utils::HashMap; use std::any::{Any, TypeId}; use std::slice::Iter; +use std::sync::Arc; /// A trait used to power [enum-like] operations via [reflection]. /// @@ -138,6 +140,7 @@ pub struct EnumInfo { variants: Box<[VariantInfo]>, variant_names: Box<[&'static str]>, variant_indices: HashMap<&'static str, usize>, + custom_attributes: Arc, #[cfg(feature = "documentation")] docs: Option<&'static str>, } @@ -164,6 +167,7 @@ impl EnumInfo { variants: variants.to_vec().into_boxed_slice(), variant_names, variant_indices, + custom_attributes: Arc::new(CustomAttributes::default()), #[cfg(feature = "documentation")] docs: None, } @@ -175,6 +179,14 @@ impl EnumInfo { Self { docs, ..self } } + /// Sets the custom attributes for this enum. + pub fn with_custom_attributes(self, custom_attributes: CustomAttributes) -> Self { + Self { + custom_attributes: Arc::new(custom_attributes), + ..self + } + } + /// A slice containing the names of all variants in order. pub fn variant_names(&self) -> &[&'static str] { &self.variant_names @@ -251,6 +263,8 @@ impl EnumInfo { pub fn docs(&self) -> Option<&'static str> { self.docs } + + impl_custom_attribute_methods!(self.custom_attributes, "enum"); } /// An iterator over the fields in the current enum variant. diff --git a/crates/bevy_reflect/src/enums/variants.rs b/crates/bevy_reflect/src/enums/variants.rs index 6901474041..08a7ac7b44 100644 --- a/crates/bevy_reflect/src/enums/variants.rs +++ b/crates/bevy_reflect/src/enums/variants.rs @@ -1,6 +1,8 @@ +use crate::attributes::{impl_custom_attribute_methods, CustomAttributes}; use crate::{NamedField, UnnamedField}; use bevy_utils::HashMap; use std::slice::Iter; +use std::sync::Arc; /// Describes the form of an enum variant. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] @@ -82,6 +84,16 @@ impl VariantInfo { Self::Unit(info) => info.docs(), } } + + impl_custom_attribute_methods!( + self, + match self { + Self::Struct(info) => info.custom_attributes(), + Self::Tuple(info) => info.custom_attributes(), + Self::Unit(info) => info.custom_attributes(), + }, + "variant" + ); } /// Type info for struct variants. @@ -91,6 +103,7 @@ pub struct StructVariantInfo { fields: Box<[NamedField]>, field_names: Box<[&'static str]>, field_indices: HashMap<&'static str, usize>, + custom_attributes: Arc, #[cfg(feature = "documentation")] docs: Option<&'static str>, } @@ -105,6 +118,7 @@ impl StructVariantInfo { fields: fields.to_vec().into_boxed_slice(), field_names, field_indices, + custom_attributes: Arc::new(CustomAttributes::default()), #[cfg(feature = "documentation")] docs: None, } @@ -116,6 +130,14 @@ impl StructVariantInfo { Self { docs, ..self } } + /// Sets the custom attributes for this variant. + pub fn with_custom_attributes(self, custom_attributes: CustomAttributes) -> Self { + Self { + custom_attributes: Arc::new(custom_attributes), + ..self + } + } + /// The name of this variant. pub fn name(&self) -> &'static str { self.name @@ -166,6 +188,8 @@ impl StructVariantInfo { pub fn docs(&self) -> Option<&'static str> { self.docs } + + impl_custom_attribute_methods!(self.custom_attributes, "variant"); } /// Type info for tuple variants. @@ -173,6 +197,7 @@ impl StructVariantInfo { pub struct TupleVariantInfo { name: &'static str, fields: Box<[UnnamedField]>, + custom_attributes: Arc, #[cfg(feature = "documentation")] docs: Option<&'static str>, } @@ -183,6 +208,7 @@ impl TupleVariantInfo { Self { name, fields: fields.to_vec().into_boxed_slice(), + custom_attributes: Arc::new(CustomAttributes::default()), #[cfg(feature = "documentation")] docs: None, } @@ -194,6 +220,14 @@ impl TupleVariantInfo { Self { docs, ..self } } + /// Sets the custom attributes for this variant. + pub fn with_custom_attributes(self, custom_attributes: CustomAttributes) -> Self { + Self { + custom_attributes: Arc::new(custom_attributes), + ..self + } + } + /// The name of this variant. pub fn name(&self) -> &'static str { self.name @@ -219,12 +253,15 @@ impl TupleVariantInfo { pub fn docs(&self) -> Option<&'static str> { self.docs } + + impl_custom_attribute_methods!(self.custom_attributes, "variant"); } /// Type info for unit variants. #[derive(Clone, Debug)] pub struct UnitVariantInfo { name: &'static str, + custom_attributes: Arc, #[cfg(feature = "documentation")] docs: Option<&'static str>, } @@ -234,6 +271,7 @@ impl UnitVariantInfo { pub fn new(name: &'static str) -> Self { Self { name, + custom_attributes: Arc::new(CustomAttributes::default()), #[cfg(feature = "documentation")] docs: None, } @@ -245,6 +283,14 @@ impl UnitVariantInfo { Self { docs, ..self } } + /// Sets the custom attributes for this variant. + pub fn with_custom_attributes(self, custom_attributes: CustomAttributes) -> Self { + Self { + custom_attributes: Arc::new(custom_attributes), + ..self + } + } + /// The name of this variant. pub fn name(&self) -> &'static str { self.name @@ -255,4 +301,6 @@ impl UnitVariantInfo { pub fn docs(&self) -> Option<&'static str> { self.docs } + + impl_custom_attribute_methods!(self.custom_attributes, "variant"); } diff --git a/crates/bevy_reflect/src/fields.rs b/crates/bevy_reflect/src/fields.rs index 763d04ab2b..31855aeb78 100644 --- a/crates/bevy_reflect/src/fields.rs +++ b/crates/bevy_reflect/src/fields.rs @@ -1,5 +1,7 @@ +use crate::attributes::{impl_custom_attribute_methods, CustomAttributes}; use crate::{Reflect, TypePath, TypePathTable}; use std::any::{Any, TypeId}; +use std::sync::Arc; /// The named field of a reflected struct. #[derive(Clone, Debug)] @@ -7,6 +9,7 @@ pub struct NamedField { name: &'static str, type_path: TypePathTable, type_id: TypeId, + custom_attributes: Arc, #[cfg(feature = "documentation")] docs: Option<&'static str>, } @@ -18,6 +21,7 @@ impl NamedField { name, type_path: TypePathTable::of::(), type_id: TypeId::of::(), + custom_attributes: Arc::new(CustomAttributes::default()), #[cfg(feature = "documentation")] docs: None, } @@ -29,6 +33,14 @@ impl NamedField { Self { docs, ..self } } + /// Sets the custom attributes for this field. + pub fn with_custom_attributes(self, custom_attributes: CustomAttributes) -> Self { + Self { + custom_attributes: Arc::new(custom_attributes), + ..self + } + } + /// The name of the field. pub fn name(&self) -> &'static str { self.name @@ -66,6 +78,8 @@ impl NamedField { pub fn docs(&self) -> Option<&'static str> { self.docs } + + impl_custom_attribute_methods!(self.custom_attributes, "field"); } /// The unnamed field of a reflected tuple or tuple struct. @@ -74,6 +88,7 @@ pub struct UnnamedField { index: usize, type_path: TypePathTable, type_id: TypeId, + custom_attributes: Arc, #[cfg(feature = "documentation")] docs: Option<&'static str>, } @@ -84,6 +99,7 @@ impl UnnamedField { index, type_path: TypePathTable::of::(), type_id: TypeId::of::(), + custom_attributes: Arc::new(CustomAttributes::default()), #[cfg(feature = "documentation")] docs: None, } @@ -95,6 +111,14 @@ impl UnnamedField { Self { docs, ..self } } + /// Sets the custom attributes for this field. + pub fn with_custom_attributes(self, custom_attributes: CustomAttributes) -> Self { + Self { + custom_attributes: Arc::new(custom_attributes), + ..self + } + } + /// Returns the index of the field. pub fn index(&self) -> usize { self.index @@ -132,4 +156,6 @@ impl UnnamedField { pub fn docs(&self) -> Option<&'static str> { self.docs } + + impl_custom_attribute_methods!(self.custom_attributes, "field"); } diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index aa4918de51..1ba4d1a3ec 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -512,6 +512,7 @@ mod impls { mod uuid; } +pub mod attributes; mod enums; pub mod serde; pub mod std_traits; diff --git a/crates/bevy_reflect/src/struct_trait.rs b/crates/bevy_reflect/src/struct_trait.rs index 5d13403469..4585a4382f 100644 --- a/crates/bevy_reflect/src/struct_trait.rs +++ b/crates/bevy_reflect/src/struct_trait.rs @@ -1,3 +1,4 @@ +use crate::attributes::{impl_custom_attribute_methods, CustomAttributes}; use crate::{ self as bevy_reflect, ApplyError, NamedField, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, TypePath, TypePathTable, @@ -5,6 +6,7 @@ use crate::{ use bevy_reflect_derive::impl_type_path; use bevy_utils::HashMap; use std::fmt::{Debug, Formatter}; +use std::sync::Arc; use std::{ any::{Any, TypeId}, borrow::Cow, @@ -81,6 +83,7 @@ pub struct StructInfo { fields: Box<[NamedField]>, field_names: Box<[&'static str]>, field_indices: HashMap<&'static str, usize>, + custom_attributes: Arc, #[cfg(feature = "documentation")] docs: Option<&'static str>, } @@ -107,6 +110,7 @@ impl StructInfo { fields: fields.to_vec().into_boxed_slice(), field_names, field_indices, + custom_attributes: Arc::new(CustomAttributes::default()), #[cfg(feature = "documentation")] docs: None, } @@ -118,6 +122,14 @@ impl StructInfo { Self { docs, ..self } } + /// Sets the custom attributes for this struct. + pub fn with_custom_attributes(self, custom_attributes: CustomAttributes) -> Self { + Self { + custom_attributes: Arc::new(custom_attributes), + ..self + } + } + /// A slice containing the names of all fields in order. pub fn field_names(&self) -> &[&'static str] { &self.field_names @@ -182,6 +194,8 @@ impl StructInfo { pub fn docs(&self) -> Option<&'static str> { self.docs } + + impl_custom_attribute_methods!(self.custom_attributes, "struct"); } /// An iterator over the field values of a struct. diff --git a/crates/bevy_reflect/src/tuple_struct.rs b/crates/bevy_reflect/src/tuple_struct.rs index 8aeb103984..56767cd0e1 100644 --- a/crates/bevy_reflect/src/tuple_struct.rs +++ b/crates/bevy_reflect/src/tuple_struct.rs @@ -1,5 +1,6 @@ use bevy_reflect_derive::impl_type_path; +use crate::attributes::{impl_custom_attribute_methods, CustomAttributes}; use crate::{ self as bevy_reflect, ApplyError, DynamicTuple, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, Tuple, TypeInfo, TypePath, TypePathTable, UnnamedField, @@ -7,6 +8,7 @@ use crate::{ use std::any::{Any, TypeId}; use std::fmt::{Debug, Formatter}; use std::slice::Iter; +use std::sync::Arc; /// A trait used to power [tuple struct-like] operations via [reflection]. /// @@ -59,6 +61,7 @@ pub struct TupleStructInfo { type_path: TypePathTable, type_id: TypeId, fields: Box<[UnnamedField]>, + custom_attributes: Arc, #[cfg(feature = "documentation")] docs: Option<&'static str>, } @@ -75,6 +78,7 @@ impl TupleStructInfo { type_path: TypePathTable::of::(), type_id: TypeId::of::(), fields: fields.to_vec().into_boxed_slice(), + custom_attributes: Arc::new(CustomAttributes::default()), #[cfg(feature = "documentation")] docs: None, } @@ -86,6 +90,14 @@ impl TupleStructInfo { Self { docs, ..self } } + /// Sets the custom attributes for this struct. + pub fn with_custom_attributes(self, custom_attributes: CustomAttributes) -> Self { + Self { + custom_attributes: Arc::new(custom_attributes), + ..self + } + } + /// Get the field at the given index. pub fn field_at(&self, index: usize) -> Option<&UnnamedField> { self.fields.get(index) @@ -133,6 +145,8 @@ impl TupleStructInfo { pub fn docs(&self) -> Option<&'static str> { self.docs } + + impl_custom_attribute_methods!(self.custom_attributes, "struct"); } /// An iterator over the field values of a tuple struct. diff --git a/examples/README.md b/examples/README.md index 4a0abc3e44..966f2552ce 100644 --- a/examples/README.md +++ b/examples/README.md @@ -323,6 +323,7 @@ Example | Description Example | Description --- | --- +[Custom Attributes](../examples/reflection/custom_attributes.rs) | Registering and accessing custom attributes on reflected types [Dynamic Types](../examples/reflection/dynamic_types.rs) | How dynamic types are used with reflection [Generic Reflection](../examples/reflection/generic_reflection.rs) | Registers concrete instances of generic types that may be used with reflection [Reflection](../examples/reflection/reflection.rs) | Demonstrates how reflection in Bevy provides a way to dynamically interact with Rust types diff --git a/examples/reflection/custom_attributes.rs b/examples/reflection/custom_attributes.rs new file mode 100644 index 0000000000..d89783c85f --- /dev/null +++ b/examples/reflection/custom_attributes.rs @@ -0,0 +1,90 @@ +//! Demonstrates how to register and access custom attributes on reflected types. + +use bevy::reflect::{Reflect, TypeInfo, Typed}; +use std::any::TypeId; +use std::ops::RangeInclusive; + +fn main() { + // Bevy supports statically registering custom attribute data on reflected types, + // which can then be accessed at runtime via the type's `TypeInfo`. + // Attributes are registered using the `#[reflect(@...)]` syntax, + // where the `...` is any expression that resolves to a value which implements `Reflect`. + // Note that these attributes are stored based on their type: + // if two attributes have the same type, the second one will overwrite the first. + + // Here is an example of registering custom attributes on a type: + #[derive(Reflect)] + struct Slider { + #[reflect(@RangeInclusive::::new(0.0, 1.0))] + // Alternatively, we could have used the `0.0..=1.0` syntax, + // but remember to ensure the type is the one you want! + #[reflect(@0.0..=1.0_f32)] + value: f32, + } + + // Now, we can access the custom attributes at runtime: + let TypeInfo::Struct(type_info) = Slider::type_info() else { + panic!("expected struct"); + }; + + let field = type_info.field("value").unwrap(); + + let range = field.get_attribute::>().unwrap(); + assert_eq!(*range, 0.0..=1.0); + + // And remember that our attributes can be any type that implements `Reflect`: + #[derive(Reflect)] + struct Required; + + #[derive(Reflect, PartialEq, Debug)] + struct Tooltip(String); + + impl Tooltip { + fn new(text: &str) -> Self { + Self(text.to_string()) + } + } + + #[derive(Reflect)] + #[reflect(@Required, @Tooltip::new("An ID is required!"))] + struct Id(u8); + + let TypeInfo::TupleStruct(type_info) = Id::type_info() else { + panic!("expected struct"); + }; + + // We can check if an attribute simply exists on our type: + assert!(type_info.has_attribute::()); + + // We can also get attribute data dynamically: + let some_type_id = TypeId::of::(); + + let tooltip: &dyn Reflect = type_info.get_attribute_by_id(some_type_id).unwrap(); + assert_eq!( + tooltip.downcast_ref::(), + Some(&Tooltip::new("An ID is required!")) + ); + + // And again, attributes of the same type will overwrite each other: + #[derive(Reflect)] + enum Status { + // This will result in `false` being stored: + #[reflect(@true)] + #[reflect(@false)] + Disabled, + // This will result in `true` being stored: + #[reflect(@false)] + #[reflect(@true)] + Enabled, + } + + let TypeInfo::Enum(type_info) = Status::type_info() else { + panic!("expected enum"); + }; + + let disabled = type_info.variant("Disabled").unwrap(); + assert!(!disabled.get_attribute::().unwrap()); + + let enabled = type_info.variant("Enabled").unwrap(); + assert!(enabled.get_attribute::().unwrap()); +}