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}