From 5db52663b33cb7e4f77635eec312980df31e7d44 Mon Sep 17 00:00:00 2001 From: Gino Valente <49806985+MrGVSV@users.noreply.github.com> Date: Mon, 20 May 2024 12:30:21 -0700 Subject: [PATCH] bevy_reflect: Custom attributes (#11659) # Objective As work on the editor starts to ramp up, it might be nice to start allowing types to specify custom attributes. These can be used to provide certain functionality to fields, such as ranges or controlling how data is displayed. A good example of this can be seen in [`bevy-inspector-egui`](https://github.com/jakobhellermann/bevy-inspector-egui) with its [`InspectorOptions`](https://docs.rs/bevy-inspector-egui/0.22.1/bevy_inspector_egui/struct.InspectorOptions.html): ```rust #[derive(Reflect, Default, InspectorOptions)] #[reflect(InspectorOptions)] struct Slider { #[inspector(min = 0.0, max = 1.0)] value: f32, } ``` Normally, as demonstrated in the example above, these attributes are handled by a derive macro and stored in a corresponding `TypeData` struct (i.e. `ReflectInspectorOptions`). Ideally, we would have a good way of defining this directly via reflection so that users don't need to create and manage a whole proc macro just to allow these sorts of attributes. And note that this doesn't have to just be for inspectors and editors. It can be used for things done purely on the code side of things. ## Solution Create a new method for storing attributes on fields via the `Reflect` derive. These custom attributes are stored in type info (e.g. `NamedField`, `StructInfo`, etc.). ```rust #[derive(Reflect)] struct Slider { #[reflect(@0.0..=1.0)] value: f64, } 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!(*range, 0.0..=1.0); ``` ## TODO - [x] ~~Bikeshed syntax~~ Went with a type-based approach, prefixed by `@` for ease of parsing and flexibility - [x] Add support for custom struct/tuple struct field attributes - [x] Add support for custom enum variant field attributes - [x] ~~Add support for custom enum variant attributes (maybe?)~~ ~~Will require a larger refactor. Can be saved for a future PR if we really want it.~~ Actually, we apparently still have support for variant attributes despite not using them, so it was pretty easy to add lol. - [x] Add support for custom container attributes - [x] Allow custom attributes to store any reflectable value (not just `Lit`) - [x] ~~Store attributes in registry~~ This PR used to store these in attributes in the registry, however, it has since switched over to storing them in type info - [x] Add example ## Bikeshedding > [!note] > This section was made for the old method of handling custom attributes, which stored them by name (i.e. `some_attribute = 123`). The PR has shifted away from that, to a more type-safe approach. > > This section has been left for reference. There are a number of ways we can syntactically handle custom attributes. Feel free to leave a comment on your preferred one! Ideally we want one that is clear, readable, and concise since these will potentially see _a lot_ of use. Below is a small, non-exhaustive list of them. Note that the `skip_serializing` reflection attribute is added to demonstrate how each case plays with existing reflection attributes.
List ##### 1. `@(name = value)` > The `@` was chosen to make them stand out from other attributes and because the "at" symbol is a subtle pneumonic for "attribute". Of course, other symbols could be used (e.g. `$`, `#`, etc.). ```rust #[derive(Reflect)] struct Slider { #[reflect(@(min = 0.0, max = 1.0), skip_serializing)] #[[reflect(@(bevy_editor::hint = "Range: 0.0 to 1.0"))] value: f32, } ``` ##### 2. `@name = value` > This is my personal favorite. ```rust #[derive(Reflect)] struct Slider { #[reflect(@min = 0.0, @max = 1.0, skip_serializing)] #[[reflect(@bevy_editor::hint = "Range: 0.0 to 1.0")] value: f32, } ``` ##### 3. `custom_attr(name = value)` > `custom_attr` can be anything. Other possibilities include `with` or `tag`. ```rust #[derive(Reflect)] struct Slider { #[reflect(custom_attr(min = 0.0, max = 1.0), skip_serializing)] #[[reflect(custom_attr(bevy_editor::hint = "Range: 0.0 to 1.0"))] value: f32, } ``` ##### 4. `reflect_attr(name = value)` ```rust #[derive(Reflect)] struct Slider { #[reflect(skip_serializing)] #[reflect_attr(min = 0.0, max = 1.0)] #[[reflect_attr(bevy_editor::hint = "Range: 0.0 to 1.0")] value: f32, } ```
--- ## Changelog - Added support for custom attributes on reflected types (i.e. `#[reflect(@Foo::new("bar")]`) --- Cargo.toml | 11 + .../derive/src/container_attributes.rs | 10 +- .../derive/src/custom_attributes.rs | 42 ++ crates/bevy_reflect/derive/src/derive_data.rs | 173 +++++++ .../derive/src/field_attributes.rs | 16 +- crates/bevy_reflect/derive/src/impls/enums.rs | 152 ++---- .../bevy_reflect/derive/src/impls/structs.rs | 39 +- .../derive/src/impls/tuple_structs.rs | 39 +- crates/bevy_reflect/derive/src/lib.rs | 60 +++ crates/bevy_reflect/src/attributes.rs | 445 ++++++++++++++++++ crates/bevy_reflect/src/enums/enum_trait.rs | 14 + crates/bevy_reflect/src/enums/variants.rs | 48 ++ crates/bevy_reflect/src/fields.rs | 26 + crates/bevy_reflect/src/lib.rs | 1 + crates/bevy_reflect/src/struct_trait.rs | 14 + crates/bevy_reflect/src/tuple_struct.rs | 14 + examples/README.md | 1 + examples/reflection/custom_attributes.rs | 90 ++++ 18 files changed, 1008 insertions(+), 187 deletions(-) create mode 100644 crates/bevy_reflect/derive/src/custom_attributes.rs create mode 100644 crates/bevy_reflect/src/attributes.rs create mode 100644 examples/reflection/custom_attributes.rs diff --git a/Cargo.toml b/Cargo.toml index c219ccdd41d30..f5f95239f8dc4 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 e87e565940074..7b7a12752de11 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 0000000000000..68bb7d1843a7c --- /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 3c45b5ee3be15..77b33396519c3 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 6ab88260420ed..d66daf8382b4d 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 8293da7157785..ab72539a71be9 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 + }; + + f(field); + + field_len += 1; + } + + field_len } - 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 - ) - }); + match &variant.fields { + EnumVariantFields::Unit => { + let field_len = process_fields(&[], |_| {}); + enum_field_len.push(quote! { #unit{..} => #field_len }); - enum_variant_type.push(quote! { - #unit{..} => #bevy_reflect_path::VariantType::#variant_type_ident - }); - }; - - match &variant.fields { - EnumVariantFields::Unit => { - push_variant(variant, quote!(#name), 0); } 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 f51f0b2de4d23..249ac22745b08 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 255928cf97c6e..cf43e5fe9874d 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 743c558dded22..86e0069a887fb 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 0000000000000..9fb2e694e4a31 --- /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 66029923da25b..cddc6df97d07c 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 6901474041408..08a7ac7b44cf0 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 763d04ab2b7e6..31855aeb78228 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 aa4918de51097..1ba4d1a3ec3ac 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 5d134034691d6..4585a4382fd6d 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 8aeb103984029..56767cd0e106c 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 4a0abc3e449d8..966f2552cecfe 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 0000000000000..d89783c85f95f --- /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()); +}