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}