Skip to content

Commit d92c7d4

Browse files
authored
Merge pull request #1166 from godot-rust/feature/export-file-arrays
Support `@export_file`, `@export_dir` etc. for `Array<GString>` and `PackedStringArray`
2 parents 2f670db + 0065fb5 commit d92c7d4

File tree

9 files changed

+231
-61
lines changed

9 files changed

+231
-61
lines changed

.github/workflows/release-version.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ env:
2020
# Crates to publish -- important, this doesn't work when there are spaces in any of the paths!
2121
# Keep in sync with update-version.sh
2222
GDEXT_CRATES: >
23+
godot-bindings
24+
godot-codegen
2325
godot-macros
2426
godot-ffi
2527
godot-cell

godot-bindings/src/lib.rs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -217,20 +217,16 @@ pub fn remove_dir_all_reliable(path: &Path) {
217217
}
218218
}
219219

220-
// Duplicates code from `make_gdext_build_struct` in `godot-codegen/generator/gdext_build_struct.rs`.
220+
/// Concrete check against an API level, not runtime level.
221+
///
222+
/// Necessary in `build.rs`, which doesn't itself have the cfgs.
221223
pub fn before_api(major_minor: &str) -> bool {
222-
let mut parts = major_minor.split('.');
223-
let queried_major = parts
224-
.next()
225-
.unwrap()
226-
.parse::<u8>()
227-
.expect("invalid major version");
228-
let queried_minor = parts
229-
.next()
230-
.unwrap()
231-
.parse::<u8>()
232-
.expect("invalid minor version");
233-
assert_eq!(queried_major, 4, "major version must be 4");
224+
let queried_minor = major_minor
225+
.strip_prefix("4.")
226+
.expect("major version must be 4");
227+
228+
let queried_minor = queried_minor.parse::<u8>().expect("invalid minor version");
229+
234230
let godot_version = get_godot_version();
235231
godot_version.minor < queried_minor
236232
}

godot-core/src/meta/property_info.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ use godot_ffi::VariantType;
2323
/// Keeps the actual allocated values (the `sys` equivalent only keeps pointers, which fall out of scope).
2424
#[derive(Debug, Clone)]
2525
// Note: is not #[non_exhaustive], so adding fields is a breaking change. Mostly used internally at the moment though.
26+
// Note: There was an idea of a high-level representation of the following, but it's likely easier and more efficient to use introspection
27+
// APIs like `is_array_of_elem()`, unless there's a real user-facing need.
28+
// pub(crate) enum SimplePropertyType {
29+
// Variant { ty: VariantType },
30+
// Array { elem_ty: VariantType },
31+
// Object { class_name: ClassName },
32+
// }
2633
pub struct PropertyInfo {
2734
/// Which type this property has.
2835
///
@@ -134,6 +141,21 @@ impl PropertyInfo {
134141
}
135142
}
136143

144+
// ------------------------------------------------------------------------------------------------------------------------------------------
145+
// Introspection API -- could be made public in the future
146+
147+
pub(crate) fn is_array_of_elem<T>(&self) -> bool
148+
where
149+
T: ArrayElement,
150+
{
151+
self.variant_type == VariantType::ARRAY
152+
&& self.hint_info.hint == PropertyHint::ARRAY_TYPE
153+
&& self.hint_info.hint_string == T::Via::godot_type_name().into()
154+
}
155+
156+
// ------------------------------------------------------------------------------------------------------------------------------------------
157+
// FFI conversion functions
158+
137159
/// Converts to the FFI type. Keep this object allocated while using that!
138160
pub fn property_sys(&self) -> sys::GDExtensionPropertyInfo {
139161
use crate::obj::EngineBitfield as _;

godot-core/src/meta/traits.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ pub trait GodotType: GodotConvert<Via = Self> + sealed::Sealed + Sized + 'static
118118
))
119119
}
120120

121+
/// Returns a string representation of the Godot type name, as it is used in several property hint contexts.
122+
///
123+
/// Examples:
124+
/// - `MyClass` for objects
125+
/// - `StringName`, `AABB` or `int` for builtins
126+
/// - `Array` for arrays
121127
#[doc(hidden)]
122128
fn godot_type_name() -> String;
123129

