Skip to content

Commit c2f6666

Browse files
authored
Try #775:
2 parents 6dc9721 + 825ad68 commit c2f6666

File tree

13 files changed

+261
-3
lines changed

13 files changed

+261
-3
lines changed

gdnative-core/src/export/property/hint.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,22 @@ where
116116
/// ```
117117
#[derive(Clone, Eq, PartialEq, Debug, Default)]
118118
pub struct EnumHint {
119-
values: Vec<String>,
119+
values: Vec<(String, Option<i64>)>,
120120
}
121121

122122
impl EnumHint {
123123
#[inline]
124124
pub fn new(values: Vec<String>) -> Self {
125+
let values = values.into_iter().map(|v| (v, None)).collect();
126+
EnumHint { values }
127+
}
128+
129+
#[inline]
130+
pub fn with_numbers(values: Vec<(String, i64)>) -> Self {
131+
let values = values
132+
.into_iter()
133+
.map(|(key, val)| (key, Some(val)))
134+
.collect();
125135
EnumHint { values }
126136
}
127137

@@ -130,13 +140,22 @@ impl EnumHint {
130140
let mut s = String::new();
131141

132142
let mut iter = self.values.iter();
143+
let write_item = |s: &mut String, item: &(String, Option<i64>)| match item {
144+
(key, Some(val)) => {
145+
write!(s, "{key}:{val}")
146+
}
147+
(key, None) => {
148+
write!(s, "{key}")
149+
}
150+
};
133151

134152
if let Some(first) = iter.next() {
135-
write!(s, "{first}").unwrap();
153+
write_item(&mut s, first).unwrap();
136154
}
137155

138156
for rest in iter {
139-
write!(s, ",{rest}").unwrap();
157+
write!(s, ",").unwrap();
158+
write_item(&mut s, rest).unwrap();
140159
}
141160

142161
s.into()
@@ -469,3 +488,13 @@ impl ArrayHint {
469488
}
470489
}
471490
}
491+
492+
godot_test!(test_enum_hint_without_mapping {
493+
let hint = EnumHint::new(vec!["Foo".into(), "Bar".into()]);
494+
assert_eq!(hint.to_godot_hint_string().to_string(), "Foo,Bar".to_string(),);
495+
});
496+
497+
godot_test!(test_enum_hint_with_mapping {
498+
let hint = EnumHint::with_numbers(vec![("Foo".into(), 42), ("Bar".into(), 67)]);
499+
assert_eq!(hint.to_godot_hint_string().to_string(), "Foo:42,Bar:67".to_string(),);
500+
});

