Skip to content

Support @export_file, @export_dir etc. for Array<GString> and PackedStringArray #1166

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions godot-bindings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,20 +217,16 @@ pub fn remove_dir_all_reliable(path: &Path) {
}
}

// Duplicates code from `make_gdext_build_struct` in `godot-codegen/generator/gdext_build_struct.rs`.
/// Concrete check against an API level, not runtime level.
///
/// Necessary in `build.rs`, which doesn't itself have the cfgs.
pub fn before_api(major_minor: &str) -> bool {
let mut parts = major_minor.split('.');
let queried_major = parts
.next()
.unwrap()
.parse::<u8>()
.expect("invalid major version");
let queried_minor = parts
.next()
.unwrap()
.parse::<u8>()
.expect("invalid minor version");
assert_eq!(queried_major, 4, "major version must be 4");
let queried_minor = major_minor
.strip_prefix("4.")
.expect("major version must be 4");

let queried_minor = queried_minor.parse::<u8>().expect("invalid minor version");

let godot_version = get_godot_version();
godot_version.minor < queried_minor
}
Expand Down
22 changes: 22 additions & 0 deletions godot-core/src/meta/property_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ use godot_ffi::VariantType;
/// Keeps the actual allocated values (the `sys` equivalent only keeps pointers, which fall out of scope).
#[derive(Debug, Clone)]
// Note: is not #[non_exhaustive], so adding fields is a breaking change. Mostly used internally at the moment though.
// Note: There was an idea of a high-level representation of the following, but it's likely easier and more efficient to use introspection
// APIs like `is_array_of_elem()`, unless there's a real user-facing need.
// pub(crate) enum SimplePropertyType {
// Variant { ty: VariantType },
// Array { elem_ty: VariantType },
// Object { class_name: ClassName },
// }
pub struct PropertyInfo {
/// Which type this property has.
///
Expand Down Expand Up @@ -134,6 +141,21 @@ impl PropertyInfo {
}
}

// ------------------------------------------------------------------------------------------------------------------------------------------
// Introspection API -- could be made public in the future

pub(crate) fn is_array_of_elem<T>(&self) -> bool
where
T: ArrayElement,
{
self.variant_type == VariantType::ARRAY
&& self.hint_info.hint == PropertyHint::ARRAY_TYPE
&& self.hint_info.hint_string == T::Via::godot_type_name().into()
}

// ------------------------------------------------------------------------------------------------------------------------------------------
// FFI conversion functions

/// Converts to the FFI type. Keep this object allocated while using that!
pub fn property_sys(&self) -> sys::GDExtensionPropertyInfo {
use crate::obj::EngineBitfield as _;
Expand Down
10 changes: 9 additions & 1 deletion godot-core/src/meta/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ pub trait GodotType: GodotConvert<Via = Self> + sealed::Sealed + Sized + 'static
))
}

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

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

