Skip to main content

godot_core/meta/
shape.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//! Static type descriptors for Rust types towards Godot.
9//!
10//! The symbols in this module and primarily [`GodotShape`] are used to describe the _shape_ of a Rust type, which combines information about:
11//! - To which Godot type it maps.
12//! - Metadata relevant for properties (var/export).
13//! - Metadata relevant for method signatures (parameters and return values).
14//!
15//! These shapes are then transformed to lower-level GDExtension descriptors, located in [`register::info`][crate::registry::info].
16//! Godot then accepts those for registration.
17
18use std::borrow::Cow;
19use std::fmt::Display;
20
21use godot_ffi as sys;
22
23use crate::builtin::{CowStr, GString, StringName, VariantType};
24use crate::global::godot_str;
25use crate::meta::{ClassId, GodotType};
26use crate::obj::EngineEnum as _;
27use crate::registry::info::{
28    ParamMetadata, PropertyHint, PropertyHintInfo, PropertyInfo, PropertyUsageFlags,
29};
30
31/// The "shape" of a Godot type: whether it's a builtin, a class, an enum/bitfield, etc.
32///
33/// Describes a _static_ (compile-time) type as it should be registered with Godot; returned by [`GodotConvert::godot_shape()`].
34/// This is distinct from runtime introspection APIs such [`AnyArray::element_type()`].
35///
36/// Usually you need to deal with `GodotShape` only if you define custom types through manual `GodotConvert` impls.
37///
38/// # Information provided by the shape
39/// A shape description is used for three purposes:
40/// - Property registrations (`#[var]`) so that Godot has static type information of your type.
41///   - See [`to_var_property()`] and [`var_hint()`].
42/// - Exported properties (`#[export]`) so that properties show up correctly in the editor's inspector UI.
43///   - See [`to_export_property()`] and [`export_hint()`].
44/// - Method signatures (`#[func]`), so that Godot has the static type information for parameters and return values.
45///   - See [`to_method_signature_property()`].
46///
47/// # Property registration
48/// During registration of class properties, the runtime resolves hints and usage flags from the shape:
49///
50/// - For `#[var]`, it calls [`var_hint()`] and uses `NONE` as base usage.
51/// - For `#[export]`, it calls [`export_hint()`] and uses `DEFAULT` as base usage.
52/// - If the user specifies explicit overrides (e.g. `#[var(hint = ...)]` or `#[export(range = ...)]`), those replace hints from the shape.
53///
54/// The shape also contributes structural metadata -- variant type, class name, and additional usage flags (via
55/// [`usage_flags()`]). These are combined with the hint and base usage into a [`PropertyInfo`] for the Godot FFI call.
56///
57/// [`PropertyInfo`]: PropertyInfo
58/// [`GodotConvert::godot_shape()`]: crate::meta::GodotConvert::godot_shape
59/// [`AnyArray::element_type()`]: crate::builtin::AnyArray::element_type
60/// [`to_var_property()`]: Self::to_var_property
61/// [`to_export_property()`]: Self::to_export_property
62/// [`to_method_signature_property()`]: Self::to_method_signature_property
63/// [`var_hint()`]: Self::var_hint
64/// [`export_hint()`]: Self::export_hint
65/// [`usage_flags()`]: Self::usage_flags
66#[non_exhaustive]
67#[derive(Clone, Debug)]
68pub enum GodotShape {
69    /// The general [`Variant`][crate::builtin::Variant] type. Can hold any Godot value.
70    ///
71    /// Distinct from `Builtin { variant_type: NIL }`, which represents the `()` unit type (`void` in GDScript).
72    Variant,
73
74    /// A built-in Godot type (`int`, `String`, `Vector3`, `PackedByteArray`, etc.).
75    ///
76    /// The variant type used here must match the one from [`GodotConvert::Via`][crate::meta::GodotConvert::Via].
77    ///
78    /// Packed arrays, untyped arrays and untyped dictionaries are also represented as `Builtin`.  \
79    /// Typed arrays, typed dictionaries, objects and variants have their own shape representation.
80    Builtin {
81        /// Godot variant type (e.g. `INT`, `FLOAT`, `STRING`, `VECTOR3`, `PACKED_BYTE_ARRAY`). Never `OBJECT`.
82        variant_type: VariantType,
83
84        /// Numeric precision metadata for method parameters/return values.
85        ///
86        /// Distinguishes sub-types like `i8` from `i64` even though both map to Godot's `INT`.
87        metadata: ParamMetadata,
88    },
89
90    /// A Godot object type (`Gd<T>`, `DynGd<T, D>`, `Option<Gd<T>>`, `OnReady<Gd<T>>`, etc.).
91    ///
92    /// Always has `VariantType::OBJECT`.
93    Class {
94        /// The Godot class of this object type (e.g. `Node`, `Resource`).
95        class_id: ClassId,
96
97        /// Whether this inherits from `Resource`, `Node`, or other object class.
98        heritage: ClassHeritage,
99
100        /// Whether the object can be null (e.g. `Option<Gd<T>>`).
101        ///
102        /// [`param_metadata()`][Self::param_metadata] returns:
103        /// - `NONE` if the object is nullable,
104        /// - `OBJECT_IS_REQUIRED` otherwise.
105        is_nullable: bool,
106    },
107
108    /// An [`Array<T>`][crate::builtin::Array] where `T` is not `Variant`.
109    ///
110    /// Untyped arrays are represented as `Builtin { variant_type: VariantType::ARRAY }`.
111    TypedArray {
112        /// Shape of the array element type.
113        element: GodotElementShape,
114    },
115
116    /// A [`Dictionary<K, V>`][crate::builtin::Dictionary] where at least one of `K`, `V` is not `Variant` (Godot 4.4+).
117    ///
118    /// Untyped dictionaries are represented as `Builtin { variant_type: VariantType::DICTIONARY }`.
119    TypedDictionary {
120        /// Shape of the dictionary key type.
121        key: GodotElementShape,
122
123        /// Shape of the dictionary value type.
124        value: GodotElementShape,
125    },
126
127    /// An enum or bitfield type (engine-defined or user-defined).
128    Enum {
129        /// Godot variant type of the underlying representation (typically `INT` for int-backed enums, `STRING` for string-backed).
130        variant_type: VariantType,
131
132        /// Display name and ordinal for each enumerator. `Borrowed` for compile-time data, `Owned` for dynamic enumerators.
133        enumerators: Cow<'static, [EnumeratorShape]>,
134
135        /// Godot-qualified enum name. `Some("Orientation")` or `Some("Node.ProcessMode")` for engine enums; `None` for user enums.
136        ///
137        /// When `Some`, the framework sets `class_name` + `CLASS_IS_ENUM` in `PropertyInfo` and uses `NONE` hint for `#[var]`
138        /// (Godot resolves the enum from class_name). When `None`, uses `ENUM`/`FLAGS` hint with hint_string directly.
139        ///
140        // TODO(v0.6): In future, user enums could set this to register constants with Godot via `classdb_register_extension_class_integer_constant`,
141        // enabling GDScript to reference them by name. The `is_bitfield` field maps to that FFI method's `p_is_bitfield` parameter.
142        // Decide if we should even *require* enums to be associated with Godot classes or globally -- meaning this would become non-optional.
143        // There would need to be a differentiator for "inside a class" or "global" (but still registered with Godot).
144        godot_name: Option<CowStr>,
145
146        /// Whether this is a bitfield:
147        /// * `true` for bitfields (`FLAGS` hint, `CLASS_IS_BITFIELD` usage).
148        /// * `false` for regular enums (`ENUM` hint, `CLASS_IS_ENUM` usage).
149        is_bitfield: bool,
150    },
151
152    /// Fully custom property metadata. Use only when the type doesn't fit any predefined shape
153    Custom {
154        /// Godot variant type.
155        variant_type: VariantType,
156
157        /// Property hint info for `#[var]` context.
158        var_hint: PropertyHintInfo,
159
160        /// Property hint info for `#[export]` context.
161        export_hint: PropertyHintInfo,
162
163        /// Stored as `CowStr`; converted to `ClassId` only at registration time to avoid eager global cache allocation.
164        class_name: Option<CowStr>,
165
166        /// Additional usage flags.
167        ///
168        /// These are bit-ORed with the base usage, see [`to_var_property()`][Self::to_var_property] and
169        /// [`to_export_property()`][Self::to_export_property].
170        ///
171        /// Typically you can use `NONE` if the shape doesn't need extra flags.
172        usage_flags: PropertyUsageFlags,
173
174        /// Numeric precision metadata for method parameters/return values.
175        metadata: ParamMetadata,
176    },
177}
178
179impl GodotShape {
180    /// Creates `GodotShape::Builtin` for a type `T` directly representable as a Godot builtin, including packed arrays.
181    ///
182    /// Returns either of:
183    /// * Variant, if `T` is `Variant`.
184    /// * `GodotShape::Builtin { variant_type: ffi_variant_type::<T>().variant_as_nil() }` for other builtins.
185    ///
186    /// Do not use for objects, typed arrays/dictionaries or enums; those have their own shape variants.
187    pub fn of_builtin<T: GodotType>() -> Self {
188        match crate::meta::ffi_variant_type::<T>() {
189            sys::ExtVariantType::Variant => Self::Variant,
190            ext => Self::Builtin {
191                variant_type: ext.variant_as_nil(),
192                metadata: T::default_metadata(),
193            },
194        }
195    }
196
197    /// Returns the parameter metadata for this shape, used in method signature registration.
198    ///
199    /// - `Builtin` and `Custom`: returns the stored `metadata` field.
200    /// - `Class` with `is_nullable: false`: returns [`ParamMetadata::OBJECT_IS_REQUIRED`].
201    /// - All other shapes: returns [`ParamMetadata::NONE`].
202    pub fn param_metadata(&self) -> ParamMetadata {
203        match self {
204            Self::Builtin { metadata, .. } | Self::Custom { metadata, .. } => *metadata,
205
206            Self::Class {
207                is_nullable: false, ..
208            } => ParamMetadata::OBJECT_IS_REQUIRED,
209
210            _ => ParamMetadata::NONE,
211        }
212    }
213
214    /// Returns the Godot `VariantType` for this shape.
215    pub fn variant_type(&self) -> VariantType {
216        match self {
217            Self::Variant => VariantType::NIL,
218            Self::Builtin { variant_type, .. } => *variant_type,
219            Self::Class { .. } => VariantType::OBJECT,
220            Self::Enum { variant_type, .. } => *variant_type,
221            Self::Custom { variant_type, .. } => *variant_type,
222            Self::TypedArray { .. } => VariantType::ARRAY,
223            Self::TypedDictionary { .. } => VariantType::DICTIONARY,
224        }
225    }
226
227    /// Property hint for `#[var]` context.
228    pub fn var_hint(&self) -> PropertyHintInfo {
229        match self {
230            Self::Variant | Self::Builtin { .. } | Self::Class { .. } => PropertyHintInfo::none(),
231            Self::Enum {
232                godot_name,
233                enumerators,
234                is_bitfield,
235                ..
236            } => enum_hint_info(enumerators, *is_bitfield, godot_name.is_some()),
237
238            Self::Custom { var_hint, .. } => var_hint.clone(),
239
240            Self::TypedArray {
241                element: element_shape,
242            } => PropertyHintInfo {
243                hint: PropertyHint::ARRAY_TYPE,
244                hint_string: GString::from(&element_shape.element_godot_type_name()),
245            },
246
247            Self::TypedDictionary {
248                key: key_shape,
249                value: value_shape,
250            } => {
251                // PropertyHint::DICTIONARY_TYPE, only available since Godot 4.4 -- so the `if` is essentially a version check.
252                if let Some(hint) = PropertyHint::try_from_ord(38) {
253                    PropertyHintInfo {
254                        hint,
255                        hint_string: godot_str!(
256                            "{};{}",
257                            key_shape.element_godot_type_name(),
258                            value_shape.element_godot_type_name()
259                        ),
260                    }
261                } else {
262                    let _unused = (key_shape, value_shape);
263                    PropertyHintInfo::none()
264                }
265            }
266        }
267    }
268
269    /// Property hint for `#[export]` context.
270    ///
271    /// For enums, always uses `ENUM`/`FLAGS` + hint_string (even engine enums need explicit hints for export).
272    pub fn export_hint(&self) -> PropertyHintInfo {
273        match self {
274            Self::Variant => PropertyHintInfo::none(),
275
276            Self::Builtin { variant_type, .. } => {
277                // In 4.3+, packed arrays use a TYPE_STRING hint with their element type.
278                // See https://github.com/godotengine/godot/pull/82952.
279                if sys::GdextBuild::since_api("4.3") {
280                    if let Some(elem_vtype) = packed_element_variant_type(*variant_type) {
281                        return PropertyHintInfo {
282                            hint: PropertyHint::TYPE_STRING,
283                            hint_string: GString::from(&format_elements_untyped(elem_vtype)),
284                        };
285                    }
286                    PropertyHintInfo::none()
287                } else {
288                    // Pre-4.3 Godot uses the type name in hint_string even with NONE hint.
289                    PropertyHintInfo {
290                        hint: PropertyHint::NONE,
291                        hint_string: GString::from(variant_type.godot_type_name()),
292                    }
293                }
294            }
295
296            Self::Class {
297                class_id, heritage, ..
298            } => match heritage {
299                ClassHeritage::Node => PropertyHintInfo {
300                    hint: PropertyHint::NODE_TYPE,
301                    hint_string: class_id.to_gstring(),
302                },
303                ClassHeritage::Resource => PropertyHintInfo {
304                    hint: PropertyHint::RESOURCE_TYPE,
305                    hint_string: class_id.to_gstring(),
306                },
307                ClassHeritage::DynResource { implementors } => PropertyHintInfo {
308                    hint: PropertyHint::RESOURCE_TYPE,
309                    hint_string: GString::from(&join_class_ids(implementors)),
310                },
311                ClassHeritage::Other => PropertyHintInfo::none(),
312            },
313
314            Self::TypedArray {
315                element: element_shape,
316            } => PropertyHintInfo {
317                hint: PropertyHint::TYPE_STRING,
318                hint_string: GString::from(&element_shape.element_type_string()),
319            },
320
321            Self::TypedDictionary {
322                key: key_shape,
323                value: value_shape,
324            } => {
325                if sys::GdextBuild::since_api("4.4") {
326                    PropertyHintInfo {
327                        hint: PropertyHint::TYPE_STRING,
328                        hint_string: godot_str!(
329                            "{};{}",
330                            key_shape.element_type_string(),
331                            value_shape.element_type_string()
332                        ),
333                    }
334                } else {
335                    PropertyHintInfo::none()
336                }
337            }
338
339            Self::Enum {
340                enumerators,
341                is_bitfield,
342                ..
343            } => enum_hint_info(enumerators, *is_bitfield, false),
344
345            Self::Custom { export_hint, .. } => export_hint.clone(),
346        }
347    }
348
349    /// Converts `godot_name`/`class_name` to a `StringName`. Only called during registration.
350    ///
351    /// For engine enums, returns the enum's qualified name (e.g. `"Node.ProcessMode"`).
352    /// For classes, returns the class name.
353    /// For other types, returns an empty `StringName`.
354    pub(crate) fn class_name_or_none(&self) -> StringName {
355        match self {
356            Self::Variant
357            | Self::Builtin { .. }
358            | Self::TypedArray { .. }
359            | Self::TypedDictionary { .. } => StringName::default(),
360            Self::Class { class_id, .. } => class_id.to_string_name(),
361            Self::Enum { godot_name, .. } => match godot_name {
362                Some(name) => StringName::from(name.as_ref()),
363                None => StringName::default(),
364            },
365            Self::Custom { class_name, .. } => match class_name {
366                Some(name) => StringName::from(name.as_ref()),
367                None => StringName::default(),
368            },
369        }
370    }
371
372    /// Shape-specific usage flags for property registration.
373    ///
374    /// These are combined with the base usage, which is `NONE` for `#[var]` and `DEFAULT` for `#[export]`.
375    // Only engine enums (those with `godot_name`) get `CLASS_IS_ENUM`. User enums don't set this flag: `CLASS_IS_ENUM` tells Godot to resolve
376    // the enum's enumerators via `class_name` in ClassDB -- but user enums aren't registered there yet. Setting the flag without a valid
377    // `class_name` would cause Godot to look up a nonexistent name. Once we call `classdb_register_extension_class_integer_constant` for user
378    // enums (making them visible to GDScript by name), they can set `godot_name` and get `CLASS_IS_ENUM` automatically.
379    pub fn usage_flags(&self) -> PropertyUsageFlags {
380        match self {
381            Self::Variant => PropertyUsageFlags::NIL_IS_VARIANT,
382
383            Self::Builtin { .. }
384            | Self::Class { .. }
385            | Self::TypedArray { .. }
386            | Self::TypedDictionary { .. } => PropertyUsageFlags::NONE,
387
388            Self::Enum {
389                godot_name,
390                is_bitfield,
391                ..
392            } => match (godot_name, *is_bitfield) {
393                (Some(_), true) => PropertyUsageFlags::CLASS_IS_BITFIELD,
394                (Some(_), false) => PropertyUsageFlags::CLASS_IS_ENUM,
395                (None, _) => PropertyUsageFlags::NONE, // User enums are currently not yet registered.
396            },
397
398            Self::Custom { usage_flags, .. } => *usage_flags,
399        }
400    }
401
402    /// Builds the low-level Godot property info for method parameter/return type registration.
403    ///
404    /// Uses:
405    /// - Hint and hint string: [`var_hint()`][Self::var_hint].
406    /// - Usage: [`DEFAULT`][PropertyUsageFlags::DEFAULT] (required for method params; not shown in editor).
407    pub fn to_method_signature_property(&self, property_name: &str) -> PropertyInfo {
408        let hint_info = self.var_hint();
409        self.to_property(property_name, hint_info, PropertyUsageFlags::DEFAULT)
410    }
411
412    /// Builds the low-level Godot property info for `#[var]` context.
413    ///
414    /// Uses:
415    /// - Hint and hint string: [`var_hint()`][Self::var_hint].
416    /// - Base usage: [`NONE`][PropertyUsageFlags::NONE], combined with specific [`usage_flags()`][Self::usage_flags] from this shape.
417    ///   Property is accessible from GDScript, but not shown in editor or saved.
418    ///
419    /// See also [`PropertyInfo::new_var()`].
420    pub fn to_var_property(&self, property_name: &str) -> PropertyInfo {
421        let hint_info = self.var_hint();
422        self.to_property(property_name, hint_info, PropertyUsageFlags::NONE)
423    }
424
425    /// Builds the low-level Godot property info for `#[export]` context.
426    ///
427    /// Uses:
428    /// - Hint and hint string: [`export_hint()`][Self::export_hint].
429    /// - Usage: [`DEFAULT`][PropertyUsageFlags::DEFAULT], combined with specific [`usage_flags()`][Self::usage_flags] from this shape.
430    ///   Property is shown in editor and saved.
431    ///
432    /// See also [`PropertyInfo::new_export()`].
433    pub fn to_export_property(&self, property_name: &str) -> PropertyInfo {
434        let hint_info = self.export_hint();
435        self.to_property(property_name, hint_info, PropertyUsageFlags::DEFAULT)
436    }
437
438    /// Low-level builder for [`PropertyInfo`]. Derives `class_name`, `variant_type`, and shape-specific usage flags from
439    /// `self`, but takes `hint_info` and `base_usage` as parameters because they depend on context (`#[var]` vs
440    /// `#[export]`) and may be overridden by the user (e.g. `#[var(hint = ..., hint_string = "...")]`).
441    ///
442    /// Prefer [`to_var_property()`](Self::to_var_property) or [`to_export_property()`](Self::to_export_property)
443    /// when no user override is involved.
444    fn to_property(
445        &self,
446        property_name: &str,
447        hint_info: PropertyHintInfo,
448        base_usage: PropertyUsageFlags,
449    ) -> PropertyInfo {
450        use crate::builtin::StringName;
451
452        let class_name = self.class_name_or_none();
453        let variant_type = self.variant_type();
454        let usage = base_usage | self.usage_flags();
455
456        PropertyInfo {
457            variant_type,
458            class_name,
459            property_name: StringName::from(property_name),
460            hint_info,
461            usage,
462        }
463    }
464}
465
466// ----------------------------------------------------------------------------------------------------------------------------------------------
467// ClassAncestor
468
469/// Which tree in the Godot hierarchy a class belongs to; determines how it appears in property hints.
470///
471/// Used inside [`GodotShape::Class`].
472#[derive(Clone, Debug)]
473pub enum ClassHeritage {
474    /// A class inheriting from [`Node`][crate::classes::Node] (uses `NODE_TYPE` hint for `#[export]`).
475    Node,
476
477    /// A class inheriting from [`Resource`][crate::classes::Resource] (uses `RESOURCE_TYPE` hint for `#[export]`).
478    Resource,
479
480    /// A `DynGd<T, D>` where `T` inherits `Resource`. Stores the concrete implementor `ClassId`s from the `#[godot_dyn]` registry.
481    DynResource {
482        /// Class IDs of all concrete implementors registered via `#[godot_dyn]` or [`AsDyn`][crate::obj::AsDyn] for the trait.
483        implementors: Vec<ClassId>,
484    },
485
486    /// Any other class that doesn't inherit from `Node` or `Resource`. No special hint for `#[export]`.
487    Other,
488}
489
490impl ClassHeritage {
491    /// Returns the `PropertyHint` for `#[export]` context.
492    pub fn export_property_hint(&self) -> PropertyHint {
493        match self {
494            Self::Resource | Self::DynResource { .. } => PropertyHint::RESOURCE_TYPE,
495            Self::Node => PropertyHint::NODE_TYPE,
496            Self::Other => PropertyHint::NONE,
497        }
498    }
499}
500
501// ----------------------------------------------------------------------------------------------------------------------------------------------
502// Inner/nested shape
503
504/// Same as [`GodotShape`], but for element types nested in typed arrays/dictionaries.
505///
506/// Matches the same layout as `GodotShape`, exists to avoid recursive definition (and also `Box` allocations). Also constrains the possible
507/// shapes (elements cannot be typed arrays/dictionaries themselves).
508///
509/// In contrast to [`ElementType`][crate::meta::inspect::ElementType], this is a _static_ type description for Godot registration purposes.
510///
511/// Use [`into_outer()`][Self::into_outer] to convert into a full `GodotShape`.
512#[non_exhaustive]
513#[derive(Clone, Debug)]
514pub enum GodotElementShape {
515    // Inner types are not structs like Builtin(BuiltinShape), to avoid type proliferation in niche APIs. to_outer() is good enough.
516    Variant,
517
518    Builtin {
519        variant_type: VariantType,
520    },
521
522    Class {
523        class_id: ClassId,
524        heritage: ClassHeritage,
525    },
526
527    Enum {
528        variant_type: VariantType,
529        enumerators: Cow<'static, [EnumeratorShape]>,
530        godot_name: Option<CowStr>,
531        is_bitfield: bool,
532    },
533
534    Custom {
535        variant_type: VariantType,
536        var_hint: PropertyHintInfo,
537        export_hint: PropertyHintInfo,
538        class_name: Option<CowStr>,
539        usage_flags: PropertyUsageFlags,
540    },
541}
542
543impl GodotElementShape {
544    #[rustfmt::skip]
545    pub(crate) fn new(outer: GodotShape) -> Self {
546        type GShape = GodotShape;
547
548        match outer {
549             GShape::Variant
550            => Self::Variant,
551
552            // Strip metadata: element shapes don't carry param metadata.
553            GShape::Builtin { variant_type, .. }
554            => Self::Builtin { variant_type },
555
556            // Strip is_nullable: element shapes don't carry object-nullability metadata.
557             GShape::Class { class_id, heritage, .. }
558            => Self::Class { class_id, heritage },
559
560             GShape::Enum { variant_type, enumerators, godot_name, is_bitfield }
561            => Self::Enum { variant_type, enumerators, godot_name, is_bitfield },
562
563            // Strip metadata.
564             GShape::Custom { variant_type, var_hint, export_hint, class_name, usage_flags, .. }
565            => Self::Custom { variant_type, var_hint, export_hint, class_name, usage_flags },
566
567            GShape::TypedArray { .. } |
568            GShape::TypedDictionary { .. } => panic!("nested shapes cannot be typed arrays/dictionaries")
569        }
570    }
571
572    /// Converts this nested shape into a full `GodotShape`. Infallible.
573    #[rustfmt::skip]
574    pub fn into_outer(self) -> GodotShape {
575        type G = GodotShape;
576
577        match self {
578            Self::Variant
579            => G::Variant,
580
581            // Fill default metadata on conversion out.
582            Self::Builtin { variant_type }
583            => G::Builtin { variant_type, metadata: ParamMetadata::NONE },
584
585            // Fill is_nullable: true (nullable by default for element shapes).
586            Self::Class { class_id, heritage }
587            => G::Class { class_id, heritage, is_nullable: true },
588
589            Self::Enum { variant_type, enumerators, godot_name, is_bitfield }
590            => G::Enum { variant_type, enumerators, godot_name, is_bitfield },
591
592            // Fill NONE metadata (arrays/dictionaries don't store param metadata).
593            Self::Custom { variant_type, var_hint, export_hint, class_name, usage_flags}
594            => G::Custom { variant_type, var_hint, export_hint, class_name, usage_flags, metadata: ParamMetadata::NONE },
595        }
596    }
597
598    /// Returns the Godot type name for use in `#[var]` array/dictionary type hints.
599    ///
600    /// Defaults to the `Via` type's name (e.g. `"int"` for `i32`). Engine enums override this to return their qualified class name
601    /// (e.g. `"Node.ProcessMode"`).
602    pub(crate) fn element_godot_type_name(&self) -> String {
603        match self {
604            Self::Variant => VariantType::NIL.godot_type_name().to_string(),
605            Self::Builtin { variant_type } => variant_type.godot_type_name().to_string(),
606            Self::Class { class_id, .. } => class_id.to_cow_str().to_string(),
607            Self::Enum {
608                godot_name,
609                variant_type,
610                ..
611            } => match godot_name {
612                Some(name) => name.to_string(),
613                None => variant_type.godot_type_name().to_string(),
614            },
615            Self::Custom { variant_type, .. } => variant_type.godot_type_name().to_string(),
616        }
617    }
618
619    /// Returns the representation of this type as an element inside an array, e.g. `"4:"` for string, or `"24:34/MyClass"` for objects.
620    ///
621    /// `4` and `24` are variant type ords; `34` is `PropertyHint::NODE_TYPE` ord.
622    ///
623    /// See [`PropertyHint::TYPE_STRING`] and
624    /// [upstream docs](https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#enum-globalscope-propertyhint).
625    pub(crate) fn element_type_string(&self) -> String {
626        match self {
627            Self::Variant
628            | Self::Builtin {
629                variant_type: VariantType::NIL,
630            } => {
631                // Variant (or void) as element: untyped, no hint.
632                format_elements_untyped(VariantType::NIL)
633            }
634
635            Self::Builtin { variant_type } => {
636                if sys::GdextBuild::since_api("4.3") {
637                    format_elements_untyped(*variant_type)
638                } else {
639                    format!("{}:{}", variant_type.ord(), variant_type.godot_type_name())
640                }
641            }
642
643            Self::Class { class_id, heritage } => {
644                let export_hint = heritage.export_property_hint();
645                sys::strict_assert_ne!(
646                    export_hint,
647                    PropertyHint::NONE,
648                    "element_type_string() should only be called for exportable object classes (Resource or Node), \
649                     but got ClassAncestor::Other for class `{}`",
650                    class_id.to_cow_str()
651                );
652
653                let hint_string = match heritage {
654                    ClassHeritage::DynResource { implementors } => join_class_ids(implementors),
655                    _ => class_id.to_cow_str().to_string(),
656                };
657                format_elements_typed(VariantType::OBJECT, export_hint, &hint_string)
658            }
659
660            Self::Enum { .. } => {
661                let outer = self.clone().into_outer(); // slightly expensive
662                let variant_type = outer.variant_type();
663                let info = outer.export_hint();
664                format_elements_typed(variant_type, info.hint, &info.hint_string)
665            }
666
667            Self::Custom {
668                variant_type,
669                var_hint,
670                ..
671            } => format_elements_typed(*variant_type, var_hint.hint, &var_hint.hint_string),
672        }
673    }
674}
675
676// ----------------------------------------------------------------------------------------------------------------------------------------------
677
678/// Describes a single enumerator entry: display name and ordinal value. Used in [`GodotShape::Enum`].
679#[derive(Clone, Debug)]
680pub struct EnumeratorShape {
681    pub name: CowStr,
682    /// Ordinal value. `None` for string-backed enums (hint string omits ordinals: `"Grass,Rock,Water"`).
683    pub value: Option<i64>,
684}
685
686impl EnumeratorShape {
687    /// Creates a new int-backed enumerator.
688    pub const fn new_int(name: &'static str, value: i64) -> Self {
689        Self {
690            name: Cow::Borrowed(name),
691            value: Some(value),
692        }
693    }
694
695    /// Creates a new string-backed enumerator (no ordinal in hint string).
696    pub const fn new_string(name: &'static str) -> Self {
697        Self {
698            name: Cow::Borrowed(name),
699            value: None,
700        }
701    }
702}
703
704// ----------------------------------------------------------------------------------------------------------------------------------------------
705// Global helper functions
706
707/// Builds the element type string used in Godot's `TYPE_STRING` hint for typed collections.
708///
709/// Format: `"{vtype}:"` if `hint` is `NONE`, otherwise `"{vtype}/{hint}:{hint_string}"`.
710pub(crate) fn format_elements_typed(
711    variant_type: VariantType,
712    hint: PropertyHint,
713    hint_string: impl std::fmt::Display,
714) -> String {
715    if hint == PropertyHint::NONE {
716        format!("{}:", variant_type.ord())
717    } else {
718        format!("{}/{}:{}", variant_type.ord(), hint.ord(), hint_string)
719    }
720}
721
722/// Formats the element type string for untyped collections (e.g. `Array` without `TYPE_STRING` hint), which only includes the variant type.
723///
724/// Format: `"{vtype}:"`.
725fn format_elements_untyped(variant_type: VariantType) -> String {
726    format!("{}:", variant_type.ord())
727}
728
729/// Returns `PropertyHintInfo` for an enum or bitfield: `ENUM`/`FLAGS` hint with formatted hint string.
730fn enum_hint_info(
731    enumerators: &[EnumeratorShape],
732    is_bitfield: bool,
733    is_engine_enum: bool,
734) -> PropertyHintInfo {
735    // For engine enums used as `#[var]`: before Godot 4.7, GDScript registers these with `NONE` hint, relying on
736    // `class_name` + `CLASS_IS_ENUM` usage flag. From 4.7 onward, GDScript provides the full `ENUM` hint with enumerator list.
737    if is_engine_enum && sys::GdextBuild::before_api("4.7") {
738        return PropertyHintInfo::none();
739    }
740
741    let hint = if is_bitfield {
742        PropertyHint::FLAGS
743    } else {
744        PropertyHint::ENUM
745    };
746
747    PropertyHintInfo {
748        hint,
749        hint_string: GString::from(&format_hint_string(enumerators)),
750    }
751}
752
753/// Builds `"Name:0,Name2:1"` or `"Name,Name2"` hint string from enumerators.
754fn format_hint_string(enumerators: &[EnumeratorShape]) -> String {
755    enumerators
756        .iter()
757        .map(|e| format_hint_entry(&e.name, e.value))
758        .collect::<Vec<_>>()
759        .join(",")
760}
761
762/// Formats a single hint as `"Name:value"` if value is `Some`, otherwise `"Name"`.
763pub(crate) fn format_hint_entry(name: &str, value: Option<impl Display>) -> String {
764    match value {
765        Some(v) => format!("{name}:{v}"),
766        None => name.to_string(),
767    }
768}
769
770/// Maps a packed array's variant type to its element's variant type, if applicable.
771///
772/// Returns `Some(element_type)` for packed array variant types (e.g. `PACKED_BYTE_ARRAY` → `INT`,
773/// `PACKED_STRING_ARRAY` → `STRING`), or `None` for all other variant types.
774#[rustfmt::skip]
775pub(crate) fn packed_element_variant_type(packed_vtype: VariantType) -> Option<VariantType> {
776    match packed_vtype {
777        | VariantType::PACKED_BYTE_ARRAY
778        | VariantType::PACKED_INT32_ARRAY
779        | VariantType::PACKED_INT64_ARRAY   => Some(VariantType::INT),
780        | VariantType::PACKED_FLOAT32_ARRAY
781        | VariantType::PACKED_FLOAT64_ARRAY => Some(VariantType::FLOAT),
782        | VariantType::PACKED_STRING_ARRAY  => Some(VariantType::STRING),
783        | VariantType::PACKED_VECTOR2_ARRAY => Some(VariantType::VECTOR2),
784        | VariantType::PACKED_VECTOR3_ARRAY => Some(VariantType::VECTOR3),
785        #[cfg(since_api = "4.3")]
786        | VariantType::PACKED_VECTOR4_ARRAY => Some(VariantType::VECTOR4),
787        | VariantType::PACKED_COLOR_ARRAY   => Some(VariantType::COLOR),
788        _                                   => None,
789    }
790}
791
792/// Joins class IDs into a comma-separated string for use in DynGd property hints.
793fn join_class_ids(class_ids: &[ClassId]) -> String {
794    class_ids
795        .iter()
796        .map(|id| id.to_cow_str().to_string())
797        .collect::<Vec<_>>()
798        .join(",")
799}