Skip to content

Commit 83eccfd

Browse files
authored
Try #775:
2 parents 6dc9721 + 348bac2 commit 83eccfd

24 files changed

+475
-5
lines changed

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

+56-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Strongly typed property hints.
22
3-
use std::fmt::{self, Write};
3+
use std::fmt::{self, Display, Write};
44
use std::ops::RangeInclusive;
55

66
use crate::core_types::GodotString;
@@ -116,20 +116,26 @@ where
116116
/// ```
117117
#[derive(Clone, Eq, PartialEq, Debug, Default)]
118118
pub struct EnumHint {
119-
values: Vec<String>,
119+
entries: Vec<EnumHintEntry>,
120120
}
121121

122122
impl EnumHint {
123123
#[inline]
124-
pub fn new(values: Vec<String>) -> Self {
125-
EnumHint { values }
124+
pub fn new(keys: Vec<String>) -> Self {
125+
let entries = keys.into_iter().map(EnumHintEntry::new).collect();
126+
EnumHint { entries }
127+
}
128+
129+
#[inline]
130+
pub fn with_entries(entries: Vec<EnumHintEntry>) -> Self {
131+
EnumHint { entries }
126132
}
127133

128134
/// Formats the hint as a Godot hint string.
129135
fn to_godot_hint_string(&self) -> GodotString {
130136
let mut s = String::new();
131137

132-
let mut iter = self.values.iter();
138+
let mut iter = self.entries.iter();
133139

134140
if let Some(first) = iter.next() {
135141
write!(s, "{first}").unwrap();
@@ -143,6 +149,38 @@ impl EnumHint {
143149
}
144150
}
145151

152+
#[derive(Clone, PartialEq, Eq, Debug)]
153+
pub struct EnumHintEntry {
154+
key: String,
155+
value: Option<i64>,
156+
}
157+
158+
impl EnumHintEntry {
159+
#[inline]
160+
pub fn new(key: String) -> Self {
161+
Self { key, value: None }
162+
}
163+
164+
#[inline]
165+
pub fn with_value(key: String, value: i64) -> Self {
166+
Self {
167+
key,
168+
value: Some(value),
169+
}
170+
}
171+
}
172+
173+
impl Display for EnumHintEntry {
174+
#[inline]
175+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176+
write!(f, "{}", self.key)?;
177+
if let Some(value) = self.value {
178+
write!(f, ":{}", value)?;
179+
}
180+
Ok(())
181+
}
182+
}
183+
146184
/// Possible hints for integers.
147185
#[derive(Clone, Debug)]
148186
#[non_exhaustive]
@@ -469,3 +507,16 @@ impl ArrayHint {
469507
}
470508
}
471509
}
510+
511+
godot_test!(test_enum_hint_without_mapping {
512+
let hint = EnumHint::new(vec!["Foo".into(), "Bar".into()]);
513+
assert_eq!(hint.to_godot_hint_string().to_string(), "Foo,Bar".to_string(),);
514+
});
515+
516+
godot_test!(test_enum_hint_with_mapping {
517+
let hint = EnumHint::with_entries(vec![
518+
EnumHintEntry::with_value("Foo".to_string(), 42),
519+
EnumHintEntry::with_value("Bar".to_string(), 67),
520+
]);
521+
assert_eq!(hint.to_godot_hint_string().to_string(), "Foo:42,Bar:67".to_string(),);
522+
});

