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
240    /// Turn a list of variables into a comma separated string containing only the identifiers corresponding
241    /// to a true boolean variable.
242    macro_rules! comma_separate_boolean_idents {
243        ($( $ident:ident),* $(,)?) => {
244            {
245                let mut strings = Vec::new();
246
247                $(
248                    if $ident {
249                        strings.push(stringify!($ident));
250                    }
251                )*
252
253                strings.join(",")
254            }
255        };
256    }
257
258    // We want this to match the options available on `@export_range(..)`
259    /// Mark an exported numerical value to use the editor's range UI.
260    ///
261    /// You'll never call this function itself, but will instead use the macro `#[export(range=(...))]`, as below.  The syntax is
262    /// very similar to Godot's [`@export_range`](https://docs.godotengine.org/en/stable/classes/class_%40gdscript.html#class-gdscript-annotation-export-range).
263    /// `min`, `max`, and `step` are `f32` positional arguments, with `step` being optional and defaulting to `1.0`.  The rest of
264    /// 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"`.
265    ///
266    /// ```
267    /// # use godot::prelude::*;
268    /// #[derive(GodotClass)]
269    /// #[class(init, base=Node)]
270    /// struct MyClassWithRangedValues {
271    ///     #[export(range=(0.0, 400.0, 1.0, or_greater, suffix="px"))]
272    ///     icon_width: i32,
273    ///     #[export(range=(-180.0, 180.0, degrees))]
274    ///     angle: f32,
275    /// }
276    /// ```
277    #[allow(clippy::too_many_arguments)]
278    pub fn export_range(
279        min: f64,
280        max: f64,
281        step: Option<f64>,
282        or_greater: bool,
283        or_less: bool,
284        exp: bool,
285        radians_as_degrees: bool,
286        degrees: bool,
287        hide_slider: bool,
288        suffix: Option<String>,
289    ) -> PropertyHintInfo {
290        // From Godot 4.4, GDScript uses `.0` for integral floats, see https://github.com/godotengine/godot/pull/47502.
291        // We still register them the old way, to test compatibility. See also property_template_test.rs.
292
293        let hint_beginning = if let Some(step) = step {
294            format!("{min},{max},{step}")
295        } else {
296            format!("{min},{max}")
297        };
298
299        let rest = comma_separate_boolean_idents!(
300            or_greater,
301            or_less,
302            exp,
303            radians_as_degrees,
304            degrees,
305            hide_slider
306        );
307
308        let mut hint_string = hint_beginning;
309        if !rest.is_empty() {
310            hint_string.push_str(&format!(",{rest}"));
311        }
312        if let Some(suffix) = suffix {
313            hint_string.push_str(&format!(",suffix:{suffix}"));
314        }
315
316        PropertyHintInfo {
317            hint: PropertyHint::RANGE,
318            hint_string: GString::from(&hint_string),
319        }
320    }
321
322    #[doc(hidden)]
323    pub struct ExportValueWithKey<T> {
324        variant: String,
325        key: Option<T>,
326    }
327
328    impl<T: std::fmt::Display> ExportValueWithKey<T> {
329        fn as_hint_string(&self) -> String {
330            let Self { variant, key } = self;
331
332            match key {
333                Some(key) => format!("{variant}:{key}"),
334                None => variant.clone(),
335            }
336        }
337
338        fn slice_as_hint_string<V>(values: &[V]) -> String
339        where
340            for<'a> &'a V: Into<Self>,
341        {
342            let values = values
343                .iter()
344                .map(|v| v.into().as_hint_string())
345                .collect::<Vec<_>>();
346
347            values.join(",")
348        }
349    }
350
351    impl<T, S> From<&(S, Option<T>)> for ExportValueWithKey<T>
352    where
353        T: Clone,
354        S: AsRef<str>,
355    {
356        fn from((variant, key): &(S, Option<T>)) -> Self {
357            Self {
358                variant: variant.as_ref().into(),
359                key: key.clone(),
360            }
361        }
362    }
363
364    type EnumVariant = ExportValueWithKey<i64>;
365
366    /// Equivalent to `@export_enum` in Godot.
367    ///
368    /// A name without a key would be represented as `(name, None)`, and a name with a key as `(name, Some(key))`.
369    ///
370    /// # Examples
371    ///
372    /// ```no_run
373    /// # use godot::register::property::export_info_functions::export_enum;
374    /// export_enum(&[("a", None), ("b", Some(10))]);
375    /// ```
376    pub fn export_enum<T>(variants: &[T]) -> PropertyHintInfo
377    where
378        for<'a> &'a T: Into<EnumVariant>,
379    {
380        let hint_string: String = EnumVariant::slice_as_hint_string(variants);
381
382        PropertyHintInfo {
383            hint: PropertyHint::ENUM,
384            hint_string: GString::from(&hint_string),
385        }
386    }
387
388    pub fn export_exp_easing(attenuation: bool, positive_only: bool) -> PropertyHintInfo {
389        let hint_string = comma_separate_boolean_idents!(attenuation, positive_only);
390
391        PropertyHintInfo {
392            hint: PropertyHint::EXP_EASING,
393            hint_string: GString::from(&hint_string),
394        }
395    }
396
397    type BitFlag = ExportValueWithKey<u32>;
398
399    /// Equivalent to `@export_flags` in Godot.
400    ///
401    /// A flag without a key would be represented as `(flag, None)`, and a flag with a key as `(flag, Some(key))`.
402    ///
403    /// # Examples
404    ///
405    /// ```no_run
406    /// # use godot::register::property::export_info_functions::export_flags;
407    /// export_flags(&[("a", None), ("b", Some(10))]);
408    /// ```
409    pub fn export_flags<T>(bits: &[T]) -> PropertyHintInfo
410    where
411        for<'a> &'a T: Into<BitFlag>,
412    {
413        let hint_string = BitFlag::slice_as_hint_string(bits);
414
415        PropertyHintInfo {
416            hint: PropertyHint::FLAGS,
417            hint_string: GString::from(&hint_string),
418        }
419    }
420
421    /// Handles `@export_file`, `@export_global_file`, `@export_dir` and `@export_global_dir`.
422    pub fn export_file_or_dir<T: Export>(
423        is_file: bool,
424        is_global: bool,
425        filter: impl AsRef<str>,
426    ) -> PropertyHintInfo {
427        let field_ty = T::Via::property_info("");
428        let filter = filter.as_ref();
429        debug_assert!(is_file || filter.is_empty()); // Dir never has filter.
430
431        export_file_or_dir_inner(&field_ty, is_file, is_global, filter)
432    }
433
434    pub fn export_file_or_dir_inner(
435        field_ty: &PropertyInfo,
436        is_file: bool,
437        is_global: bool,
438        filter: &str,
439    ) -> PropertyHintInfo {
440        let hint = match (is_file, is_global) {
441            (true, true) => PropertyHint::GLOBAL_FILE,
442            (true, false) => PropertyHint::FILE,
443            (false, true) => PropertyHint::GLOBAL_DIR,
444            (false, false) => PropertyHint::DIR,
445        };
446
447        // Returned value depends on field type.
448        match field_ty.variant_type {
449            // GString field:
450            // { "type": 4, "hint": 13, "hint_string": "*.png" }
451            VariantType::STRING => PropertyHintInfo {
452                hint,
453                hint_string: GString::from(filter),
454            },
455
456            // Array<GString> or PackedStringArray field:
457            // { "type": 28, "hint": 23, "hint_string": "4/13:*.png" }
458            #[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
459            VariantType::PACKED_STRING_ARRAY => to_string_array_hint(hint, filter),
460            #[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
461            VariantType::ARRAY if field_ty.is_array_of_elem::<GString>() => {
462                to_string_array_hint(hint, filter)
463            }
464
465            _ => {
466                // E.g. `global_file`.
467                let attribute_name = hint.as_str().to_lowercase();
468
469                // TODO nicer error handling.
470                // Compile time may be difficult (at least without extra traits... maybe const fn?). But at least more context info, field name etc.
471                #[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
472                panic!(
473                    "#[export({attribute_name})] only supports GString, Array<String> or PackedStringArray field types\n\
474                    encountered: {field_ty:?}"
475                );
476
477                #[cfg(before_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(before_api = "4.3")))]
478                panic!(
479                    "#[export({attribute_name})] only supports GString type prior to Godot 4.3\n\
480                    encountered: {field_ty:?}"
481                );
482            }
483        }
484    }
485
486    /// For `Array<GString>` and `PackedStringArray` fields using one of the `@export[_global]_{file|dir}` annotations.
487    ///
488    /// Formats: `"4/13:"`, `"4/15:*.png"`, ...
489    fn to_string_array_hint(hint: PropertyHint, filter: &str) -> PropertyHintInfo {
490        let variant_ord = VariantType::STRING.ord(); // "4"
491        let hint_ord = hint.ord();
492        let hint_string = format!("{variant_ord}/{hint_ord}");
493
494        PropertyHintInfo {
495            hint: PropertyHint::TYPE_STRING,
496            hint_string: GString::from(&format!("{hint_string}:{filter}")),
497        }
498    }
499
500    pub fn export_placeholder<S: AsRef<str>>(placeholder: S) -> PropertyHintInfo {
501        PropertyHintInfo {
502            hint: PropertyHint::PLACEHOLDER_TEXT,
503            hint_string: GString::from(placeholder.as_ref()),
504        }
505    }
506
507    macro_rules! default_export_funcs {
508        (
509            $( $function_name:ident => $property_hint:ident, )*
510        ) => {
511            $(
512                pub fn $function_name() -> PropertyHintInfo {
513                    PropertyHintInfo {
514                        hint: PropertyHint::$property_hint,
515                        hint_string: GString::new()
516                    }
517                }
518            )*
519        };
520    }
521
522    // The left side of these declarations follows the export annotation provided by GDScript, whereas the
523    // right side are the corresponding property hint. Godot is not always consistent between the two, such
524    // as `export_multiline` being `PROPERTY_HINT_MULTILINE_TEXT`.
525    default_export_funcs!(
526        export_storage => NONE, // Storage exports don't display in the editor.
527        export_flags_2d_physics => LAYERS_2D_PHYSICS,
528        export_flags_2d_render => LAYERS_2D_RENDER,
529        export_flags_2d_navigation => LAYERS_2D_NAVIGATION,
530        export_flags_3d_physics => LAYERS_3D_PHYSICS,
531        export_flags_3d_render => LAYERS_3D_RENDER,
532        export_flags_3d_navigation => LAYERS_3D_NAVIGATION,
533        export_multiline => MULTILINE_TEXT,
534        export_color_no_alpha => COLOR_NO_ALPHA,
535    );
536}
537
538mod export_impls {
539    use super::*;
540    use crate::builtin::*;
541
542    macro_rules! impl_property_by_godot_convert {
543        ($Ty:ty, no_export) => {
544            impl_property_by_godot_convert!(@property $Ty);
545        };
546
547        ($Ty:ty) => {
548            impl_property_by_godot_convert!(@property $Ty);
549            impl_property_by_godot_convert!(@export $Ty);
550            impl_property_by_godot_convert!(@builtin $Ty);
551        };
552
553        (@property $Ty:ty) => {
554            impl Var for $Ty {
555                fn get_property(&self) -> Self::Via {
556                    self.to_godot_owned()
557                }
558
559                fn set_property(&mut self, value: Self::Via) {
560                    *self = FromGodot::from_godot(value);
561                }
562            }
563        };
564
565        (@export $Ty:ty) => {
566            impl Export for $Ty {
567                fn export_hint() -> PropertyHintInfo {
568                    PropertyHintInfo::type_name::<$Ty>()
569                }
570            }
571        };
572
573        (@builtin $Ty:ty) => {
574            impl BuiltinExport for $Ty {}
575        }
576    }
577
578    // Bounding boxes
579    impl_property_by_godot_convert!(Aabb);
580    impl_property_by_godot_convert!(Rect2);
581    impl_property_by_godot_convert!(Rect2i);
582
583    // Matrices
584    impl_property_by_godot_convert!(Basis);
585    impl_property_by_godot_convert!(Transform2D);
586    impl_property_by_godot_convert!(Transform3D);
587    impl_property_by_godot_convert!(Projection);
588
589    // Vectors
590    impl_property_by_godot_convert!(Vector2);
591    impl_property_by_godot_convert!(Vector2i);
592    impl_property_by_godot_convert!(Vector3);
593    impl_property_by_godot_convert!(Vector3i);
594    impl_property_by_godot_convert!(Vector4);
595    impl_property_by_godot_convert!(Vector4i);
596
597    // Misc math
598    impl_property_by_godot_convert!(Quaternion);
599    impl_property_by_godot_convert!(Plane);
600
601    // Stringy types
602    impl_property_by_godot_convert!(GString);
603    impl_property_by_godot_convert!(StringName);
604    impl_property_by_godot_convert!(NodePath);
605
606    impl_property_by_godot_convert!(Color);
607
608    // Dictionary: will need to be done manually once they become typed.
609    impl_property_by_godot_convert!(Dictionary);
610    impl_property_by_godot_convert!(Variant);
611
612    // Primitives
613    impl_property_by_godot_convert!(f64);
614    impl_property_by_godot_convert!(i64);
615    impl_property_by_godot_convert!(bool);
616
617    // Godot uses f64 internally for floats, and if Godot tries to pass an invalid f32 into a rust property
618    // then the property will just round the value or become inf.
619    impl_property_by_godot_convert!(f32);
620
621    // Godot uses i64 internally for integers, and if Godot tries to pass an invalid integer into a property
622    // accepting one of the below values then rust will panic. In the editor this will appear as the property
623    // failing to be set to a value and an error printed in the console. During runtime this will crash the
624    // program and print the panic from rust stating that the property cannot store the value.
625    impl_property_by_godot_convert!(i32);
626    impl_property_by_godot_convert!(i16);
627    impl_property_by_godot_convert!(i8);
628    impl_property_by_godot_convert!(u32);
629    impl_property_by_godot_convert!(u16);
630    impl_property_by_godot_convert!(u8);
631
632    // Callables and Signals are useless when exported to the editor, so we only need to make them available as
633    // properties.
634    impl_property_by_godot_convert!(Callable, no_export);
635    impl_property_by_godot_convert!(Signal, no_export);
636
637    // RIDs when exported act slightly weird. They are largely read-only, however you can reset them to their
638    // default value. This seems to me very unintuitive. Since if we are storing an RID we would likely not
639    // want that RID to be spuriously resettable. And if used for debugging purposes we can use another
640    // mechanism than exporting the RID to the editor. Such as exporting a string containing the RID.
641    //
642    // Additionally, RIDs aren't persistent, and can sometimes behave a bit weirdly when passed from the
643    // editor to the runtime.
644    impl_property_by_godot_convert!(Rid, no_export);
645
646    // Var/Export for Array<T> and PackedArray<T> are implemented in the files of their struct declaration.
647
648    // impl_property_by_godot_convert!(Signal);
649}
650
651// ----------------------------------------------------------------------------------------------------------------------------------------------
652// Crate-local utilities
653
654pub(crate) fn builtin_type_string<T: GodotType>() -> String {
655    use sys::GodotFfi as _;
656
657    let variant_type = T::Ffi::VARIANT_TYPE.variant_as_nil();
658
659    // Godot 4.3 changed representation for type hints, see https://github.com/godotengine/godot/pull/90716.
660    if sys::GdextBuild::since_api("4.3") {
661        format!("{}:", variant_type.ord())
662    } else {
663        format!("{}:{}", variant_type.ord(), T::godot_type_name())
664    }
665}
666
667/// Creates `hint_string` to be used for given `GodotClass` when used as an `ArrayElement`.
668pub(crate) fn object_export_element_type_string<T>(class_hint: impl Display) -> String
669where
670    T: GodotClass,
671{
672    let hint = if T::inherits::<classes::Resource>() {
673        Some(PropertyHint::RESOURCE_TYPE)
674    } else if T::inherits::<classes::Node>() {
675        Some(PropertyHint::NODE_TYPE)
676    } else {
677        None
678    };
679
680    // Exportable classes (Resource/Node based) include the {RESOURCE|NODE}_TYPE hint + the class name.
681    if let Some(export_hint) = hint {
682        format!(
683            "{variant}/{hint}:{class}",
684            variant = VariantType::OBJECT.ord(),
685            hint = export_hint.ord(),
686            class = class_hint
687        )
688    } else {
689        // Previous impl: format!("{variant}:", variant = VariantType::OBJECT.ord())
690        unreachable!("element_type_string() should only be invoked for exportable classes")
691    }
692}