mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
5db52663b3
# 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")]`)
90 lines
2.9 KiB
Rust
90 lines
2.9 KiB
Rust
//! 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());
|
|
}
|