gdnative-derive/src/export.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use proc_macro2::{Span, TokenStream as TokenStream2};
2+
use syn::spanned::Spanned;
3+
use syn::{DeriveInput, Fields};
4+
5+
fn err_only_supports_fieldless_enums(span: Span) -> syn::Error {
6+
syn::Error::new(span, "#[derive(Export)] only supports fieldless enums")
7+
}
8+
9+
pub(crate) fn derive_export(input: &DeriveInput) -> syn::Result<TokenStream2> {
10+
let derived_enum = match &input.data {
11+
syn::Data::Enum(data) => data,
12+
syn::Data::Struct(data) => {
13+
return Err(err_only_supports_fieldless_enums(data.struct_token.span()));
14+
}
15+
syn::Data::Union(data) => {
16+
return Err(err_only_supports_fieldless_enums(data.union_token.span()));
17+
}
18+
};
19+
20+
let export_impl = impl_export(&input.ident, derived_enum)?;
21+
Ok(export_impl)
22+
}
23+
24+
fn impl_export(enum_ty: &syn::Ident, data: &syn::DataEnum) -> syn::Result<TokenStream2> {
25+
let err = data
26+
.variants
27+
.iter()
28+
.filter(|variant| !matches!(variant.fields, Fields::Unit))
29+
.map(|variant| err_only_supports_fieldless_enums(variant.ident.span()))
30+
.reduce(|mut acc, err| {
31+
acc.combine(err);
32+
acc
33+
});
34+
if let Some(err) = err {
35+
return Err(err);
36+
}
37+
38+
let mappings = data
39+
.variants
40+
.iter()
41+
.map(|variant| {
42+
let key = &variant.ident;
43+
let val = quote! { #enum_ty::#key as i64 };
44+
quote! { (stringify!(#key).to_string(), #val) }
45+
})
46+
.collect::<Vec<_>>();
47+
48+
let impl_block = quote! {
49+
impl ::gdnative::export::Export for #enum_ty {
50+
type Hint = ::gdnative::export::hint::IntHint<i64>;
51+
#[inline]
52+
fn export_info(hint: Option<Self::Hint>) -> ::gdnative::export::ExportInfo {
53+
if let Some(hint) = hint {
54+
return hint.export_info();
55+
} else {
56+
let mappings = vec![ #(#mappings),* ];
57+
let enum_hint = ::gdnative::export::hint::EnumHint::with_numbers(mappings);
58+
return ::gdnative::export::hint::IntHint::<i64>::Enum(enum_hint).export_info();
59+
}
60+
}
61+
}
62+
};
63+
64+
Ok(impl_block)
65+
}

gdnative-derive/src/lib.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use proc_macro2::TokenStream as TokenStream2;
1010
use quote::ToTokens;
1111
use syn::{parse::Parser, AttributeArgs, DeriveInput, ItemFn, ItemImpl, ItemType};
1212

13+
mod export;
1314
mod init;
1415
mod methods;
1516
mod native_script;
@@ -663,6 +664,63 @@ pub fn godot_wrap_method(input: TokenStream) -> TokenStream {
663664
}
664665
}
665666

667+
/// Make a rust `enum` has drop-down list in Godot editor.
668+
/// Note that the derived `enum` should also implements `Copy` trait.
669+
///
670+
/// Take the following example, you will see a drop-down list for the `dir`
671+
/// property, and `Up` and `Down` converts to `1` and `-1` in the GDScript
672+
/// side.
673+
///
674+
/// ```
675+
/// use gdnative::prelude::*;
676+
///
677+
/// #[derive(Debug, PartialEq, Clone, Copy, Export, ToVariant, FromVariant)]
678+
/// #[variant(enum = "repr")]
679+
/// #[repr(i32)]
680+
/// enum Dir {
681+
/// Up = 1,
682+
/// Down = -1,
683+
/// }
684+
///
685+
/// #[derive(NativeClass)]
686+
/// #[no_constructor]
687+
/// struct Move {
688+
/// #[property]
689+
/// pub dir: Dir,
690+
/// }
691+
/// ```
692+
///
693+
/// You can't derive `Export` on `enum` that has non-unit variant.
694+
///
695+
/// ```compile_fail
696+
/// use gdnative::prelude::*;
697+
///
698+
/// #[derive(Debug, PartialEq, Clone, Copy, Export)]
699+
/// enum Action {
700+
/// Move((f32, f32, f32)),
701+
/// Attack(u64),
702+
/// }
703+
/// ```
704+
///
705+
/// You can't derive `Export` on `struct` or `union`.
706+
///
707+
/// ```compile_fail
708+
/// use gdnative::prelude::*;
709+
///
710+
/// #[derive(Export)]
711+
/// struct Foo {
712+
/// f1: i32
713+
/// }
714+
/// ```
715+
#[proc_macro_derive(Export)]
716+
pub fn derive_export(input: TokenStream) -> TokenStream {
717+
let derive_input = syn::parse_macro_input!(input as syn::DeriveInput);
718+
match export::derive_export(&derive_input) {
719+
Ok(stream) => stream.into(),
720+
Err(err) => err.to_compile_error().into(),
721+
}
722+
}
723+
666724
/// Returns a standard header for derived implementations.
667725
///
668726
/// Adds the `automatically_derived` attribute and prevents common lints from triggering

gdnative/tests/ui.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ fn ui_tests() {
4040
t.compile_fail("tests/ui/from_variant_fail_07.rs");
4141
t.compile_fail("tests/ui/from_variant_fail_08.rs");
4242
t.compile_fail("tests/ui/from_variant_fail_09.rs");
43+
44+
// Export
45+
t.pass("tests/ui/export_pass.rs");
46+
t.compile_fail("tests/ui/export_fail_01.rs");
47+
t.compile_fail("tests/ui/export_fail_02.rs");
48+
t.compile_fail("tests/ui/export_fail_03.rs");
4349
}
4450

4551
// FIXME(rust/issues/54725): Full path spans are only available on nightly as of now

gdnative/tests/ui/export_fail_01.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use gdnative::prelude::*;
2+
3+
#[derive(Export, ToVariant)]
4+
pub enum Foo {
5+
Bar(String),
6+
Baz { a: i32, b: u32 },
7+
}
8+
9+
fn main() {}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
error: #[derive(Export)] only supports fieldless enums
2+
--> tests/ui/export_fail_01.rs:5:5
3+
|
4+
5 | Bar(String),
5+
| ^^^
6+
7+
error: #[derive(Export)] only supports fieldless enums
8+
--> tests/ui/export_fail_01.rs:6:5
9+
|
10+
6 | Baz { a: i32, b: u32 },
11+
| ^^^

gdnative/tests/ui/export_fail_02.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
use gdnative::prelude::*;
2+
3+
#[derive(Export, ToVariant)]
4+
pub struct Foo {
5+
bar: i32,
6+
}
7+
8+
fn main() {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
error: #[derive(Export)] only supports fieldless enums
2+
--> tests/ui/export_fail_02.rs:4:5
3+
|
4+
4 | pub struct Foo {
5+
| ^^^^^^

gdnative/tests/ui/export_fail_03.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
use gdnative::prelude::*;
2+
3+
#[derive(Export, ToVariant)]
4+
pub union Foo {
5+
bar: i32,
6+
}
7+
8+
fn main() {}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
error: #[derive(Export)] only supports fieldless enums
2+
--> tests/ui/export_fail_03.rs:4:5
3+
|
4+
4 | pub union Foo {
5+
| ^^^^^
6+
7+
error: Variant conversion derive macro does not work on unions.
8+
--> tests/ui/export_fail_03.rs:4:1
9+
|
10+
4 | pub union Foo {
11+
| ^^^

gdnative/tests/ui/export_pass.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use gdnative::prelude::*;
2+
3+
#[derive(Export, ToVariant, Clone, Copy)]
4+
#[variant(enum = "repr")]
5+
#[repr(i32)]
6+
pub enum Foo {
7+
Bar,
8+
Baz,
9+
}
10+
11+
fn main() {}

test/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod test_as_arg;
88
mod test_async;
99
mod test_constructor;
1010
mod test_derive;
11+
mod test_export_enum;
1112
mod test_free_ub;
1213
mod test_generic_class;
1314
mod test_indexed_props;
@@ -28,6 +29,11 @@ pub extern "C" fn run_tests(
2829

2930
status &= gdnative::core_types::test_core_types();
3031

32+
status &= gdnative::export::hint::test_enum_hint_without_mapping();
33+
status &= gdnative::export::hint::test_enum_hint_with_mapping();
34+
35+
status &= test_underscore_method_binding();
36+
status &= test_rust_class_construction();
3137
status &= test_from_instance_id();
3238
status &= test_nil_object_return_value();
3339
status &= test_rust_class_construction();
@@ -47,6 +53,7 @@ pub extern "C" fn run_tests(
4753
status &= test_vararray_return::run_tests();
4854
status &= test_variant_call_args::run_tests();
4955
status &= test_variant_ops::run_tests();
56+
status &= test_export_enum::run_tests();
5057

5158
Variant::new(status).leak()
5259
}

test/src/test_export_enum.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use gdnative::prelude::*;
2+
3+
#[derive(Debug, PartialEq, Clone, Copy, Export, ToVariant, FromVariant)]
4+
#[variant(enum = "repr")]
5+
#[repr(i32)]
6+
enum Dir {
7+
Up = 1,
8+
Down = -1,
9+
}
10+
11+
pub(crate) fn run_tests() -> bool {
12+
let mut ok = true;
13+
14+
ok &= test_from_variant();
15+
ok &= test_to_variant();
16+
17+
ok
18+
}
19+
20+
crate::godot_itest!(test_from_variant {
21+
assert_eq!(Dir::from_variant(&1_i32.to_variant()), Ok(Dir::Up));
22+
assert_eq!(Dir::from_variant(&(-1_i32).to_variant()), Ok(Dir::Down));
23+
// 42 isn't mapped to any variant of `Dir`
24+
assert!(Dir::from_variant(&42_i32.to_variant()).is_err());
25+
});
26+
27+
crate::godot_itest!(test_to_variant {
28+
assert_eq!(Dir::Up.to_variant(), 1_i32.to_variant());
29+
assert_eq!(Dir::Down.to_variant(), (-1_i32).to_variant());
30+
});

0 commit comments

Comments
 (0)