/// Returns the representation of this type as a type string.
/// Returns the representation of this type as a type string, e.g. `"4:"` for string, or `"24:34/MyClass"` for objects.
///
/// (`4` and `24` are variant type ords; `34` is `PropertyHint::NODE_TYPE` ord).
///
/// Used for elements in arrays (the latter despite `ArrayElement` not having a direct relation).
///
Expand Down
101 changes: 77 additions & 24 deletions godot-core/src/registry/property.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,10 @@ where
pub mod export_info_functions {
use crate::builtin::GString;
use crate::global::PropertyHint;
use crate::meta::PropertyHintInfo;
use crate::meta::{GodotType, PropertyHintInfo, PropertyInfo};
use crate::obj::EngineEnum;
use crate::registry::property::Export;
use godot_ffi::VariantType;

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

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

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

pub fn export_file_inner<S: AsRef<str>>(global: bool, filter: S) -> PropertyHintInfo {
let hint = if global {
PropertyHint::GLOBAL_FILE
} else {
PropertyHint::FILE
pub fn export_file_or_dir_inner(
field_ty: &PropertyInfo,
is_file: bool,
is_global: bool,
filter: &str,
) -> PropertyHintInfo {
let hint = match (is_file, is_global) {
(true, true) => PropertyHint::GLOBAL_FILE,
(true, false) => PropertyHint::FILE,
(false, true) => PropertyHint::GLOBAL_DIR,
(false, false) => PropertyHint::DIR,
};

// Returned value depends on field type.
match field_ty.variant_type {
// GString field:
// { "type": 4, "hint": 13, "hint_string": "*.png" }
VariantType::STRING => PropertyHintInfo {
hint,
hint_string: GString::from(filter),
},

// Array<GString> or PackedStringArray field:
// { "type": 28, "hint": 23, "hint_string": "4/13:*.png" }
#[cfg(since_api = "4.3")]
VariantType::PACKED_STRING_ARRAY => to_string_array_hint(hint, filter),
#[cfg(since_api = "4.3")]
VariantType::ARRAY if field_ty.is_array_of_elem::<GString>() => {
to_string_array_hint(hint, filter)
}

_ => {
// E.g. `global_file`.
let attribute_name = hint.as_str().to_lowercase();

// TODO nicer error handling.
// Compile time may be difficult (at least without extra traits... maybe const fn?). But at least more context info, field name etc.
#[cfg(since_api = "4.3")]
panic!(
"#[export({attribute_name})] only supports GString, Array<String> or PackedStringArray field types\n\
encountered: {field_ty:?}"
);

#[cfg(before_api = "4.3")]
panic!(
"#[export({attribute_name})] only supports GString type prior to Godot 4.3\n\
encountered: {field_ty:?}"
);
}
}
}

/// For `Array<GString>` and `PackedStringArray` fields using one of the `@export[_global]_{file|dir}` annotations.
///
/// Formats: `"4/13:"`, `"4/15:*.png"`, ...
fn to_string_array_hint(hint: PropertyHint, filter: &str) -> PropertyHintInfo {
let variant_ord = VariantType::STRING.ord(); // "4"
let hint_ord = hint.ord();
let hint_string = format!("{variant_ord}/{hint_ord}");

PropertyHintInfo {
hint,
hint_string: filter.as_ref().into(),
hint: PropertyHint::TYPE_STRING,
hint_string: format!("{hint_string}:{filter}").into(),
}
}

pub fn export_placeholder<S: AsRef<str>>(placeholder: S) -> PropertyHintInfo {
PropertyHintInfo {
hint: PropertyHint::PLACEHOLDER_TEXT,
hint_string: placeholder.as_ref().into(),
hint_string: GString::from(placeholder.as_ref()),
}
}

Expand Down Expand Up @@ -468,8 +523,6 @@ pub mod export_info_functions {
export_flags_3d_physics => LAYERS_3D_PHYSICS,
export_flags_3d_render => LAYERS_3D_RENDER,
export_flags_3d_navigation => LAYERS_3D_NAVIGATION,
export_dir => DIR,
export_global_dir => GLOBAL_DIR,
export_multiline => MULTILINE_TEXT,
export_color_no_alpha => COLOR_NO_ALPHA,
);
Expand Down Expand Up @@ -609,9 +662,9 @@ pub(crate) fn builtin_type_string<T: GodotType>() -> String {

// Godot 4.3 changed representation for type hints, see https://github.com/godotengine/godot/pull/90716.
if sys::GdextBuild::since_api("4.3") {
format!("{}:", variant_type.sys())
format!("{}:", variant_type.ord())
} else {
format!("{}:{}", variant_type.sys(), T::godot_type_name())
format!("{}:{}", variant_type.ord(), T::godot_type_name())
}
}

Expand Down
26 changes: 16 additions & 10 deletions godot-macros/src/class/data_models/field_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,15 @@ macro_rules! quote_export_func {
Some(quote! {
::godot::register::property::export_info_functions::$function_name($($tt)*)
})
}
};

// Passes in a previously declared local `type FieldType = ...` as first generic argument.
// Doesn't work if function takes other generic arguments -- in that case it could be converted to a Type<...> parameter.
($function_name:ident < T > ($($tt:tt)*)) => {
Some(quote! {
::godot::register::property::export_info_functions::$function_name::<FieldType>($($tt)*)
})
};
}

impl ExportType {
Expand Down Expand Up @@ -487,29 +495,27 @@ impl ExportType {
} => quote_export_func! { export_flags_3d_navigation() },

Self::File {
global: false,
kind: FileKind::Dir,
} => quote_export_func! { export_dir() },

Self::File {
global: true,
global,
kind: FileKind::Dir,
} => quote_export_func! { export_global_dir() },
} => {
let filter = quote! { "" };
quote_export_func! { export_file_or_dir<T>(false, #global, #filter) }
}

Self::File {
global,
kind: FileKind::File { filter },
} => {
let filter = filter.clone().unwrap_or(quote! { "" });

quote_export_func! { export_file_inner(#global, #filter) }
quote_export_func! { export_file_or_dir<T>(true, #global, #filter) }
}

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

Self::PlaceholderText { placeholder } => quote_export_func! {
export_placeholder(#placeholder)
},

Self::ColorNoAlpha => quote_export_func! { export_color_no_alpha() },
}
}
Expand Down
4 changes: 3 additions & 1 deletion godot-macros/src/class/data_models/property.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream {
);

export_tokens.push(quote! {
::godot::register::private::#registration_fn::<#class_name, #field_type>(
// This type may be reused in #hint, in case of generic functions.
type FieldType = #field_type;
::godot::register::private::#registration_fn::<#class_name, FieldType>(
#field_name,
#getter_tokens,
#setter_tokens,
Expand Down
Loading
Loading