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::<RangeInclusive<f64>>().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.

<details>
<summary>List</summary>

##### 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,
}
```

</details>

---

## Changelog

- Added support for custom attributes on reflected types (i.e.
`#[reflect(@Foo::new("bar")]`)
This commit is contained in:
Gino Valente 2024-05-20 12:30:21 -07:00 committed by GitHub
parent 2aed777435
commit 5db52663b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1008 additions and 187 deletions

View file

@ -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"

View file

@ -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<WhereClause>,
no_field_bounds: bool,
custom_attributes: CustomAttributes,
idents: Vec<Ident>,
}
@ -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()

View file

@ -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<Expr>,
}
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::<Token![@]>()?;
self.push(input.parse()?)
}
}

View file

@ -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::<Self>(&[
#(#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::<Self>(&[
#(#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.

View file

@ -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)
}
}

View file

@ -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::<Self>(&variants).with_docs(#doc)
}
};
#[cfg(not(feature = "documentation"))]
let info_generator = {
quote! {
#bevy_reflect_path::EnumInfo::new::<Self>(&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<proc_macro2::TokenStream>,
enum_field: Vec<proc_macro2::TokenStream>,
enum_field_at: Vec<proc_macro2::TokenStream>,
enum_index_of: Vec<proc_macro2::TokenStream>,
@ -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<proc_macro2::TokenStream> {
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<proc_macro2::TokenStream> = 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<proc_macro2::TokenStream> = 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<proc_macro2::TokenStream> = 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,

View file

@ -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::<Vec<_>>();
let field_types = reflect_struct.active_types();
let field_count = field_idents.len();
let field_indices = (0..field_count).collect::<Vec<usize>>();
@ -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::<Self>(&fields).with_docs(#doc)
}
};
#[cfg(not(feature = "documentation"))]
let info_generator = {
quote! {
#bevy_reflect_path::StructInfo::new::<Self>(&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());

View file

@ -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::<Vec<_>>();
let field_types = reflect_struct.active_types();
let field_count = field_idents.len();
let field_indices = (0..field_count).collect::<Vec<usize>>();
@ -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::<Self>(&fields).with_docs(#doc)
}
};
#[cfg(not(feature = "documentation"))]
let info_generator = {
quote! {
#bevy_reflect_path::TupleStructInfo::new::<Self>(&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());

View file

@ -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 {

View file

@ -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::<f32>::new(0.0, 1.0))]
/// value: f32
/// }
///
/// let TypeInfo::Struct(info) = <Slider as Typed>::type_info() else {
/// panic!("expected struct info");
/// };
///
/// let range = info.field("value").unwrap().get_attribute::<RangeInclusive<f32>>().unwrap();
/// assert_eq!(0.0..=1.0, *range);
/// ```
///
/// [`Reflect` derive macro]: derive@crate::Reflect
#[derive(Default)]
pub struct CustomAttributes {
attributes: TypeIdMap<CustomAttribute>,
}
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<T: Reflect>(mut self, value: T) -> Self {
self.attributes
.insert(TypeId::of::<T>(), CustomAttribute::new(value));
self
}
/// Returns `true` if this collection contains a custom attribute of the specified type.
pub fn contains<T: Reflect>(&self) -> bool {
self.attributes.contains_key(&TypeId::of::<T>())
}
/// 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<T: Reflect>(&self) -> Option<&T> {
self.attributes.get(&TypeId::of::<T>())?.value::<T>()
}
/// 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<Item = (&TypeId, &dyn Reflect)> {
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<dyn Reflect>,
}
impl CustomAttribute {
pub fn new<T: Reflect>(value: T) -> Self {
Self {
value: Box::new(value),
}
}
pub fn value<T: Reflect>(&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<T: Reflect>(&self) -> Option<&T>`
/// * `fn get_attribute_by_id(&self, id: TypeId) -> Option<&dyn Reflect>`
/// * `fn has_attribute<T: Reflect>(&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<T: $crate::Reflect>(&$self) -> Option<&T> {
$self.custom_attributes().get::<T>()
}
/// 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<T: $crate::Reflect>(&$self) -> bool {
$self.custom_attributes().contains::<T>()
}
#[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<String>) -> Self {
Self(value.into())
}
}
#[test]
fn should_get_custom_attribute() {
let attributes = CustomAttributes::default().with_attribute(0.0..=1.0);
let value = attributes.get::<RangeInclusive<f64>>().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::<String>()).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::<Tooltip>().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::<RangeInclusive<f64>>().unwrap();
assert_eq!(&(0.0..=1.0), range);
let tooltip = field.get_attribute::<Tooltip>().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::<Tooltip>().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::<RangeInclusive<f64>>().unwrap();
assert_eq!(&(0.0..=1.0), range);
let tooltip = field.get_attribute::<Tooltip>().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::<Tooltip>().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::<Display>().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::<Display>().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::<Display>().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::<RangeInclusive<f32>>().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::<RangeInclusive<u8>>().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::<Required>());
}
#[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::<bool>().unwrap());
}
}

View file

@ -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<CustomAttributes>,
#[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.

View file

@ -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<CustomAttributes>,
#[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<CustomAttributes>,
#[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<CustomAttributes>,
#[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");
}

View file

@ -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<CustomAttributes>,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
@ -18,6 +21,7 @@ impl NamedField {
name,
type_path: TypePathTable::of::<T>(),
type_id: TypeId::of::<T>(),
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<CustomAttributes>,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
@ -84,6 +99,7 @@ impl UnnamedField {
index,
type_path: TypePathTable::of::<T>(),
type_id: TypeId::of::<T>(),
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");
}

View file

@ -512,6 +512,7 @@ mod impls {
mod uuid;
}
pub mod attributes;
mod enums;
pub mod serde;
pub mod std_traits;

View file

@ -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<CustomAttributes>,
#[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.

View file

@ -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<CustomAttributes>,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
@ -75,6 +78,7 @@ impl TupleStructInfo {
type_path: TypePathTable::of::<T>(),
type_id: TypeId::of::<T>(),
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.

View file

@ -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

View file

@ -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::<f32>::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::<RangeInclusive<f32>>().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::<Required>());
// We can also get attribute data dynamically:
let some_type_id = TypeId::of::<Tooltip>();
let tooltip: &dyn Reflect = type_info.get_attribute_by_id(some_type_id).unwrap();
assert_eq!(
tooltip.downcast_ref::<Tooltip>(),
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::<bool>().unwrap());
let enabled = type_info.variant("Enabled").unwrap();
assert!(enabled.get_attribute::<bool>().unwrap());
}