diff --git a/gdnative-core/src/export/property/hint.rs b/gdnative-core/src/export/property/hint.rs index 8dbd28365..bacf626ac 100644 --- a/gdnative-core/src/export/property/hint.rs +++ b/gdnative-core/src/export/property/hint.rs @@ -1,6 +1,6 @@ //! Strongly typed property hints. -use std::fmt::{self, Write}; +use std::fmt::{self, Display, Write}; use std::ops::RangeInclusive; use crate::core_types::GodotString; @@ -116,12 +116,22 @@ where /// ``` #[derive(Clone, Eq, PartialEq, Debug, Default)] pub struct EnumHint { - values: Vec, + values: Vec, } impl EnumHint { #[inline] pub fn new(values: Vec) -> Self { + let values = values.into_iter().map(EnumHintEntry::new).collect(); + EnumHint { values } + } + + #[inline] + pub fn with_values(values: Vec<(String, i64)>) -> Self { + let values = values + .into_iter() + .map(|(key, value)| EnumHintEntry::with_value(key, value)) + .collect(); EnumHint { values } } @@ -136,13 +146,46 @@ impl EnumHint { } for rest in iter { - write!(s, ",{rest}").unwrap(); + write!(s, ",").unwrap(); + write!(s, "{rest}").unwrap(); } s.into() } } +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct EnumHintEntry { + key: String, + value: Option, +} + +impl EnumHintEntry { + #[inline] + pub fn new(key: String) -> Self { + Self { key, value: None } + } + + #[inline] + pub fn with_value(key: String, value: i64) -> Self { + Self { + key, + value: Some(value), + } + } +} + +impl Display for EnumHintEntry { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.key)?; + if let Some(value) = self.value { + write!(f, ":{}", value)?; + } + Ok(()) + } +} + /// Possible hints for integers. #[derive(Clone, Debug)] #[non_exhaustive] @@ -469,3 +512,13 @@ impl ArrayHint { } } } + +godot_test!(test_enum_hint_without_mapping { + let hint = EnumHint::new(vec!["Foo".into(), "Bar".into()]); + assert_eq!(hint.to_godot_hint_string().to_string(), "Foo,Bar".to_string(),); +}); + +godot_test!(test_enum_hint_with_mapping { + let hint = EnumHint::with_values(vec![("Foo".into(), 42), ("Bar".into(), 67)]); + assert_eq!(hint.to_godot_hint_string().to_string(), "Foo:42,Bar:67".to_string(),); +}); diff --git a/gdnative-derive/src/export.rs b/gdnative-derive/src/export.rs new file mode 100644 index 000000000..11dae0bde --- /dev/null +++ b/gdnative-derive/src/export.rs @@ -0,0 +1,68 @@ +use crate::crate_gdnative_core; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use syn::spanned::Spanned; +use syn::{DeriveInput, Fields}; + +fn err_only_supports_fieldless_enums(span: Span) -> syn::Error { + syn::Error::new(span, "#[derive(Export)] only supports fieldless enums") +} + +pub(crate) fn derive_export(input: &DeriveInput) -> syn::Result { + let derived_enum = match &input.data { + syn::Data::Enum(data) => data, + syn::Data::Struct(data) => { + return Err(err_only_supports_fieldless_enums(data.struct_token.span())); + } + syn::Data::Union(data) => { + return Err(err_only_supports_fieldless_enums(data.union_token.span())); + } + }; + + let export_impl = impl_export(&input.ident, derived_enum)?; + Ok(export_impl) +} + +fn impl_export(enum_ty: &syn::Ident, data: &syn::DataEnum) -> syn::Result { + let err = data + .variants + .iter() + .filter(|variant| !matches!(variant.fields, Fields::Unit)) + .map(|variant| err_only_supports_fieldless_enums(variant.ident.span())) + .reduce(|mut acc, err| { + acc.combine(err); + acc + }); + if let Some(err) = err { + return Err(err); + } + + let mappings = data + .variants + .iter() + .map(|variant| { + let key = &variant.ident; + let val = quote! { #enum_ty::#key as i64 }; + quote! { (stringify!(#key).to_string(), #val) } + }) + .collect::>(); + let gdnative_core = crate_gdnative_core(); + + let impl_block = quote! { + const _: () = { + pub enum NoHint {} + + impl #gdnative_core::export::Export for #enum_ty { + type Hint = NoHint; + + #[inline] + fn export_info(_hint: Option) -> #gdnative_core::export::ExportInfo { + let mappings = vec![ #(#mappings),* ]; + let enum_hint = #gdnative_core::export::hint::EnumHint::with_values(mappings); + return #gdnative_core::export::hint::IntHint::::Enum(enum_hint).export_info(); + } + } + }; + }; + + Ok(impl_block) +} diff --git a/gdnative-derive/src/lib.rs b/gdnative-derive/src/lib.rs index a71625381..88f1f6b04 100644 --- a/gdnative-derive/src/lib.rs +++ b/gdnative-derive/src/lib.rs @@ -10,6 +10,7 @@ use proc_macro2::TokenStream as TokenStream2; use quote::ToTokens; use syn::{parse::Parser, AttributeArgs, DeriveInput, ItemFn, ItemImpl, ItemType}; +mod export; mod init; mod methods; mod native_script; @@ -663,6 +664,63 @@ pub fn godot_wrap_method(input: TokenStream) -> TokenStream { } } +/// Make a rust `enum` has drop-down list in Godot editor. +/// Note that the derived `enum` should also implements `Copy` trait. +/// +/// Take the following example, you will see a drop-down list for the `dir` +/// property, and `Up` and `Down` converts to `1` and `-1` in the GDScript +/// side. +/// +/// ``` +/// use gdnative::prelude::*; +/// +/// #[derive(Debug, PartialEq, Clone, Copy, Export, ToVariant, FromVariant)] +/// #[variant(enum = "repr")] +/// #[repr(i32)] +/// enum Dir { +/// Up = 1, +/// Down = -1, +/// } +/// +/// #[derive(NativeClass)] +/// #[no_constructor] +/// struct Move { +/// #[property] +/// pub dir: Dir, +/// } +/// ``` +/// +/// You can't derive `Export` on `enum` that has non-unit variant. +/// +/// ```compile_fail +/// use gdnative::prelude::*; +/// +/// #[derive(Debug, PartialEq, Clone, Copy, Export)] +/// enum Action { +/// Move((f32, f32, f32)), +/// Attack(u64), +/// } +/// ``` +/// +/// You can't derive `Export` on `struct` or `union`. +/// +/// ```compile_fail +/// use gdnative::prelude::*; +/// +/// #[derive(Export)] +/// struct Foo { +/// f1: i32 +/// } +/// ``` +#[proc_macro_derive(Export)] +pub fn derive_export(input: TokenStream) -> TokenStream { + let derive_input = syn::parse_macro_input!(input as syn::DeriveInput); + match export::derive_export(&derive_input) { + Ok(stream) => stream.into(), + Err(err) => err.to_compile_error().into(), + } +} + /// Returns a standard header for derived implementations. /// /// Adds the `automatically_derived` attribute and prevents common lints from triggering diff --git a/gdnative/tests/ui.rs b/gdnative/tests/ui.rs index 859d569c9..4a8684396 100644 --- a/gdnative/tests/ui.rs +++ b/gdnative/tests/ui.rs @@ -40,6 +40,12 @@ fn ui_tests() { t.compile_fail("tests/ui/from_variant_fail_07.rs"); t.compile_fail("tests/ui/from_variant_fail_08.rs"); t.compile_fail("tests/ui/from_variant_fail_09.rs"); + + // Export + t.pass("tests/ui/export_pass.rs"); + t.compile_fail("tests/ui/export_fail_01.rs"); + t.compile_fail("tests/ui/export_fail_02.rs"); + t.compile_fail("tests/ui/export_fail_03.rs"); } // FIXME(rust/issues/54725): Full path spans are only available on nightly as of now diff --git a/gdnative/tests/ui/export_fail_01.rs b/gdnative/tests/ui/export_fail_01.rs new file mode 100644 index 000000000..c69fed7d3 --- /dev/null +++ b/gdnative/tests/ui/export_fail_01.rs @@ -0,0 +1,9 @@ +use gdnative::prelude::*; + +#[derive(Export, ToVariant)] +pub enum Foo { + Bar(String), + Baz { a: i32, b: u32 }, +} + +fn main() {} diff --git a/gdnative/tests/ui/export_fail_01.stderr b/gdnative/tests/ui/export_fail_01.stderr new file mode 100644 index 000000000..79cc55485 --- /dev/null +++ b/gdnative/tests/ui/export_fail_01.stderr @@ -0,0 +1,11 @@ +error: #[derive(Export)] only supports fieldless enums + --> tests/ui/export_fail_01.rs:5:5 + | +5 | Bar(String), + | ^^^ + +error: #[derive(Export)] only supports fieldless enums + --> tests/ui/export_fail_01.rs:6:5 + | +6 | Baz { a: i32, b: u32 }, + | ^^^ diff --git a/gdnative/tests/ui/export_fail_02.rs b/gdnative/tests/ui/export_fail_02.rs new file mode 100644 index 000000000..7ce4c080e --- /dev/null +++ b/gdnative/tests/ui/export_fail_02.rs @@ -0,0 +1,8 @@ +use gdnative::prelude::*; + +#[derive(Export, ToVariant)] +pub struct Foo { + bar: i32, +} + +fn main() {} diff --git a/gdnative/tests/ui/export_fail_02.stderr b/gdnative/tests/ui/export_fail_02.stderr new file mode 100644 index 000000000..288715542 --- /dev/null +++ b/gdnative/tests/ui/export_fail_02.stderr @@ -0,0 +1,5 @@ +error: #[derive(Export)] only supports fieldless enums + --> tests/ui/export_fail_02.rs:4:5 + | +4 | pub struct Foo { + | ^^^^^^ diff --git a/gdnative/tests/ui/export_fail_03.rs b/gdnative/tests/ui/export_fail_03.rs new file mode 100644 index 000000000..d89f24118 --- /dev/null +++ b/gdnative/tests/ui/export_fail_03.rs @@ -0,0 +1,8 @@ +use gdnative::prelude::*; + +#[derive(Export, ToVariant)] +pub union Foo { + bar: i32, +} + +fn main() {} diff --git a/gdnative/tests/ui/export_fail_03.stderr b/gdnative/tests/ui/export_fail_03.stderr new file mode 100644 index 000000000..304eff3b3 --- /dev/null +++ b/gdnative/tests/ui/export_fail_03.stderr @@ -0,0 +1,11 @@ +error: #[derive(Export)] only supports fieldless enums + --> tests/ui/export_fail_03.rs:4:5 + | +4 | pub union Foo { + | ^^^^^ + +error: Variant conversion derive macro does not work on unions. + --> tests/ui/export_fail_03.rs:4:1 + | +4 | pub union Foo { + | ^^^ diff --git a/gdnative/tests/ui/export_pass.rs b/gdnative/tests/ui/export_pass.rs new file mode 100644 index 000000000..a5b12b5de --- /dev/null +++ b/gdnative/tests/ui/export_pass.rs @@ -0,0 +1,11 @@ +use gdnative::prelude::*; + +#[derive(Export, ToVariant, Clone, Copy)] +#[variant(enum = "repr")] +#[repr(i32)] +pub enum Foo { + Bar, + Baz, +} + +fn main() {} diff --git a/test/src/lib.rs b/test/src/lib.rs index 92a0fbd2a..8aa79bd38 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -28,6 +28,11 @@ pub extern "C" fn run_tests( status &= gdnative::core_types::test_core_types(); + status &= gdnative::export::hint::test_enum_hint_without_mapping(); + status &= gdnative::export::hint::test_enum_hint_with_mapping(); + + status &= test_underscore_method_binding(); + status &= test_rust_class_construction(); status &= test_from_instance_id(); status &= test_nil_object_return_value(); status &= test_rust_class_construction();