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>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "oxide-engine-derive"
|
||||
description = "Derive macros for Oxide's reflection system (#[derive(Reflect)])"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn.workspace = true
|
||||
quote.workspace = true
|
||||
proc-macro2.workspace = true
|
||||
@@ -0,0 +1,257 @@
|
||||
//! Derive macros for Oxide's reflection system.
|
||||
//!
|
||||
//! This crate exists for exactly one job: `#[derive(Reflect)]`. It is the
|
||||
//! compile-time half of the engine's **dual-editable types** principle —
|
||||
//! every component's public fields should be editable from the editor
|
||||
//! inspector and from scripts through *one* representation, with no
|
||||
//! hand-written per-type code. The runtime half (the `Reflect` trait, the
|
||||
//! `FieldInfo` descriptor, and the `TypeRegistry`) lives in
|
||||
//! `oxide_engine::reflect`; this crate only generates the trait impl.
|
||||
//!
|
||||
//! ## What the derive generates
|
||||
//!
|
||||
//! For a struct with named fields, `#[derive(Reflect)]` emits an
|
||||
//! `oxide_engine::reflect::Reflect` impl that exposes each **public**,
|
||||
//! non-skipped field as:
|
||||
//!
|
||||
//! - a static [`FieldInfo`] entry (`name` + syntactic `type_name`), so a
|
||||
//! generic inspector can enumerate fields and pick a widget per type, and
|
||||
//! - per-field RON get/set, so a single field can be read or written without
|
||||
//! touching the rest of the component (the unit an inspector edits).
|
||||
//!
|
||||
//! Only `pub` fields are reflected — this matches the Unity/Godot convention
|
||||
//! that *public* fields are the editable surface. Use `#[reflect(skip)]` to
|
||||
//! exclude a public field.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use oxide_engine::reflect::Reflect;
|
||||
//!
|
||||
//! #[derive(Reflect, serde::Serialize, serde::Deserialize)]
|
||||
//! struct Timer {
|
||||
//! pub repeating: bool,
|
||||
//! pub duration: f32,
|
||||
//! #[reflect(skip)]
|
||||
//! pub elapsed: f32, // runtime state — not an authored field
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Every reflected field must itself be `serde`-serializable, since get/set
|
||||
//! round-trip through RON.
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, Data, DeriveInput, Fields, Visibility};
|
||||
|
||||
/// Derives `oxide_engine::reflect::Reflect` for a struct with named fields.
|
||||
///
|
||||
/// See the [crate-level docs](crate) for the field-selection rules
|
||||
/// (public-only, `#[reflect(skip)]`).
|
||||
#[proc_macro_derive(Reflect, attributes(reflect))]
|
||||
pub fn derive_reflect(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
let name = &input.ident;
|
||||
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
|
||||
|
||||
// Named-field structs and tuple structs are both supported. Tuple-struct
|
||||
// fields are addressed by their positional index ("0", "1", …), matching
|
||||
// Rust's own `self.0` / `self.1` syntax — this lets one-field newtype
|
||||
// components like `Layers(pub LayerMask)` reflect without a wrapper.
|
||||
let raw_fields = match &input.data {
|
||||
Data::Struct(data) => match &data.fields {
|
||||
Fields::Named(named) => named.named.iter().collect::<Vec<_>>(),
|
||||
Fields::Unnamed(unnamed) => unnamed.unnamed.iter().collect::<Vec<_>>(),
|
||||
Fields::Unit => {
|
||||
return compile_error(name, "Reflect cannot be derived for unit structs")
|
||||
}
|
||||
},
|
||||
_ => return compile_error(name, "Reflect can only be derived for structs"),
|
||||
};
|
||||
|
||||
let mut infos = Vec::new();
|
||||
let mut get_arms = Vec::new();
|
||||
let mut set_arms = Vec::new();
|
||||
|
||||
for (index, field) in raw_fields.iter().enumerate() {
|
||||
// Public-only: private fields are implementation detail, not the
|
||||
// authored/editable surface.
|
||||
if !matches!(field.vis, Visibility::Public(_)) {
|
||||
continue;
|
||||
}
|
||||
let attrs = parse_field_attrs(field);
|
||||
if attrs.skip {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For named structs the field name + accessor is the ident; for tuple
|
||||
// structs the name is the index as a string and the accessor is the
|
||||
// syn::Index token (which renders as `0`, `1`, ...).
|
||||
let (field_name, accessor) = match &field.ident {
|
||||
Some(ident) => (ident.to_string(), quote!(#ident)),
|
||||
None => {
|
||||
let idx = syn::Index::from(index);
|
||||
(index.to_string(), quote!(#idx))
|
||||
}
|
||||
};
|
||||
let ty = &field.ty;
|
||||
// Syntactic type text, e.g. "f32", "bool", "Vec3", "Handle < Font >".
|
||||
// The inspector dispatches a widget on this; unknown types fall back to
|
||||
// a raw RON editor.
|
||||
let type_name = quote!(#ty).to_string();
|
||||
|
||||
let range_tokens = match attrs.range {
|
||||
Some((min, max)) => quote! {
|
||||
::core::option::Option::Some((#min, #max))
|
||||
},
|
||||
None => quote! { ::core::option::Option::None },
|
||||
};
|
||||
|
||||
infos.push(quote! {
|
||||
::oxide_engine::reflect::FieldInfo {
|
||||
name: #field_name,
|
||||
type_name: #type_name,
|
||||
range: #range_tokens,
|
||||
}
|
||||
});
|
||||
get_arms.push(quote! {
|
||||
#field_name => ::oxide_engine::reflect::__reflect_to_ron(&self.#accessor),
|
||||
});
|
||||
set_arms.push(quote! {
|
||||
#field_name => {
|
||||
self.#accessor = ::oxide_engine::reflect::__reflect_from_ron(#field_name, value)?;
|
||||
::core::result::Result::Ok(())
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let field_count = infos.len();
|
||||
|
||||
quote! {
|
||||
impl #impl_generics ::oxide_engine::reflect::Reflect for #name #ty_generics #where_clause {
|
||||
fn fields(&self) -> &'static [::oxide_engine::reflect::FieldInfo] {
|
||||
static FIELDS: [::oxide_engine::reflect::FieldInfo; #field_count] = [
|
||||
#(#infos),*
|
||||
];
|
||||
&FIELDS
|
||||
}
|
||||
|
||||
fn get_field(&self, name: &str) -> ::core::option::Option<::std::string::String> {
|
||||
match name {
|
||||
#(#get_arms)*
|
||||
_ => ::core::option::Option::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_field(
|
||||
&mut self,
|
||||
name: &str,
|
||||
value: &str,
|
||||
) -> ::core::result::Result<(), ::oxide_engine::reflect::ReflectError> {
|
||||
match name {
|
||||
#(#set_arms)*
|
||||
_ => ::core::result::Result::Err(
|
||||
::oxide_engine::reflect::ReflectError::UnknownField(
|
||||
::std::string::ToString::to_string(name),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Parsed `#[reflect(...)]` attributes on a single field.
|
||||
#[derive(Default)]
|
||||
struct FieldAttrs {
|
||||
/// `#[reflect(skip)]` — exclude this public field from reflection.
|
||||
skip: bool,
|
||||
/// `#[reflect(min = X, max = Y)]` — numeric bounds passed to inspector
|
||||
/// widgets so a normalized `f32` field becomes a slider instead of a drag.
|
||||
/// Both must be present for a range to be recorded.
|
||||
range: Option<(f32, f32)>,
|
||||
}
|
||||
|
||||
fn parse_field_attrs(field: &syn::Field) -> FieldAttrs {
|
||||
let mut out = FieldAttrs::default();
|
||||
let mut min: Option<f32> = None;
|
||||
let mut max: Option<f32> = None;
|
||||
for attr in &field.attrs {
|
||||
if !attr.path().is_ident("reflect") {
|
||||
continue;
|
||||
}
|
||||
let _ = attr.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("skip") {
|
||||
out.skip = true;
|
||||
} else if meta.path.is_ident("min") {
|
||||
let lit: syn::LitFloat = meta.value()?.parse()?;
|
||||
min = Some(lit.base10_parse::<f32>()?);
|
||||
} else if meta.path.is_ident("max") {
|
||||
let lit: syn::LitFloat = meta.value()?.parse()?;
|
||||
max = Some(lit.base10_parse::<f32>()?);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
if let (Some(mn), Some(mx)) = (min, max) {
|
||||
out.range = Some((mn, mx));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Derives `oxide_engine::reflect::ReflectEnum` for a fieldless (C-like) enum,
|
||||
/// exposing its variant names so a generic inspector can render a dropdown for
|
||||
/// fields of that enum type.
|
||||
///
|
||||
/// Only **unit** variants are supported — a variant carrying data has no single
|
||||
/// "pick from a list" representation. Variant names round-trip as RON (a unit
|
||||
/// variant `Foo::Bar` serializes as `Bar`), which is exactly what `set_field`
|
||||
/// consumes.
|
||||
///
|
||||
/// ```ignore
|
||||
/// use oxide_engine::reflect::ReflectEnum;
|
||||
///
|
||||
/// #[derive(ReflectEnum, serde::Serialize, serde::Deserialize)]
|
||||
/// enum Facing { North, East, South, West }
|
||||
/// assert_eq!(Facing::variants(), &["North", "East", "South", "West"]);
|
||||
/// ```
|
||||
#[proc_macro_derive(ReflectEnum)]
|
||||
pub fn derive_reflect_enum(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
let name = &input.ident;
|
||||
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
|
||||
|
||||
let data = match &input.data {
|
||||
Data::Enum(data) => data,
|
||||
_ => return compile_error(name, "ReflectEnum can only be derived for enums"),
|
||||
};
|
||||
|
||||
let mut variant_names = Vec::new();
|
||||
for variant in &data.variants {
|
||||
if !matches!(variant.fields, Fields::Unit) {
|
||||
return compile_error(
|
||||
&variant.ident,
|
||||
"ReflectEnum requires unit (fieldless) variants",
|
||||
);
|
||||
}
|
||||
variant_names.push(variant.ident.to_string());
|
||||
}
|
||||
let count = variant_names.len();
|
||||
|
||||
quote! {
|
||||
impl #impl_generics ::oxide_engine::reflect::ReflectEnum for #name #ty_generics #where_clause {
|
||||
fn variants() -> &'static [&'static str] {
|
||||
static VARIANTS: [&str; #count] = [ #(#variant_names),* ];
|
||||
&VARIANTS
|
||||
}
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Emit a `compile_error!` at the derived type so the message is attributed
|
||||
/// to the user's struct, not somewhere inside the generated impl.
|
||||
fn compile_error(name: &syn::Ident, message: &str) -> TokenStream {
|
||||
syn::Error::new(name.span(), message)
|
||||
.to_compile_error()
|
||||
.into()
|
||||
}
|
||||
Reference in New Issue
Block a user