gdnative-derive/src/export.rs

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
use crate::crate_gdnative_core;
2+
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
3+
use quote::ToTokens;
4+
use syn::spanned::Spanned;
5+
use syn::{DeriveInput, Fields, Meta};
6+
7+
#[derive(Copy, Clone, Debug)]
8+
enum Kind {
9+
Enum,
10+
}
11+
12+
#[derive(Debug)]
13+
struct DeriveData {
14+
kind: Kind,
15+
ident: Ident,
16+
data: syn::Data,
17+
}
18+
19+
fn parse_derive_input(input: DeriveInput) -> syn::Result<DeriveData> {
20+
let DeriveInput {
21+
ident, data, attrs, ..
22+
} = input.clone();
23+
24+
let (kind, errors) = attrs
25+
.iter()
26+
.filter(|attr| attr.path.is_ident("export"))
27+
.fold((None, vec![]), |(mut kind, mut errors), attr| {
28+
let list = match attr.parse_meta() {
29+
Ok(meta) => match meta {
30+
Meta::List(list) => list,
31+
Meta::Path(path) => {
32+
errors.push(syn::Error::new(
33+
path.span(),
34+
"missing macro arguments. expected #[export(...)]",
35+
));
36+
return (kind, errors);
37+
}
38+
Meta::NameValue(pair) => {
39+
errors.push(syn::Error::new(
40+
pair.span(),
41+
"missing macro arguments. expected #[export(...)]",
42+
));
43+
return (kind, errors);
44+
}
45+
},
46+
Err(e) => {
47+
errors.push(syn::Error::new(
48+
e.span(),
49+
format!("unknown attribute format. expected #[export(...)]: {e}"),
50+
));
51+
return (kind, errors);
52+
}
53+
};
54+
55+
for meta in list.nested.into_iter() {
56+
let syn::NestedMeta::Meta(Meta::NameValue(pair)) = meta else {
57+
errors.push(syn::Error::new(
58+
meta.span(),
59+
"invalid syntax. expected #[export(key = \"value\")]",
60+
));
61+
continue;
62+
};
63+
64+
if !pair.path.is_ident("kind") {
65+
errors.push(syn::Error::new(
66+
pair.span(),
67+
format!("found {}, expected kind", pair.path.into_token_stream()),
68+
));
69+
continue;
70+
}
71+
72+
let syn::Lit::Str(str) = pair.lit else {
73+
errors.push(syn::Error::new(
74+
pair.lit.span(),
75+
"string literal expected, wrap with double quotes",
76+
));
77+
continue;
78+
};
79+
80+
match str.value().as_str() {
81+
"enum" => {
82+
if kind.is_some() {
83+
errors.push(syn::Error::new(str.span(), "kind already set"));
84+
} else {
85+
kind = Some(Kind::Enum);
86+
}
87+
}
88+
_ => {
89+
errors.push(syn::Error::new(str.span(), "unknown kind, expected enum"));
90+
}
91+
}
92+
}
93+
94+
(kind, errors)
95+
});
96+
97+
if let Some(err) = errors.into_iter().reduce(|mut acc, err| {
98+
acc.combine(err);
99+
acc
100+
}) {
101+
return Err(err);
102+
}
103+
104+
match kind {
105+
Some(kind) => Ok(DeriveData { ident, kind, data }),
106+
None => Err(syn::Error::new(Span::call_site(), "kind not found")),
107+
}
108+
}
109+
110+
fn err_only_supports_fieldless_enums(span: Span) -> syn::Error {
111+
syn::Error::new(span, "#[derive(Export)] only supports fieldless enums")
112+
}
113+
114+
pub(crate) fn derive_export(input: DeriveInput) -> syn::Result<TokenStream2> {
115+
let derive_data = parse_derive_input(input)?;
116+
117+
match derive_data.kind {
118+
Kind::Enum => {
119+
let derived_enum = match derive_data.data {
120+
syn::Data::Enum(data) => data,
121+
syn::Data::Struct(data) => {
122+
return Err(err_only_supports_fieldless_enums(data.struct_token.span()));
123+
}
124+
syn::Data::Union(data) => {
125+
return Err(err_only_supports_fieldless_enums(data.union_token.span()));
126+
}
127+
};
128+
let export_impl = impl_export(&derive_data.ident, &derived_enum)?;
129+
Ok(export_impl)
130+
}
131+
}
132+
}
133+
134+
fn impl_export(enum_ty: &syn::Ident, data: &syn::DataEnum) -> syn::Result<TokenStream2> {
135+
let err = data
136+
.variants
137+
.iter()
138+
.filter(|variant| !matches!(variant.fields, Fields::Unit))
139+
.map(|variant| err_only_supports_fieldless_enums(variant.ident.span()))
140+
.reduce(|mut acc, err| {
141+
acc.combine(err);
142+
acc
143+
});
144+
if let Some(err) = err {
145+
return Err(err);
146+
}
147+
148+
let gdnative_core = crate_gdnative_core();
149+
let mappings = data
150+
.variants
151+
.iter()
152+
.map(|variant| {
153+
let key = &variant.ident;
154+
let val = quote! { #enum_ty::#key as i64 };
155+
quote! { #gdnative_core::export::hint::EnumHintEntry::with_value(stringify!(#key).to_string(), #val) }
156+
})
157+
.collect::<Vec<_>>();
158+
159+
let impl_block = quote! {
160+
const _: () = {
161+
pub enum NoHint {}
162+
163+
impl #gdnative_core::export::Export for #enum_ty {
164+
type Hint = NoHint;
165+
166+
#[inline]
167+
fn export_info(_hint: Option<Self::Hint>) -> #gdnative_core::export::ExportInfo {
168+
let mappings = vec![ #(#mappings),* ];
169+
let enum_hint = #gdnative_core::export::hint::EnumHint::with_entries(mappings);
170+
return #gdnative_core::export::hint::IntHint::<i64>::Enum(enum_hint).export_info();
171+
}
172+
}
173+
};
174+
};
175+
176+
Ok(impl_block)
177+
}

gdnative-derive/src/lib.rs

+59
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,64 @@ 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+
/// #[export(kind = "enum")]
680+
/// #[repr(i32)]
681+
/// enum Dir {
682+
/// Up = 1,
683+
/// Down = -1,
684+
/// }
685+
///
686+
/// #[derive(NativeClass)]
687+
/// #[no_constructor]
688+
/// struct Move {
689+
/// #[property]
690+
/// pub dir: Dir,
691+
/// }
692+
/// ```
693+
///
694+
/// You can't derive `Export` on `enum` that has non-unit variant.
695+
///
696+
/// ```compile_fail
697+
/// use gdnative::prelude::*;
698+
///
699+
/// #[derive(Debug, PartialEq, Clone, Copy, Export)]
700+
/// enum Action {
701+
/// Move((f32, f32, f32)),
702+
/// Attack(u64),
703+
/// }
704+
/// ```
705+
///
706+
/// You can't derive `Export` on `struct` or `union`.
707+
///
708+
/// ```compile_fail
709+
/// use gdnative::prelude::*;
710+
///
711+
/// #[derive(Export)]
712+
/// struct Foo {
713+
/// f1: i32
714+
/// }
715+
/// ```
716+
#[proc_macro_derive(Export, attributes(export))]
717+
pub fn derive_export(input: TokenStream) -> TokenStream {
718+
let derive_input = syn::parse_macro_input!(input as syn::DeriveInput);
719+
match export::derive_export(derive_input) {
720+
Ok(stream) => stream.into(),
721+
Err(err) => err.to_compile_error().into(),
722+
}
723+
}
724+
666725
/// Returns a standard header for derived implementations.
667726
///
668727
/// Adds the `automatically_derived` attribute and prevents common lints from triggering

gdnative/tests/ui.rs

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ 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_*.rs");
4347
}
4448

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

gdnative/tests/ui/export_fail_01.rs

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

gdnative/tests/ui/export_fail_02.rs

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use gdnative::prelude::*;
2+
3+
#[derive(Export, ToVariant)]
4+
#[export(kind = "enum")]
5+
pub struct Foo {
6+
bar: i32,
7+
}
8+
9+
fn main() {}
+5
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:5:5
3+
|
4+
5 | pub struct Foo {
5+
| ^^^^^^

0 commit comments

Comments
 (0)