@@ -165,7 +171,9 @@ pub trait ArrayElement: ToGodot + FromGodot + sealed::Sealed + meta::ParamType {
165171
// Note: several indirections in ArrayElement and the global `element_*` functions go through `GodotConvert::Via`,
166172
// to not require Self: GodotType. What matters is how array elements map to Godot on the FFI level (GodotType trait).
167173

168-
/// Returns the representation of this type as a type string.
174+
/// Returns the representation of this type as a type string, e.g. `"4:"` for string, or `"24:34/MyClass"` for objects.
175+
///
176+
/// (`4` and `24` are variant type ords; `34` is `PropertyHint::NODE_TYPE` ord).
169177
///
170178
/// Used for elements in arrays (the latter despite `ArrayElement` not having a direct relation).
171179
///

godot-core/src/registry/property.rs

Lines changed: 77 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,10 @@ where
226226
pub mod export_info_functions {
227227
use crate::builtin::GString;
228228
use crate::global::PropertyHint;
229-
use crate::meta::PropertyHintInfo;
229+
use crate::meta::{GodotType, PropertyHintInfo, PropertyInfo};
230+
use crate::obj::EngineEnum;
231+
use crate::registry::property::Export;
232+
use godot_ffi::VariantType;
230233

231234
/// Turn a list of variables into a comma separated string containing only the identifiers corresponding
232235
/// to a true boolean variable.
@@ -409,37 +412,89 @@ pub mod export_info_functions {
409412
}
410413
}
411414

412-
/// Equivalent to `@export_file` in Godot.
413-
///
414-
/// Pass an empty string to have no filter.
415-
pub fn export_file<S: AsRef<str>>(filter: S) -> PropertyHintInfo {
416-
export_file_inner(false, filter)
417-
}
415+
/// Handles `@export_file`, `@export_global_file`, `@export_dir` and `@export_global_dir`.
416+
pub fn export_file_or_dir<T: Export>(
417+
is_file: bool,
418+
is_global: bool,
419+
filter: impl AsRef<str>,
420+
) -> PropertyHintInfo {
421+
let field_ty = T::Via::property_info("");
422+
let filter = filter.as_ref();
423+
debug_assert!(is_file || filter.is_empty()); // Dir never has filter.
418424

419-
/// Equivalent to `@export_global_file` in Godot.
420-
///
421-
/// Pass an empty string to have no filter.
422-
pub fn export_global_file<S: AsRef<str>>(filter: S) -> PropertyHintInfo {
423-
export_file_inner(true, filter)
425+
export_file_or_dir_inner(&field_ty, is_file, is_global, filter)
424426
}
425427

426-
pub fn export_file_inner<S: AsRef<str>>(global: bool, filter: S) -> PropertyHintInfo {
427-
let hint = if global {
428-
PropertyHint::GLOBAL_FILE
429-
} else {
430-
PropertyHint::FILE
428+
pub fn export_file_or_dir_inner(
429+
field_ty: &PropertyInfo,
430+
is_file: bool,
431+
is_global: bool,
432+
filter: &str,
433+
) -> PropertyHintInfo {
434+
let hint = match (is_file, is_global) {
435+
(true, true) => PropertyHint::GLOBAL_FILE,
436+
(true, false) => PropertyHint::FILE,
437+
(false, true) => PropertyHint::GLOBAL_DIR,
438+
(false, false) => PropertyHint::DIR,
431439
};
432440

441+
// Returned value depends on field type.
442+
match field_ty.variant_type {
443+
// GString field:
444+
// { "type": 4, "hint": 13, "hint_string": "*.png" }
445+
VariantType::STRING => PropertyHintInfo {
446+
hint,
447+
hint_string: GString::from(filter),
448+
},
449+
450+
// Array<GString> or PackedStringArray field:
451+
// { "type": 28, "hint": 23, "hint_string": "4/13:*.png" }
452+
#[cfg(since_api = "4.3")]
453+
VariantType::PACKED_STRING_ARRAY => to_string_array_hint(hint, filter),
454+
#[cfg(since_api = "4.3")]
455+
VariantType::ARRAY if field_ty.is_array_of_elem::<GString>() => {
456+
to_string_array_hint(hint, filter)
457+
}
458+
459+
_ => {
460+
// E.g. `global_file`.
461+
let attribute_name = hint.as_str().to_lowercase();
462+
463+
// TODO nicer error handling.
464+
// Compile time may be difficult (at least without extra traits... maybe const fn?). But at least more context info, field name etc.
465+
#[cfg(since_api = "4.3")]
466+
panic!(
467+
"#[export({attribute_name})] only supports GString, Array<String> or PackedStringArray field types\n\
468+
encountered: {field_ty:?}"
469+
);
470+
471+
#[cfg(before_api = "4.3")]
472+
panic!(
473+
"#[export({attribute_name})] only supports GString type prior to Godot 4.3\n\
474+
encountered: {field_ty:?}"
475+
);
476+
}
477+
}
478+
}
479+
480+
/// For `Array<GString>` and `PackedStringArray` fields using one of the `@export[_global]_{file|dir}` annotations.
481+
///
482+
/// Formats: `"4/13:"`, `"4/15:*.png"`, ...
483+
fn to_string_array_hint(hint: PropertyHint, filter: &str) -> PropertyHintInfo {
484+
let variant_ord = VariantType::STRING.ord(); // "4"
485+
let hint_ord = hint.ord();
486+
let hint_string = format!("{variant_ord}/{hint_ord}");
487+
433488
PropertyHintInfo {
434-
hint,
435-
hint_string: filter.as_ref().into(),
489+
hint: PropertyHint::TYPE_STRING,
490+
hint_string: format!("{hint_string}:{filter}").into(),
436491
}
437492
}
438493

439494
pub fn export_placeholder<S: AsRef<str>>(placeholder: S) -> PropertyHintInfo {
440495
PropertyHintInfo {
441496
hint: PropertyHint::PLACEHOLDER_TEXT,
442-
hint_string: placeholder.as_ref().into(),
497+
hint_string: GString::from(placeholder.as_ref()),
443498
}
444499
}
445500

@@ -468,8 +523,6 @@ pub mod export_info_functions {
468523
export_flags_3d_physics => LAYERS_3D_PHYSICS,
469524
export_flags_3d_render => LAYERS_3D_RENDER,
470525
export_flags_3d_navigation => LAYERS_3D_NAVIGATION,
471-
export_dir => DIR,
472-
export_global_dir => GLOBAL_DIR,
473526
export_multiline => MULTILINE_TEXT,
474527
export_color_no_alpha => COLOR_NO_ALPHA,
475528
);
@@ -609,9 +662,9 @@ pub(crate) fn builtin_type_string<T: GodotType>() -> String {
609662

610663
// Godot 4.3 changed representation for type hints, see https://github.com/godotengine/godot/pull/90716.
611664
if sys::GdextBuild::since_api("4.3") {
612-
format!("{}:", variant_type.sys())
665+
format!("{}:", variant_type.ord())
613666
} else {
614-
format!("{}:{}", variant_type.sys(), T::godot_type_name())
667+
format!("{}:{}", variant_type.ord(), T::godot_type_name())
615668
}
616669
}
617670

godot-macros/src/class/data_models/field_export.rs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,15 @@ macro_rules! quote_export_func {
388388
Some(quote! {
389389
::godot::register::property::export_info_functions::$function_name($($tt)*)
390390
})
391-
}
391+
};
392+
393+
// Passes in a previously declared local `type FieldType = ...` as first generic argument.
394+
// Doesn't work if function takes other generic arguments -- in that case it could be converted to a Type<...> parameter.
395+
($function_name:ident < T > ($($tt:tt)*)) => {
396+
Some(quote! {
397+
::godot::register::property::export_info_functions::$function_name::<FieldType>($($tt)*)
398+
})
399+
};
392400
}
393401

394402
impl ExportType {
@@ -487,29 +495,27 @@ impl ExportType {
487495
} => quote_export_func! { export_flags_3d_navigation() },
488496

489497
Self::File {
490-
global: false,
491-
kind: FileKind::Dir,
492-
} => quote_export_func! { export_dir() },
493-
494-
Self::File {
495-
global: true,
498+
global,
496499
kind: FileKind::Dir,
497-
} => quote_export_func! { export_global_dir() },
500+
} => {
501+
let filter = quote! { "" };
502+
quote_export_func! { export_file_or_dir<T>(false, #global, #filter) }
503+
}
498504

499505
Self::File {
500506
global,
501507
kind: FileKind::File { filter },
502508
} => {
503509
let filter = filter.clone().unwrap_or(quote! { "" });
504-
505-
quote_export_func! { export_file_inner(#global, #filter) }
510+
quote_export_func! { export_file_or_dir<T>(true, #global, #filter) }
506511
}
507512

508513
Self::Multiline => quote_export_func! { export_multiline() },
509514

510515
Self::PlaceholderText { placeholder } => quote_export_func! {
511516
export_placeholder(#placeholder)
512517
},
518+
513519
Self::ColorNoAlpha => quote_export_func! { export_color_no_alpha() },
514520
}
515521
}

godot-macros/src/class/data_models/property.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,9 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream {
154154
);
155155

156156
export_tokens.push(quote! {
157-
::godot::register::private::#registration_fn::<#class_name, #field_type>(
157+
// This type may be reused in #hint, in case of generic functions.
158+
type FieldType = #field_type;
159+
::godot::register::private::#registration_fn::<#class_name, FieldType>(
158160
#field_name,
159161
#getter_tokens,
160162
#setter_tokens,

0 commit comments

Comments
 (0)