Files
Homer Simpson f56a1eea3b Import Oxide engine (Stages 0–10) under MIT license
Full project snapshot migrated to new Gitea remote without history:
engine, editor, physics, script, examples, tests, docs, and assets.
Relicensed from GPLv3 to MIT and updated repo URLs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:41:02 +02:00

1007 lines
37 KiB
Rust

//! Reflection / type registry — generic, name-keyed access to components.
//!
//! The engine's *dual-editable types* principle says every component must be
//! readable and writable from the editor, from scripts, and from external tools
//! through **one** representation — without each of those callers knowing the
//! concrete Rust type. The [`TypeRegistry`] is that bridge.
//!
//! A type is registered **once** under a stable name:
//!
//! ```
//! use oxide_engine::reflect::TypeRegistry;
//! use oxide_engine::prelude::*;
//!
//! let mut registry = TypeRegistry::new();
//! registry.register::<Transform>("Transform");
//! registry.register::<Node>("Node");
//! ```
//!
//! From then on, any caller holding only the *name* can round-trip the component
//! on an entity as RON text — which is all a generic inspector or a script needs:
//!
//! ```
//! # use oxide_engine::reflect::TypeRegistry;
//! # use oxide_engine::prelude::*;
//! # let mut registry = TypeRegistry::new();
//! # registry.register::<Transform>("Transform");
//! let mut scene = Scene::new();
//! let e = scene.spawn("thing", Transform::IDENTITY);
//!
//! // Read it generically...
//! let ron = registry.get_ron(scene.world(), e, "Transform").unwrap();
//! // ...and write it back generically, no concrete type at the call site.
//! registry.set_ron(scene.world_mut(), e, "Transform", &ron).unwrap();
//! ```
//!
//! That path is **whole-value** reflection (the unit is one component,
//! serialized). On top of it, [`register_reflected`](TypeRegistry::register_reflected)
//! adds **per-field** reflection — named fields each addressable on their own —
//! for types that derive [`Reflect`], which is what a Unity/Godot-style
//! inspector needs to render one widget per field. Both coexist: the registry
//! addresses *types* by name, while [`Reflect`] addresses *fields* within a
//! value.
use std::collections::BTreeMap;
use hecs::{Component, Entity, World};
use serde::de::DeserializeOwned;
use serde::Serialize;
/// `#[derive(Reflect)]` — generates the [`Reflect`] impl for a struct's public
/// fields. Shares its name with the [`Reflect`] trait (macro vs. type
/// namespace), exactly like `serde`'s `Serialize`.
pub use oxide_engine_derive::Reflect;
/// `#[derive(ReflectEnum)]` — generates the [`ReflectEnum`] impl for a fieldless
/// enum, exposing its variant names for inspector dropdowns.
pub use oxide_engine_derive::ReflectEnum;
/// Errors from generic, name-keyed component access.
#[derive(Debug, thiserror::Error)]
pub enum ReflectError {
/// No type was registered under this name.
#[error("no registered type named '{0}'")]
UnknownType(String),
/// The entity is not live in the world.
#[error("entity is not live in this world")]
NoSuchEntity,
/// The entity is live but does not carry this component.
#[error("entity has no component '{0}'")]
Missing(String),
/// The RON text could not be parsed into the named type.
#[error("failed to parse '{type_name}': {message}")]
Parse {
/// The registered name being parsed.
type_name: String,
/// The underlying parser message.
message: String,
},
/// A per-field operation named a field this type does not reflect.
#[error("no reflected field named '{0}'")]
UnknownField(String),
/// A per-field operation targeted a type registered for whole-value access
/// only (registered with `register`, not `register_reflected`).
#[error("type '{0}' is not field-reflected")]
NotReflected(String),
/// The RON text could not be parsed into a single field's type.
#[error("failed to parse field '{field}': {message}")]
FieldParse {
/// The field being parsed.
field: String,
/// The underlying parser message.
message: String,
},
}
/// Per-field reflection generated by `#[derive(Reflect)]`.
///
/// Whole-value reflection ([`TypeRegistry::get_ron`] / [`set_ron`]) is enough
/// for serialization and scripts, but a Unity/Godot-style inspector needs to
/// see *named fields* so it can render one widget per field. `Reflect`
/// provides exactly that, without exposing the concrete field types to the
/// caller: each field is addressed by name and round-trips as RON (the same
/// representation the whole-value path uses).
///
/// Implement it with the derive — see
/// [`oxide_engine_derive::Reflect`](Reflect) (re-exported here as the
/// derive macro of the same name). Only **public** fields are reflected;
/// annotate a public field with `#[reflect(skip)]` to exclude it.
///
/// [`set_ron`]: TypeRegistry::set_ron
pub trait Reflect {
/// Static descriptors for every reflected field, in declaration order.
fn fields(&self) -> &'static [FieldInfo];
/// Serialize one field's current value to RON, or `None` if no field of
/// that name is reflected.
fn get_field(&self, name: &str) -> Option<String>;
/// Parse `value` (RON) into the named field, replacing it.
///
/// # Errors
/// [`UnknownField`](ReflectError::UnknownField) if the name isn't a
/// reflected field, or [`FieldParse`](ReflectError::FieldParse) if the
/// text isn't valid for the field's type.
fn set_field(&mut self, name: &str, value: &str) -> Result<(), ReflectError>;
}
/// A fieldless enum whose variants can be listed by name.
///
/// Per-field reflection tells the inspector a field's *type name* but not, for
/// an enum-typed field, the set of values it may take. `ReflectEnum` supplies
/// that list so the inspector can render a dropdown instead of a free-text RON
/// box. Register the enum with
/// [`register_enum`](TypeRegistry::register_enum) and the inspector looks its
/// variants up by type name. Derive it with `#[derive(ReflectEnum)]` (unit
/// variants only).
pub trait ReflectEnum {
/// The enum's variant names, in declaration order. Each is valid RON for
/// the corresponding unit variant, so it round-trips through
/// [`get_field`](TypeRegistry::get_field) / [`set_field`](TypeRegistry::set_field).
fn variants() -> &'static [&'static str];
}
/// A static description of one reflected field.
///
/// `type_name` is the field type's *syntactic* spelling (e.g. `"f32"`,
/// `"bool"`, `"Vec3"`, `"Handle < Font >"`) as written in the source. A
/// generic inspector dispatches a widget on it and falls back to a raw RON
/// editor for types it doesn't recognize.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FieldInfo {
/// The field's identifier.
pub name: &'static str,
/// The field type's syntactic name.
pub type_name: &'static str,
/// `(min, max)` bounds set with `#[reflect(min = X, max = Y)]`. The
/// inspector uses this to render a `Slider` for a `f32` field whose value
/// is normalized (e.g. metallic / roughness in `0..=1`); for plain numeric
/// fields it stays `None` and a `DragValue` is used instead.
pub range: Option<(f32, f32)>,
}
/// Implementation detail of `#[derive(Reflect)]` — serialize a field to RON.
///
/// Generated code calls this so it never needs `ron` in scope itself.
#[doc(hidden)]
pub fn __reflect_to_ron<T: Serialize>(value: &T) -> Option<String> {
ron::to_string(value).ok()
}
/// Implementation detail of `#[derive(Reflect)]` — parse a field from RON.
#[doc(hidden)]
pub fn __reflect_from_ron<T: DeserializeOwned>(
field: &str,
value: &str,
) -> Result<T, ReflectError> {
ron::from_str(value).map_err(|err| ReflectError::FieldParse {
field: field.to_string(),
message: err.to_string(),
})
}
/// The monomorphized operations for one registered type, stored as plain
/// function pointers (the closures capture nothing, so they coerce to `fn`).
struct ReflectedType {
get_ron: fn(&World, Entity) -> Option<String>,
set_ron: fn(&mut World, Entity, &str) -> Result<(), String>,
has: fn(&World, Entity) -> bool,
remove: fn(&mut World, Entity) -> bool,
/// Per-field operations, present only for types registered with
/// [`register_reflected`](TypeRegistry::register_reflected) (i.e. `T:
/// Reflect`). `None` for whole-value-only types. The registry callers
/// guarantee the component is present before invoking `get`/`set`.
fields: Option<FieldOps>,
/// Inserts a `T::default()` on an entity, present only for types registered
/// with [`register_addable`](TypeRegistry::register_addable) (i.e. `T:
/// Default`). `None` means the type can't be added from a generic "Add
/// Component" menu (no zero-arg construction).
add_default: Option<fn(&mut World, Entity)>,
}
/// The `T: Reflect` field operations, type-erased to function pointers.
struct FieldOps {
infos: fn(&World, Entity) -> Option<&'static [FieldInfo]>,
get: fn(&World, Entity, &str) -> Option<String>,
set: fn(&mut World, Entity, &str, &str) -> Result<(), ReflectError>,
}
/// A registry mapping stable type names to type-erased component operations.
///
/// Owned by the app/module system (Stage 5): each module registers the component
/// types it introduces, so the editor and scripts can address any of them by
/// name. Names are the identity used in serialized data and UI, so they should
/// be stable across versions.
#[derive(Default)]
pub struct TypeRegistry {
types: BTreeMap<&'static str, ReflectedType>,
/// Variant lists for registered enum types, keyed by the same syntactic
/// type name a [`FieldInfo::type_name`] carries, so the inspector can turn
/// an enum-typed field into a dropdown.
enums: BTreeMap<&'static str, &'static [&'static str]>,
}
impl TypeRegistry {
/// An empty registry.
pub fn new() -> Self {
Self::default()
}
/// Registers component type `T` under `name`.
///
/// `T` must be an ECS component (`Send + Sync + 'static`) and round-trip
/// through `serde`. Re-registering the same name replaces the entry.
pub fn register<T>(&mut self, name: &'static str)
where
T: Component + Serialize + DeserializeOwned,
{
self.types.insert(
name,
ReflectedType {
get_ron: |world, e| {
world
.get::<&T>(e)
.ok()
.and_then(|c| ron::to_string(&*c).ok())
},
set_ron: |world, e, text| {
let value: T = ron::from_str(text).map_err(|err| err.to_string())?;
// `contains` is checked by the caller, so insert cannot fail
// for a missing entity; map defensively all the same.
world
.insert_one(e, value)
.map_err(|_| "entity is not live".to_string())
},
has: |world, e| world.get::<&T>(e).is_ok(),
remove: |world, e| world.remove_one::<T>(e).is_ok(),
fields: None,
add_default: None,
},
);
}
/// Registers component type `T` with **per-field** reflection in addition
/// to whole-value access.
///
/// Identical to [`register`](Self::register) but also wires the
/// [`Reflect`] field operations, so [`field_infos`](Self::field_infos) /
/// [`get_field`](Self::get_field) / [`set_field`](Self::set_field) work for
/// this type. This is what lets the editor render a widget per field. Use
/// it for any type whose fields should be individually editable; use
/// `register` for opaque types edited only as a whole.
pub fn register_reflected<T>(&mut self, name: &'static str)
where
T: Component + Serialize + DeserializeOwned + Reflect,
{
self.types.insert(
name,
ReflectedType {
get_ron: |world, e| {
world
.get::<&T>(e)
.ok()
.and_then(|c| ron::to_string(&*c).ok())
},
set_ron: |world, e, text| {
let value: T = ron::from_str(text).map_err(|err| err.to_string())?;
world
.insert_one(e, value)
.map_err(|_| "entity is not live".to_string())
},
has: |world, e| world.get::<&T>(e).is_ok(),
remove: |world, e| world.remove_one::<T>(e).is_ok(),
fields: Some(FieldOps {
infos: |world, e| world.get::<&T>(e).ok().map(|c| c.fields()),
get: |world, e, field| world.get::<&T>(e).ok().and_then(|c| c.get_field(field)),
set: |world, e, field, ron| {
// The registry verifies the component is present before
// calling, so this access cannot fail.
let mut c = world
.get::<&mut T>(e)
.expect("component present (checked by caller)");
c.set_field(field, ron)
},
}),
add_default: None,
},
);
}
/// Registers a reflected component type that can also be **added from a
/// generic "Add Component" menu** — `T` must be `Default`, which supplies
/// the value inserted on the entity.
///
/// Equivalent to [`register_reflected`](Self::register_reflected) plus a
/// zero-arg constructor. Use it for components a user can attach in the
/// editor; use `register_reflected` for components that only exist
/// implicitly (every entity already has them) or that have no sensible
/// default.
pub fn register_addable<T>(&mut self, name: &'static str)
where
T: Component + Serialize + DeserializeOwned + Reflect + Default,
{
self.register_reflected::<T>(name);
if let Some(reflected) = self.types.get_mut(name) {
reflected.add_default = Some(|world, e| {
let _ = world.insert_one(e, T::default());
});
}
}
/// Removes the type registered under `name`. Returns whether it existed.
pub fn unregister(&mut self, name: &str) -> bool {
self.types.remove(name).is_some()
}
/// Whether a type is registered under `name`.
pub fn is_registered(&self, name: &str) -> bool {
self.types.contains_key(name)
}
/// The number of registered types.
pub fn len(&self) -> usize {
self.types.len()
}
/// Whether no types are registered.
pub fn is_empty(&self) -> bool {
self.types.is_empty()
}
/// The names of every registered type, sorted.
pub fn names(&self) -> impl Iterator<Item = &'static str> + '_ {
self.types.keys().copied()
}
/// Serializes the named component on `entity` to RON.
///
/// # Errors
/// [`UnknownType`](ReflectError::UnknownType) if the name is not registered,
/// [`NoSuchEntity`](ReflectError::NoSuchEntity) if the entity is dead, or
/// [`Missing`](ReflectError::Missing) if the entity lacks the component.
pub fn get_ron(
&self,
world: &World,
entity: Entity,
type_name: &str,
) -> Result<String, ReflectError> {
let reflected = self.lookup(type_name)?;
if !world.contains(entity) {
return Err(ReflectError::NoSuchEntity);
}
(reflected.get_ron)(world, entity)
.ok_or_else(|| ReflectError::Missing(type_name.to_string()))
}
/// Parses `ron` into the named type and writes it onto `entity`, inserting
/// the component if absent or replacing it if present.
///
/// # Errors
/// [`UnknownType`](ReflectError::UnknownType), [`NoSuchEntity`](ReflectError::NoSuchEntity),
/// or [`Parse`](ReflectError::Parse) if the text is not valid for the type.
pub fn set_ron(
&self,
world: &mut World,
entity: Entity,
type_name: &str,
ron: &str,
) -> Result<(), ReflectError> {
let reflected = self.lookup(type_name)?;
if !world.contains(entity) {
return Err(ReflectError::NoSuchEntity);
}
(reflected.set_ron)(world, entity, ron).map_err(|message| ReflectError::Parse {
type_name: type_name.to_string(),
message,
})
}
/// Whether `entity` carries the named component.
///
/// # Errors
/// [`UnknownType`](ReflectError::UnknownType) if the name is not registered.
pub fn has(
&self,
world: &World,
entity: Entity,
type_name: &str,
) -> Result<bool, ReflectError> {
let reflected = self.lookup(type_name)?;
Ok(world.contains(entity) && (reflected.has)(world, entity))
}
/// Removes the named component from `entity`. Returns whether it was present.
///
/// # Errors
/// [`UnknownType`](ReflectError::UnknownType) if the name is not registered.
pub fn remove(
&self,
world: &mut World,
entity: Entity,
type_name: &str,
) -> Result<bool, ReflectError> {
let reflected = self.lookup(type_name)?;
if !world.contains(entity) {
return Ok(false);
}
Ok((reflected.remove)(world, entity))
}
/// The names of all *registered* component types currently on `entity`,
/// sorted. This is what a generic inspector iterates to show every editable
/// component without knowing any concrete types.
pub fn components_on(&self, world: &World, entity: Entity) -> Vec<&'static str> {
if !world.contains(entity) {
return Vec::new();
}
self.types
.iter()
.filter(|(_, r)| (r.has)(world, entity))
.map(|(name, _)| *name)
.collect()
}
/// Whether the named type was registered with per-field reflection
/// ([`register_reflected`](Self::register_reflected)).
pub fn is_reflected(&self, type_name: &str) -> bool {
matches!(self.types.get(type_name), Some(r) if r.fields.is_some())
}
/// Registers a fieldless enum `E` under `name` (the syntactic type name its
/// fields carry), so [`enum_variants`](Self::enum_variants) can list its
/// values for an inspector dropdown. Independent of component registration —
/// an enum is a field *type*, not a component.
pub fn register_enum<E>(&mut self, name: &'static str)
where
E: ReflectEnum,
{
self.enums.insert(name, E::variants());
}
/// The variant names of an enum type registered with
/// [`register_enum`](Self::register_enum), or `None` if the type name isn't
/// a registered enum. The inspector renders a dropdown when this is `Some`.
pub fn enum_variants(&self, type_name: &str) -> Option<&'static [&'static str]> {
self.enums.get(type_name).copied()
}
/// Whether the named type can be added from a generic "Add Component" menu
/// ([`register_addable`](Self::register_addable)).
pub fn is_addable(&self, type_name: &str) -> bool {
matches!(self.types.get(type_name), Some(r) if r.add_default.is_some())
}
/// The names of every addable component type, sorted — what an "Add
/// Component" menu lists.
pub fn addable_names(&self) -> impl Iterator<Item = &'static str> + '_ {
self.types
.iter()
.filter(|(_, r)| r.add_default.is_some())
.map(|(name, _)| *name)
}
/// Adds a default-constructed instance of the named component to `entity`,
/// if the type is addable and the entity doesn't already carry it. Returns
/// whether a component was inserted.
///
/// # Errors
/// [`UnknownType`](ReflectError::UnknownType) if the name isn't registered,
/// or [`NoSuchEntity`](ReflectError::NoSuchEntity) if the entity is dead.
pub fn add_default(
&self,
world: &mut World,
entity: Entity,
type_name: &str,
) -> Result<bool, ReflectError> {
let reflected = self.lookup(type_name)?;
if !world.contains(entity) {
return Err(ReflectError::NoSuchEntity);
}
let Some(make) = reflected.add_default else {
return Ok(false);
};
// Don't clobber an existing component — "add" is a no-op if present.
if (reflected.has)(world, entity) {
return Ok(false);
}
make(world, entity);
Ok(true)
}
/// The field descriptors of the named component on `entity`.
///
/// This is what a generic inspector iterates to render one widget per
/// field. Returns [`NotReflected`](ReflectError::NotReflected) for types
/// registered for whole-value access only.
///
/// # Errors
/// [`UnknownType`](ReflectError::UnknownType),
/// [`NoSuchEntity`](ReflectError::NoSuchEntity),
/// [`NotReflected`](ReflectError::NotReflected), or
/// [`Missing`](ReflectError::Missing) if the entity lacks the component.
pub fn field_infos(
&self,
world: &World,
entity: Entity,
type_name: &str,
) -> Result<&'static [FieldInfo], ReflectError> {
let reflected = self.lookup(type_name)?;
if !world.contains(entity) {
return Err(ReflectError::NoSuchEntity);
}
let ops = reflected
.fields
.as_ref()
.ok_or_else(|| ReflectError::NotReflected(type_name.to_string()))?;
(ops.infos)(world, entity).ok_or_else(|| ReflectError::Missing(type_name.to_string()))
}
/// Serializes one field of the named component on `entity` to RON.
///
/// # Errors
/// As [`field_infos`](Self::field_infos), plus
/// [`UnknownField`](ReflectError::UnknownField) if the type has no such
/// field.
pub fn get_field(
&self,
world: &World,
entity: Entity,
type_name: &str,
field: &str,
) -> Result<String, ReflectError> {
let reflected = self.lookup(type_name)?;
if !world.contains(entity) {
return Err(ReflectError::NoSuchEntity);
}
let ops = reflected
.fields
.as_ref()
.ok_or_else(|| ReflectError::NotReflected(type_name.to_string()))?;
if !(reflected.has)(world, entity) {
return Err(ReflectError::Missing(type_name.to_string()));
}
(ops.get)(world, entity, field).ok_or_else(|| ReflectError::UnknownField(field.to_string()))
}
/// Parses `ron` into one field of the named component on `entity`.
///
/// Only the named field changes; the rest of the component is untouched —
/// this is the granularity an inspector edit needs.
///
/// # Errors
/// As [`field_infos`](Self::field_infos), plus
/// [`UnknownField`](ReflectError::UnknownField) or
/// [`FieldParse`](ReflectError::FieldParse).
pub fn set_field(
&self,
world: &mut World,
entity: Entity,
type_name: &str,
field: &str,
ron: &str,
) -> Result<(), ReflectError> {
let reflected = self.lookup(type_name)?;
if !world.contains(entity) {
return Err(ReflectError::NoSuchEntity);
}
let ops = reflected
.fields
.as_ref()
.ok_or_else(|| ReflectError::NotReflected(type_name.to_string()))?;
if !(reflected.has)(world, entity) {
return Err(ReflectError::Missing(type_name.to_string()));
}
(ops.set)(world, entity, field, ron)
}
fn lookup(&self, type_name: &str) -> Result<&ReflectedType, ReflectError> {
self.types
.get(type_name)
.ok_or_else(|| ReflectError::UnknownType(type_name.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::{Transform, Vec3};
use crate::scene::{Node, Scene};
use serde::Deserialize;
fn registry() -> TypeRegistry {
let mut r = TypeRegistry::new();
r.register_reflected::<Transform>("Transform");
r.register_reflected::<Node>("Node");
r
}
#[test]
fn registration_is_listed_and_sorted() {
let r = registry();
assert!(r.is_registered("Transform"));
assert!(!r.is_registered("Nope"));
assert_eq!(r.len(), 2);
assert_eq!(r.names().collect::<Vec<_>>(), vec!["Node", "Transform"]);
}
#[test]
fn get_then_set_round_trips_generically() {
let r = registry();
let mut scene = Scene::new();
let e = scene.spawn(
"thing",
Transform::from_translation(Vec3::new(1.0, 2.0, 3.0)),
);
// Read generically (no Transform type named at this call site beyond the
// string), then write it straight back.
let ron = r.get_ron(scene.world(), e, "Transform").unwrap();
r.set_ron(scene.world_mut(), e, "Transform", &ron).unwrap();
// The value survived the round trip.
let after = scene.local_transform(e).unwrap();
assert!((after.translation - Vec3::new(1.0, 2.0, 3.0)).length() < 1e-6);
}
#[test]
fn set_can_mutate_through_the_text() {
let r = registry();
let mut scene = Scene::new();
let e = scene.spawn("thing", Transform::IDENTITY);
// Hand-edit the serialized form (as the inspector / a script would) and
// apply it.
let edited =
ron::to_string(&Transform::from_translation(Vec3::new(5.0, 0.0, 0.0))).unwrap();
r.set_ron(scene.world_mut(), e, "Transform", &edited)
.unwrap();
assert!((scene.local_transform(e).unwrap().translation.x - 5.0).abs() < 1e-6);
}
#[test]
fn components_on_lists_present_registered_types() {
let r = registry();
let mut scene = Scene::new();
let e = scene.spawn("thing", Transform::IDENTITY); // has Node + Transform
assert_eq!(r.components_on(scene.world(), e), vec!["Node", "Transform"]);
// Removing one drops it from the listing.
assert!(r.remove(scene.world_mut(), e, "Transform").unwrap());
assert_eq!(r.components_on(scene.world(), e), vec!["Node"]);
assert!(!r.has(scene.world(), e, "Transform").unwrap());
}
#[test]
fn errors_are_specific() {
let r = registry();
let mut scene = Scene::new();
let e = scene.spawn("thing", Transform::IDENTITY);
// Unknown type name.
assert!(matches!(
r.get_ron(scene.world(), e, "Ghost"),
Err(ReflectError::UnknownType(_))
));
// Live entity missing the component.
scene.world_mut().remove_one::<Node>(e).unwrap();
assert!(matches!(
r.get_ron(scene.world(), e, "Node"),
Err(ReflectError::Missing(_))
));
// Dead entity.
let dead = scene.spawn("dead", Transform::IDENTITY);
scene.despawn(dead, crate::scene::DespawnPolicy::Recursive);
assert!(matches!(
r.get_ron(scene.world(), dead, "Transform"),
Err(ReflectError::NoSuchEntity)
));
// Malformed RON.
assert!(matches!(
r.set_ron(scene.world_mut(), e, "Transform", "not valid ron"),
Err(ReflectError::Parse { .. })
));
}
// --- Per-field reflection (`#[derive(Reflect)]`) ---
/// A representative component: a mix of field types, a skipped public
/// field, and a private field — exercises the derive's selection rules.
#[derive(Reflect, Serialize, Deserialize, PartialEq, Debug)]
struct Timer {
pub repeating: bool,
pub duration: f32,
pub label: String,
#[reflect(skip)]
pub elapsed: f32,
// Private: never reflected regardless of `skip`.
_internal: u32,
}
impl Timer {
fn sample() -> Self {
Self {
repeating: true,
duration: 2.5,
label: "tick".to_string(),
elapsed: 1.0,
_internal: 7,
}
}
}
/// A struct whose normalized fields carry slider ranges via the new
/// `#[reflect(min, max)]` attribute. The inspector dispatches a `Slider`
/// instead of a `DragValue` when both bounds are present.
#[derive(Reflect, Serialize, Deserialize)]
struct Knobs {
#[reflect(min = 0.0, max = 1.0)]
pub gain: f32,
pub bias: f32,
}
#[derive(Reflect, Serialize, Deserialize)]
struct Wrap(pub i32, pub bool);
#[test]
fn derive_supports_tuple_structs_with_positional_field_names() {
// Tuple-struct field names round-trip as "0", "1", ... — matching
// Rust's own positional accessors. Lets one-field newtype components
// like `Layer(pub LayerMask)` reflect without a wrapper.
let mut w = Wrap(42, false);
let names: Vec<_> = w.fields().iter().map(|f| f.name).collect();
assert_eq!(names, ["0", "1"]);
assert_eq!(w.get_field("0").as_deref(), Some("42"));
w.set_field("1", "true").unwrap();
assert!(w.1);
}
#[test]
fn derive_captures_min_max_attributes_as_field_range() {
let k = Knobs {
gain: 0.5,
bias: 0.0,
};
let fields = k.fields();
let gain = fields.iter().find(|f| f.name == "gain").unwrap();
let bias = fields.iter().find(|f| f.name == "bias").unwrap();
assert_eq!(gain.range, Some((0.0_f32, 1.0_f32)));
assert_eq!(bias.range, None);
}
#[test]
fn derive_lists_only_public_non_skipped_fields_in_order() {
let t = Timer::sample();
let names: Vec<_> = t.fields().iter().map(|f| f.name).collect();
assert_eq!(names, ["repeating", "duration", "label"]);
// Syntactic type names are preserved for inspector widget dispatch.
let types: Vec<_> = t.fields().iter().map(|f| f.type_name).collect();
assert_eq!(types, ["bool", "f32", "String"]);
}
#[test]
fn derive_gets_each_field_as_ron() {
let t = Timer::sample();
assert_eq!(t.get_field("repeating").as_deref(), Some("true"));
assert_eq!(t.get_field("duration").as_deref(), Some("2.5"));
assert_eq!(t.get_field("label").as_deref(), Some("\"tick\""));
// Skipped + private + unknown all read as None.
assert_eq!(t.get_field("elapsed"), None);
assert_eq!(t.get_field("_internal"), None);
assert_eq!(t.get_field("nope"), None);
}
#[test]
fn derive_sets_a_single_field_without_touching_others() {
let mut t = Timer::sample();
t.set_field("duration", "9.0").unwrap();
t.set_field("repeating", "false").unwrap();
assert_eq!(t.duration, 9.0);
assert!(!t.repeating);
// Other fields are untouched.
assert_eq!(t.label, "tick");
assert_eq!(t.elapsed, 1.0);
}
#[test]
fn derive_set_reports_unknown_field_and_parse_errors() {
let mut t = Timer::sample();
assert!(matches!(
t.set_field("elapsed", "0.0"), // public but skipped → not reflected
Err(ReflectError::UnknownField(f)) if f == "elapsed"
));
assert!(matches!(
t.set_field("missing", "0.0"),
Err(ReflectError::UnknownField(_))
));
assert!(matches!(
t.set_field("duration", "not a float"),
Err(ReflectError::FieldParse { field, .. }) if field == "duration"
));
}
#[test]
fn registry_lists_fields_of_a_reflected_component() {
let r = registry();
let mut scene = Scene::new();
let e = scene.spawn("thing", Transform::IDENTITY);
assert!(r.is_reflected("Transform"));
let names: Vec<_> = r
.field_infos(scene.world(), e, "Transform")
.unwrap()
.iter()
.map(|f| f.name)
.collect();
assert_eq!(names, ["translation", "rotation", "scale"]);
}
#[test]
fn registry_gets_and_sets_one_field_through_the_world() {
let r = registry();
let mut scene = Scene::new();
let e = scene.spawn("thing", Transform::IDENTITY);
// Set just the translation; rotation/scale stay identity. glam's Vec3
// serializes as a tuple, so RON is `(1.0,2.0,3.0)`.
r.set_field(
scene.world_mut(),
e,
"Transform",
"translation",
"(1.0, 2.0, 3.0)",
)
.unwrap();
let t = scene.world().get::<&Transform>(e).unwrap();
assert_eq!(t.translation, Vec3::new(1.0, 2.0, 3.0));
assert_eq!(t.scale, Vec3::ONE);
drop(t);
let got = r
.get_field(scene.world(), e, "Transform", "translation")
.unwrap();
assert_eq!(got, "(1.0,2.0,3.0)");
}
#[test]
fn registry_field_access_errors_are_specific() {
let mut r = registry();
// A whole-value-only type → NotReflected on field access.
r.register::<TimerWhole>("TimerWhole");
let mut scene = Scene::new();
let e = scene.spawn("thing", Transform::IDENTITY);
scene.world_mut().insert_one(e, TimerWhole(1)).unwrap();
assert!(!r.is_reflected("TimerWhole"));
assert!(matches!(
r.field_infos(scene.world(), e, "TimerWhole"),
Err(ReflectError::NotReflected(_))
));
// Unknown field on a reflected type.
assert!(matches!(
r.get_field(scene.world(), e, "Transform", "nope"),
Err(ReflectError::UnknownField(_))
));
// Reflected type, but the entity lacks the component.
let bare = scene.spawn("bare", Transform::IDENTITY);
scene.world_mut().remove_one::<Node>(bare).unwrap();
assert!(matches!(
r.get_field(scene.world(), bare, "Node", "name"),
Err(ReflectError::Missing(_))
));
}
#[derive(Serialize, Deserialize)]
struct TimerWhole(u32);
// --- Enum reflection (`#[derive(ReflectEnum)]`) ---
#[derive(ReflectEnum, Serialize, Deserialize, PartialEq, Debug)]
enum Facing {
North,
East,
South,
West,
}
#[test]
fn derive_enum_lists_variants_in_order() {
assert_eq!(Facing::variants(), &["North", "East", "South", "West"]);
}
#[test]
fn variant_names_round_trip_as_ron() {
// The names ReflectEnum returns must be valid RON for the variant, so
// the inspector can write a chosen name straight back through set_field.
for name in Facing::variants() {
let value: Facing = ron::from_str(name).unwrap();
assert_eq!(&ron::to_string(&value).unwrap(), name);
}
}
#[test]
fn addable_component_can_be_added_by_name_and_listed() {
use crate::render::MeshRenderer;
let mut r = registry();
r.register_addable::<MeshRenderer>("MeshRenderer");
// Listed as addable; Transform (register_reflected) is not.
assert!(r.is_addable("MeshRenderer"));
assert!(!r.is_addable("Transform"));
let addable: Vec<_> = r.addable_names().collect();
assert_eq!(addable, ["MeshRenderer"]);
let mut scene = Scene::new();
let e = scene.spawn("thing", Transform::IDENTITY);
assert!(!r.has(scene.world(), e, "MeshRenderer").unwrap());
// First add inserts the default; a second add is a no-op (already there).
assert!(r.add_default(scene.world_mut(), e, "MeshRenderer").unwrap());
assert!(r.has(scene.world(), e, "MeshRenderer").unwrap());
assert!(!r.add_default(scene.world_mut(), e, "MeshRenderer").unwrap());
// Its enum field is editable as a registered enum.
r.register_enum::<crate::render::PrimitiveShape>("PrimitiveShape");
let shape = r
.get_field(scene.world(), e, "MeshRenderer", "shape")
.unwrap();
assert_eq!(shape, "Cube"); // PrimitiveShape::default()
assert_eq!(
r.enum_variants("PrimitiveShape"),
Some(["Cube", "Sphere", "Plane"].as_slice())
);
}
#[test]
fn non_addable_type_add_default_is_a_noop() {
let r = registry(); // Transform/Node are register_reflected, not addable
let mut scene = Scene::new();
let e = scene.spawn("thing", Transform::IDENTITY);
// Transform isn't addable → Ok(false), nothing inserted.
assert!(!r.add_default(scene.world_mut(), e, "Transform").unwrap());
// Unknown type → error.
assert!(matches!(
r.add_default(scene.world_mut(), e, "Ghost"),
Err(ReflectError::UnknownType(_))
));
}
#[test]
fn registry_lists_enum_variants_by_type_name() {
let mut r = registry();
r.register_enum::<Facing>("Facing");
assert_eq!(
r.enum_variants("Facing"),
Some(["North", "East", "South", "West"].as_slice())
);
// Unregistered / non-enum type names return None.
assert_eq!(r.enum_variants("Transform"), None);
assert_eq!(r.enum_variants("Nope"), None);
}
#[test]
fn derive_field_values_round_trip_through_get_then_set() {
let original = Timer::sample();
let mut clone = Timer {
repeating: false,
duration: 0.0,
label: String::new(),
elapsed: 0.0,
_internal: 0,
};
for field in original.fields() {
let ron = original.get_field(field.name).unwrap();
clone.set_field(field.name, &ron).unwrap();
}
// Every reflected field now matches; non-reflected fields keep clone's.
assert_eq!(clone.repeating, original.repeating);
assert_eq!(clone.duration, original.duration);
assert_eq!(clone.label, original.label);
assert_eq!(clone.elapsed, 0.0);
}
}