Skip to content

Commit

Permalink
bevy_reflect: Custom attributes (#11659)
Browse files Browse the repository at this point in the history
# 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")]`)
  • Loading branch information
MrGVSV authored May 20, 2024
1 parent 2aed777 commit 5db5266
Show file tree
Hide file tree
Showing 18 changed files with 1,008 additions and 187 deletions.
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 9 additions & 1 deletion crates/bevy_reflect/derive/src/container_attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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>,
}

Expand Down Expand Up @@ -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_)
Expand Down Expand Up @@ -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()
Expand Down
42 changes: 42 additions & 0 deletions crates/bevy_reflect/derive/src/custom_attributes.rs
Original file line number Diff line number Diff line change
@@ -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()?)
}
}
173 changes: 173 additions & 0 deletions crates/bevy_reflect/derive/src/derive_data.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use core::fmt;
use proc_macro2::Span;

use crate::container_attributes::{ContainerAttributes, FromReflectAttrs, TypePathAttrs};
use crate::field_attributes::FieldAttributes;
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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> {
Expand All @@ -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.
Expand Down
16 changes: 15 additions & 1 deletion crates/bevy_reflect/derive/src/field_attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}
Loading

0 comments on commit 5db5266

Please sign in to comment.