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