godot_core/registry/property.rs
1/*
2 * Copyright (c) godot-rust; Bromeon and contributors.
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
6 */
7
8//! Registration support for property types.
9
10use crate::classes;
11use crate::global::PropertyHint;
12use godot_ffi as sys;
13use godot_ffi::{GodotNullableFfi, VariantType};
14use std::fmt::Display;
15
16use crate::meta::{ClassName, FromGodot, GodotConvert, GodotType, PropertyHintInfo, ToGodot};
17use crate::obj::{EngineEnum, GodotClass};
18
19// ----------------------------------------------------------------------------------------------------------------------------------------------
20// Trait definitions
21
22// Note: HTML link for #[var] works if this symbol is inside prelude, but not in register::property.
23/// Trait implemented for types that can be used as [`#[var]`](../register/derive.GodotClass.html#properties-and-exports) fields.
24///
25/// This creates a copy of the value, according to copy semantics provided by `Clone`. For example, `Array`, `Dictionary` and `Gd` are
26/// returned by shared reference instead of copying the actual data.
27///
28/// This does not require [`FromGodot`] or [`ToGodot`], so that something can be used as a property even if it can't be used in function
29/// arguments/return types.
30///
31/// See also [`Export`], a specialization of this trait for properties exported to the editor UI.
32///
33/// For enums, this trait can be derived using the [`#[derive(Var)]`](../derive.Var.html) macro.
34#[doc(alias = "property")]
35//
36// on_unimplemented: we also mention #[export] here, because we can't control the order of error messages.
37// Missing Export often also means missing Var trait, and so the Var error message appears first.
38#[diagnostic::on_unimplemented(
39 message = "`#[var]` properties require `Var` trait; #[export] ones require `Export` trait",
40 label = "type cannot be used as a property",
41 note = "see also: https://godot-rust.github.io/book/register/properties.html"
42)]
43pub trait Var: GodotConvert {
44 fn get_property(&self) -> Self::Via;
45
46 fn set_property(&mut self, value: Self::Via);
47
48 /// Specific property hints, only override if they deviate from [`GodotType::property_info`], e.g. for enums/newtypes.
49 fn var_hint() -> PropertyHintInfo {
50 Self::Via::property_hint_info()
51 }
52}
53
54// Note: HTML link for #[export] works if this symbol is inside prelude, but not in register::property.
55/// Trait implemented for types that can be used as [`#[export]`](../register/derive.GodotClass.html#properties-and-exports) fields.
56///
57/// To export objects, see the [_Exporting_ section of `Gd<T>`](../obj/struct.Gd.html#exporting).
58///
59/// For enums, this trait can be derived using the [`#[derive(Export)]`](../derive.Export.html) macro.
60#[doc(alias = "property")]
61//
62// on_unimplemented: mentioning both Var + Export; see above.
63#[diagnostic::on_unimplemented(
64 message = "`#[var]` properties require `Var` trait; #[export] ones require `Export` trait",
65 label = "type cannot be used as a property",
66 note = "see also: https://godot-rust.github.io/book/register/properties.html"
67)]
68pub trait Export: Var {
69 /// The export info to use for an exported field of this type, if no other export info is specified.
70 fn export_hint() -> PropertyHintInfo {
71 <Self as Var>::var_hint()
72 }
73
74 /// If this is a class inheriting `Node`, returns the `ClassName`; otherwise `None`.
75 ///
76 /// Only overridden for `Gd<T>`, to detect erroneous exports of `Node` inside a `Resource` class.
77 #[allow(clippy::wrong_self_convention)]
78 #[doc(hidden)]
79 fn as_node_class() -> Option<ClassName> {
80 None
81 }
82}
83
84/// Marker trait to identify `GodotType`s that can be directly used with an `#[export]`.
85///
86/// Implemented pretty much for all the [`GodotTypes`][GodotType] that are not [`GodotClass`].
87/// Provides a few blanket implementations and, by itself, has no implications
88/// for the [`Var`] or [`Export`] traits.
89///
90/// Types which don't implement the `BuiltinExport` trait can't be used directly as an `#[export]`
91/// and must be handled using associated algebraic types, such as:
92/// * [`Option<T>`], which represents optional value that can be null when used.
93/// * [`OnEditor<T>`][crate::obj::OnEditor], which represents value that must not be null when used.
94// Some Godot Types which are inherently non-nullable (e.g., `Gd<T>`),
95// might have their value set to null by the editor. Additionally, Godot must generate
96// initial, default value for such properties, causing memory leaks.
97// Such `GodotType`s don't implement `BuiltinExport`.
98//
99// Note: This marker trait is required to create a blanket implementation
100// for `OnEditor<T>` where `T` is anything other than `GodotClass`.
101// An alternative approach would involve introducing an extra associated type
102// to `GodotType` trait. However, this would not be ideal — `GodotType` is used
103// in contexts unrelated to `#[export]`, and adding unnecessary complexity
104// should be avoided. Since Rust does not yet support specialization (i.e. negative trait bounds),
105// this `MarkerTrait` serves as the intended solution to recognize aforementioned types.
106pub trait BuiltinExport {}
107
108/// This function only exists as a place to add doc-tests for the `Export` trait.
109///
110/// Test with export of exportable type should succeed:
111/// ```no_run
112/// use godot::prelude::*;
113///
114/// #[derive(GodotClass)]
115/// #[class(init)]
116/// struct Foo {
117/// #[export]
118/// obj: Option<Gd<Resource>>,
119/// #[export]
120/// array: Array<Gd<Resource>>,
121/// }
122/// ```
123///
124/// Tests with export of non-exportable type should fail:
125/// ```compile_fail
126/// use godot::prelude::*;
127///
128/// #[derive(GodotClass)]
129/// #[class(init)]
130/// struct Foo {
131/// #[export]
132/// obj: Option<Gd<Object>>,
133/// }
134/// ```
135///
136/// Neither `Gd<T>` nor `DynGd<T, D>` can be used with an `#[export]` directly:
137///
138/// ```compile_fail
139/// use godot::prelude::*;
140///
141/// #[derive(GodotClass)]
142/// #[class(init, base = Node)]
143/// struct MyClass {
144/// #[export]
145/// editor_property: Gd<Resource>,
146/// }
147/// ```
148///
149/// ```compile_fail
150/// use godot::prelude::*;
151///
152/// #[derive(GodotClass)]
153/// #[class(init, base = Node)]
154/// struct MyClass {
155/// #[export]
156/// editor_property: DynGd<Node, dyn Display>,
157/// }
158/// ```
159///
160/// ```compile_fail
161/// use godot::prelude::*;
162///
163/// #[derive(GodotClass)]
164/// #[class(init)]
165/// struct Foo {
166/// #[export]
167/// array: Array<Gd<Object>>,
168/// }
169/// ```
170#[allow(dead_code)]
171fn export_doctests() {}
172
173// ----------------------------------------------------------------------------------------------------------------------------------------------
174// Blanket impls for Option<T>
175
176impl<T> Var for Option<T>
177where
178 T: Var + FromGodot,
179 Option<T>: GodotConvert<Via = Option<T::Via>>,
180{
181 fn get_property(&self) -> Self::Via {
182 self.as_ref().map(Var::get_property)
183 }
184
185 fn set_property(&mut self, value: Self::Via) {
186 match value {
187 Some(value) => {
188 if let Some(current_value) = self {
189 current_value.set_property(value)
190 } else {
191 *self = Some(FromGodot::from_godot(value))
192 }
193 }
194 None => *self = None,
195 }
196 }
197}
198
199impl<T> Export for Option<T>
200where
201 T: Export,
202 Option<T>: Var,
203{
204 fn export_hint() -> PropertyHintInfo {
205 T::export_hint()
206 }
207}
208
209impl<T> BuiltinExport for Option<T>
210where
211 T: GodotType,
212 T::Ffi: GodotNullableFfi,
213{
214}
215
216// ----------------------------------------------------------------------------------------------------------------------------------------------
217// Export machinery
218
219/// Functions used to translate user-provided arguments into export hints.
220///
221/// You are not supposed to use these functions directly. They are used by the `#[export]` macro to generate the correct export hint.
222///
223/// Each function is named the same as the equivalent Godot annotation.
224/// For instance, `@export_range` in Godot is `fn export_range` here.
225pub mod export_info_functions {
226 use crate::builtin::GString;
227 use crate::global::PropertyHint;
228 use crate::meta::{GodotType, PropertyHintInfo, PropertyInfo};
229 use crate::obj::EngineEnum;
230 use crate::registry::property::Export;
231 use godot_ffi::VariantType;
232
233 /// Turn a list of variables into a comma separated string containing only the identifiers corresponding
234 /// to a true boolean variable.
235 macro_rules! comma_separate_boolean_idents {
236 ($( $ident:ident),* $(,)?) => {
237 {
238 let mut strings = Vec::new();
239
240 $(
241 if $ident {
242 strings.push(stringify!($ident));
243 }
244 )*
245
246 strings.join(",")
247 }
248 };
249 }
250
251 // We want this to match the options available on `@export_range(..)`
252 /// Mark an exported numerical value to use the editor's range UI.
253 ///
254 /// You'll never call this function itself, but will instead use the macro `#[export(range=(...))]`, as below. The syntax is
255 /// very similar to Godot's [`@export_range`](https://docs.godotengine.org/en/stable/classes/class_%40gdscript.html#class-gdscript-annotation-export-range).
256 /// `min`, `max`, and `step` are `f32` positional arguments, with `step` being optional and defaulting to `1.0`. The rest of
257 /// the arguments can be written in any order. The symbols of type `bool` just need to have those symbols written, and those of type `Option<T>` will be written as `{KEY}={VALUE}`, e.g. `suffix="px"`.
258 ///
259 /// ```
260 /// # use godot::prelude::*;
261 /// #[derive(GodotClass)]
262 /// #[class(init, base=Node)]
263 /// struct MyClassWithRangedValues {
264 /// #[export(range=(0.0, 400.0, 1.0, or_greater, suffix="px"))]
265 /// icon_width: i32,
266 /// #[export(range=(-180.0, 180.0, degrees))]
267 /// angle: f32,
268 /// }
269 /// ```
270 #[allow(clippy::too_many_arguments)]
271 pub fn export_range(
272 min: f64,
273 max: f64,
274 step: Option<f64>,
275 or_greater: bool,
276 or_less: bool,
277 exp: bool,
278 radians_as_degrees: bool,
279 degrees: bool,
280 hide_slider: bool,
281 suffix: Option<String>,
282 ) -> PropertyHintInfo {
283 // From Godot 4.4, GDScript uses `.0` for integral floats, see https://github.com/godotengine/godot/pull/47502.
284 // We still register them the old way, to test compatibility. See also property_template_test.rs.
285
286 let hint_beginning = if let Some(step) = step {
287 format!("{min},{max},{step}")
288 } else {
289 format!("{min},{max}")
290 };
291
292 let rest = comma_separate_boolean_idents!(
293 or_greater,
294 or_less,
295 exp,
296 radians_as_degrees,
297 degrees,
298 hide_slider
299 );
300
301 let mut hint_string = hint_beginning;
302 if !rest.is_empty() {
303 hint_string.push_str(&format!(",{rest}"));
304 }
305 if let Some(suffix) = suffix {
306 hint_string.push_str(&format!(",suffix:{suffix}"));
307 }
308
309 PropertyHintInfo {
310 hint: PropertyHint::RANGE,
311 hint_string: hint_string.into(),
312 }
313 }
314
315 #[doc(hidden)]
316 pub struct ExportValueWithKey<T> {
317 variant: String,
318 key: Option<T>,
319 }
320
321 impl<T: std::fmt::Display> ExportValueWithKey<T> {
322 fn as_hint_string(&self) -> String {
323 let Self { variant, key } = self;
324
325 match key {
326 Some(key) => format!("{variant}:{key}"),
327 None => variant.clone(),
328 }
329 }
330
331 fn slice_as_hint_string<V>(values: &[V]) -> String
332 where
333 for<'a> &'a V: Into<Self>,
334 {
335 let values = values
336 .iter()
337 .map(|v| v.into().as_hint_string())
338 .collect::<Vec<_>>();
339
340 values.join(",")
341 }
342 }
343
344 impl<T, S> From<&(S, Option<T>)> for ExportValueWithKey<T>
345 where
346 T: Clone,
347 S: AsRef<str>,
348 {
349 fn from((variant, key): &(S, Option<T>)) -> Self {
350 Self {
351 variant: variant.as_ref().into(),
352 key: key.clone(),
353 }
354 }
355 }
356
357 type EnumVariant = ExportValueWithKey<i64>;
358
359 /// Equivalent to `@export_enum` in Godot.
360 ///
361 /// A name without a key would be represented as `(name, None)`, and a name with a key as `(name, Some(key))`.
362 ///
363 /// # Examples
364 ///
365 /// ```no_run
366 /// # use godot::register::property::export_info_functions::export_enum;
367 /// export_enum(&[("a", None), ("b", Some(10))]);
368 /// ```
369 pub fn export_enum<T>(variants: &[T]) -> PropertyHintInfo
370 where
371 for<'a> &'a T: Into<EnumVariant>,
372 {
373 let hint_string: String = EnumVariant::slice_as_hint_string(variants);
374
375 PropertyHintInfo {
376 hint: PropertyHint::ENUM,
377 hint_string: hint_string.into(),
378 }
379 }
380
381 pub fn export_exp_easing(attenuation: bool, positive_only: bool) -> PropertyHintInfo {
382 let hint_string = comma_separate_boolean_idents!(attenuation, positive_only);
383
384 PropertyHintInfo {
385 hint: PropertyHint::EXP_EASING,
386 hint_string: hint_string.into(),
387 }
388 }
389
390 type BitFlag = ExportValueWithKey<u32>;
391
392 /// Equivalent to `@export_flags` in Godot.
393 ///
394 /// A flag without a key would be represented as `(flag, None)`, and a flag with a key as `(flag, Some(key))`.
395 ///
396 /// # Examples
397 ///
398 /// ```no_run
399 /// # use godot::register::property::export_info_functions::export_flags;
400 /// export_flags(&[("a", None), ("b", Some(10))]);
401 /// ```
402 pub fn export_flags<T>(bits: &[T]) -> PropertyHintInfo
403 where
404 for<'a> &'a T: Into<BitFlag>,
405 {
406 let hint_string = BitFlag::slice_as_hint_string(bits);
407
408 PropertyHintInfo {
409 hint: PropertyHint::FLAGS,
410 hint_string: hint_string.into(),
411 }
412 }
413
414 /// Handles `@export_file`, `@export_global_file`, `@export_dir` and `@export_global_dir`.
415 pub fn export_file_or_dir<T: Export>(
416 is_file: bool,
417 is_global: bool,
418 filter: impl AsRef<str>,
419 ) -> PropertyHintInfo {
420 let field_ty = T::Via::property_info("");
421 let filter = filter.as_ref();
422 debug_assert!(is_file || filter.is_empty()); // Dir never has filter.
423
424 export_file_or_dir_inner(&field_ty, is_file, is_global, filter)
425 }
426
427 pub fn export_file_or_dir_inner(
428 field_ty: &PropertyInfo,
429 is_file: bool,
430 is_global: bool,
431 filter: &str,
432 ) -> PropertyHintInfo {
433 let hint = match (is_file, is_global) {
434 (true, true) => PropertyHint::GLOBAL_FILE,
435 (true, false) => PropertyHint::FILE,
436 (false, true) => PropertyHint::GLOBAL_DIR,
437 (false, false) => PropertyHint::DIR,
438 };
439
440 // Returned value depends on field type.
441 match field_ty.variant_type {
442 // GString field:
443 // { "type": 4, "hint": 13, "hint_string": "*.png" }
444 VariantType::STRING => PropertyHintInfo {
445 hint,
446 hint_string: GString::from(filter),
447 },
448
449 // Array<GString> or PackedStringArray field:
450 // { "type": 28, "hint": 23, "hint_string": "4/13:*.png" }
451 #[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
452 VariantType::PACKED_STRING_ARRAY => to_string_array_hint(hint, filter),
453 #[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
454 VariantType::ARRAY if field_ty.is_array_of_elem::<GString>() => {
455 to_string_array_hint(hint, filter)
456 }
457
458 _ => {
459 // E.g. `global_file`.
460 let attribute_name = hint.as_str().to_lowercase();
461
462 // TODO nicer error handling.
463 // Compile time may be difficult (at least without extra traits... maybe const fn?). But at least more context info, field name etc.
464 #[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
465 panic!(
466 "#[export({attribute_name})] only supports GString, Array<String> or PackedStringArray field types\n\
467 encountered: {field_ty:?}"
468 );
469
470 #[cfg(before_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(before_api = "4.3")))]
471 panic!(
472 "#[export({attribute_name})] only supports GString type prior to Godot 4.3\n\
473 encountered: {field_ty:?}"
474 );
475 }
476 }
477 }
478
479 /// For `Array<GString>` and `PackedStringArray` fields using one of the `@export[_global]_{file|dir}` annotations.
480 ///
481 /// Formats: `"4/13:"`, `"4/15:*.png"`, ...
482 fn to_string_array_hint(hint: PropertyHint, filter: &str) -> PropertyHintInfo {
483 let variant_ord = VariantType::STRING.ord(); // "4"
484 let hint_ord = hint.ord();
485 let hint_string = format!("{variant_ord}/{hint_ord}");
486
487 PropertyHintInfo {
488 hint: PropertyHint::TYPE_STRING,
489 hint_string: format!("{hint_string}:{filter}").into(),
490 }
491 }
492
493 pub fn export_placeholder<S: AsRef<str>>(placeholder: S) -> PropertyHintInfo {
494 PropertyHintInfo {
495 hint: PropertyHint::PLACEHOLDER_TEXT,
496 hint_string: GString::from(placeholder.as_ref()),
497 }
498 }
499
500 macro_rules! default_export_funcs {
501 (
502 $( $function_name:ident => $property_hint:ident, )*
503 ) => {
504 $(
505 pub fn $function_name() -> PropertyHintInfo {
506 PropertyHintInfo {
507 hint: PropertyHint::$property_hint,
508 hint_string: GString::new()
509 }
510 }
511 )*
512 };
513 }
514
515 // The left side of these declarations follows the export annotation provided by GDScript, whereas the
516 // right side are the corresponding property hint. Godot is not always consistent between the two, such
517 // as `export_multiline` being `PROPERTY_HINT_MULTILINE_TEXT`.
518 default_export_funcs!(
519 export_storage => NONE, // Storage exports don't display in the editor.
520 export_flags_2d_physics => LAYERS_2D_PHYSICS,
521 export_flags_2d_render => LAYERS_2D_RENDER,
522 export_flags_2d_navigation => LAYERS_2D_NAVIGATION,
523 export_flags_3d_physics => LAYERS_3D_PHYSICS,
524 export_flags_3d_render => LAYERS_3D_RENDER,
525 export_flags_3d_navigation => LAYERS_3D_NAVIGATION,
526 export_multiline => MULTILINE_TEXT,
527 export_color_no_alpha => COLOR_NO_ALPHA,
528 );
529}
530
531mod export_impls {
532 use super::*;
533 use crate::builtin::*;
534
535 macro_rules! impl_property_by_godot_convert {
536 ($Ty:ty, no_export) => {
537 impl_property_by_godot_convert!(@property $Ty);
538 };
539
540 ($Ty:ty) => {
541 impl_property_by_godot_convert!(@property $Ty);
542 impl_property_by_godot_convert!(@export $Ty);
543 impl_property_by_godot_convert!(@builtin $Ty);
544 };
545
546 (@property $Ty:ty) => {
547 impl Var for $Ty {
548 fn get_property(&self) -> Self::Via {
549 self.to_godot()
550 }
551
552 fn set_property(&mut self, value: Self::Via) {
553 *self = FromGodot::from_godot(value);
554 }
555 }
556 };
557
558 (@export $Ty:ty) => {
559 impl Export for $Ty {
560 fn export_hint() -> PropertyHintInfo {
561 PropertyHintInfo::type_name::<$Ty>()
562 }
563 }
564 };
565
566 (@builtin $Ty:ty) => {
567 impl BuiltinExport for $Ty {}
568 }
569 }
570
571 // Bounding Boxes
572 impl_property_by_godot_convert!(Aabb);
573 impl_property_by_godot_convert!(Rect2);
574 impl_property_by_godot_convert!(Rect2i);
575
576 // Matrices
577 impl_property_by_godot_convert!(Basis);
578 impl_property_by_godot_convert!(Transform2D);
579 impl_property_by_godot_convert!(Transform3D);
580 impl_property_by_godot_convert!(Projection);
581
582 // Vectors
583 impl_property_by_godot_convert!(Vector2);
584 impl_property_by_godot_convert!(Vector2i);
585 impl_property_by_godot_convert!(Vector3);
586 impl_property_by_godot_convert!(Vector3i);
587 impl_property_by_godot_convert!(Vector4);
588 impl_property_by_godot_convert!(Vector4i);
589
590 // Misc Math
591 impl_property_by_godot_convert!(Quaternion);
592 impl_property_by_godot_convert!(Plane);
593
594 // Stringy Types
595 impl_property_by_godot_convert!(GString);
596 impl_property_by_godot_convert!(StringName);
597 impl_property_by_godot_convert!(NodePath);
598
599 impl_property_by_godot_convert!(Color);
600
601 // Dictionary: will need to be done manually once they become typed.
602 impl_property_by_godot_convert!(Dictionary);
603 impl_property_by_godot_convert!(Variant);
604
605 // Packed arrays: we manually implement `Export`.
606 impl_property_by_godot_convert!(PackedByteArray, no_export);
607 impl_property_by_godot_convert!(PackedInt32Array, no_export);
608 impl_property_by_godot_convert!(PackedInt64Array, no_export);
609 impl_property_by_godot_convert!(PackedFloat32Array, no_export);
610 impl_property_by_godot_convert!(PackedFloat64Array, no_export);
611 impl_property_by_godot_convert!(PackedStringArray, no_export);
612 impl_property_by_godot_convert!(PackedVector2Array, no_export);
613 impl_property_by_godot_convert!(PackedVector3Array, no_export);
614 #[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
615 impl_property_by_godot_convert!(PackedVector4Array, no_export);
616 impl_property_by_godot_convert!(PackedColorArray, no_export);
617
618 // Primitives
619 impl_property_by_godot_convert!(f64);
620 impl_property_by_godot_convert!(i64);
621 impl_property_by_godot_convert!(bool);
622
623 // Godot uses f64 internally for floats, and if Godot tries to pass an invalid f32 into a rust property
624 // then the property will just round the value or become inf.
625 impl_property_by_godot_convert!(f32);
626
627 // Godot uses i64 internally for integers, and if Godot tries to pass an invalid integer into a property
628 // accepting one of the below values then rust will panic. In the editor this will appear as the property
629 // failing to be set to a value and an error printed in the console. During runtime this will crash the
630 // program and print the panic from rust stating that the property cannot store the value.
631 impl_property_by_godot_convert!(i32);
632 impl_property_by_godot_convert!(i16);
633 impl_property_by_godot_convert!(i8);
634 impl_property_by_godot_convert!(u32);
635 impl_property_by_godot_convert!(u16);
636 impl_property_by_godot_convert!(u8);
637
638 // Callables and Signals are useless when exported to the editor, so we only need to make them available as
639 // properties.
640 impl_property_by_godot_convert!(Callable, no_export);
641 impl_property_by_godot_convert!(Signal, no_export);
642
643 // RIDs when exported act slightly weird. They are largely read-only, however you can reset them to their
644 // default value. This seems to me very unintuitive. Since if we are storing an RID we would likely not
645 // want that RID to be spuriously resettable. And if used for debugging purposes we can use another
646 // mechanism than exporting the RID to the editor. Such as exporting a string containing the RID.
647 //
648 // Additionally, RIDs aren't persistent, and can sometimes behave a bit weirdly when passed from the
649 // editor to the runtime.
650 impl_property_by_godot_convert!(Rid, no_export);
651
652 // impl_property_by_godot_convert!(Signal);
653}
654
655// ----------------------------------------------------------------------------------------------------------------------------------------------
656// Crate-local utilities
657
658pub(crate) fn builtin_type_string<T: GodotType>() -> String {
659 use sys::GodotFfi as _;
660
661 let variant_type = T::Ffi::VARIANT_TYPE.variant_as_nil();
662
663 // Godot 4.3 changed representation for type hints, see https://github.com/godotengine/godot/pull/90716.
664 if sys::GdextBuild::since_api("4.3") {
665 format!("{}:", variant_type.ord())
666 } else {
667 format!("{}:{}", variant_type.ord(), T::godot_type_name())
668 }
669}
670
671/// Creates `hint_string` to be used for given `GodotClass` when used as an `ArrayElement`.
672pub(crate) fn object_export_element_type_string<T>(class_hint: impl Display) -> String
673where
674 T: GodotClass,
675{
676 let hint = if T::inherits::<classes::Resource>() {
677 Some(PropertyHint::RESOURCE_TYPE)
678 } else if T::inherits::<classes::Node>() {
679 Some(PropertyHint::NODE_TYPE)
680 } else {
681 None
682 };
683
684 // Exportable classes (Resource/Node based) include the {RESOURCE|NODE}_TYPE hint + the class name.
685 if let Some(export_hint) = hint {
686 format!(
687 "{variant}/{hint}:{class}",
688 variant = VariantType::OBJECT.ord(),
689 hint = export_hint.ord(),
690 class = class_hint
691 )
692 } else {
693 // Previous impl: format!("{variant}:", variant = VariantType::OBJECT.ord())
694 unreachable!("element_type_string() should only be invoked for exportable classes")
695 }
696}