Skip to content

Commit a2a08d2

Browse files
committed
EngineEnum: add values() + all_constants() functions
1 parent 6690631 commit a2a08d2

File tree

8 files changed

+307
-9
lines changed

8 files changed

+307
-9
lines changed

godot-codegen/src/generator/enums.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::models::domain::{Enum, Enumerator, EnumeratorValue, RustTy};
1313
use crate::special_cases;
1414
use proc_macro2::TokenStream;
1515
use quote::{quote, ToTokens};
16+
use std::collections::HashSet;
1617

1718
pub fn make_enums(enums: &[Enum], cfg_attributes: &TokenStream) -> TokenStream {
1819
let definitions = enums.iter().map(make_enum_definition);
@@ -262,6 +263,7 @@ fn make_enum_engine_trait_impl(enum_: &Enum, enum_bitmask: Option<&RustTy>) -> T
262263
});
263264

264265
let str_functions = make_enum_str_functions(enum_);
266+
let values_and_constants_functions = make_enum_values_and_constants_functions(enum_);
265267

266268
quote! {
267269
impl #engine_trait for #name {
@@ -277,6 +279,7 @@ fn make_enum_engine_trait_impl(enum_: &Enum, enum_bitmask: Option<&RustTy>) -> T
277279
}
278280

279281
#str_functions
282+
#values_and_constants_functions
280283
}
281284
}
282285
} else {
@@ -288,6 +291,7 @@ fn make_enum_engine_trait_impl(enum_: &Enum, enum_bitmask: Option<&RustTy>) -> T
288291

289292
let unique_ords = enum_.unique_ords().expect("self is an enum");
290293
let str_functions = make_enum_str_functions(enum_);
294+
let values_and_constants_functions = make_enum_values_and_constants_functions(enum_);
291295

292296
// We can technically check against all possible mask values, remove each mask, and then verify it's a valid base-enum value.
293297
// However, this is not forward compatible: if a new mask is added in a future API version, it wouldn't be removed, and the
@@ -317,6 +321,49 @@ fn make_enum_engine_trait_impl(enum_: &Enum, enum_bitmask: Option<&RustTy>) -> T
317321
}
318322

319323
#str_functions
324+
#values_and_constants_functions
325+
}
326+
}
327+
}
328+
}
329+
330+
/// Creates both the `values()` and `all_constants()` implementations for the enum.
331+
fn make_enum_values_and_constants_functions(enum_: &Enum) -> TokenStream {
332+
let name = &enum_.name;
333+
334+
let mut distinct_values = Vec::new();
335+
let mut all_constants = Vec::new();
336+
let mut seen_ordinals = HashSet::new();
337+
338+
for (index, enumerator) in enum_.enumerators.iter().enumerate() {
339+
let constant = &enumerator.name;
340+
let rust_name = enumerator.name.to_string();
341+
let godot_name = enumerator.godot_name.to_string();
342+
let ordinal = &enumerator.value;
343+
344+
// all_constants() includes every constant unconditionally.
345+
all_constants.push(quote! {
346+
crate::meta::inspect::EnumConstant::new(#rust_name, #godot_name, #ordinal, #name::#constant)
347+
});
348+
349+
// values() contains value only if distinct (first time seen) and not MAX.
350+
if enum_.max_index != Some(index) && seen_ordinals.insert(ordinal.clone()) {
351+
distinct_values.push(quote! { #name::#constant });
352+
}
353+
}
354+
355+
quote! {
356+
fn values() -> &'static [Self] {
357+
&[
358+
#( #distinct_values ),*
359+
]
360+
}
361+
362+
fn all_constants() -> &'static [crate::meta::inspect::EnumConstant<#name>] {
363+
const {
364+
&[
365+
#( #all_constants ),*
366+
]
320367
}
321368
}
322369
}

godot-codegen/src/models/domain/enums.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ pub struct Enumerator {
129129
pub value: EnumeratorValue,
130130
}
131131

132-
#[derive(Clone)]
132+
#[derive(Clone, Hash, Eq, PartialEq)]
133133
pub enum EnumeratorValue {
134134
Enum(i32),
135135
Bitfield(u64),

godot-core/src/builtin/vectors/vector_axis.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,27 @@ macro_rules! impl_vector_axis_enum {
5454
)+
5555
}
5656
}
57+
58+
fn values() -> &'static [Self] {
59+
// For vector axis enums, all values are distinct, so both are the same
60+
&[
61+
$( $AxisEnum::$axis, )+
62+
]
63+
}
64+
65+
fn all_constants() -> &'static [crate::meta::inspect::EnumConstant<$AxisEnum>] {
66+
use crate::meta::inspect::EnumConstant;
67+
const { &[
68+
$(
69+
EnumConstant::new(
70+
stringify!($axis),
71+
concat!("AXIS_", stringify!($axis)),
72+
$AxisEnum::$axis as i32,
73+
$AxisEnum::$axis
74+
),
75+
)+
76+
] }
77+
}
5778
}
5879

5980
impl GodotConvert for $AxisEnum {

godot-core/src/meta/inspect.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
//! Introspection metadata for Godot engine types.
9+
10+
/// Metadata for a single enum constant.
11+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12+
pub struct EnumConstant<T: Copy + 'static> {
13+
rust_name: &'static str,
14+
godot_name: &'static str,
15+
ord: i32,
16+
value: T,
17+
}
18+
19+
impl<T: Copy + 'static> EnumConstant<T> {
20+
/// Creates a new enum constant metadata entry.
21+
pub(crate) const fn new(
22+
rust_name: &'static str,
23+
godot_name: &'static str,
24+
ord: i32,
25+
value: T,
26+
) -> Self {
27+
Self {
28+
rust_name,
29+
godot_name,
30+
ord,
31+
value,
32+
}
33+
}
34+
35+
/// Rust name of the enum variant, usually without Prefix (e.g. `"ESCAPE"` for `Key::ESCAPE`).
36+
///
37+
/// This is returned by [`EngineEnum::as_str()`](crate::obj::EngineEnum::as_str()).
38+
pub const fn rust_name(&self) -> &'static str {
39+
self.rust_name
40+
}
41+
42+
/// Godot constant name (e.g. `"KEY_ESCAPE"` for `Key::ESCAPE`).
43+
pub const fn godot_name(&self) -> &'static str {
44+
self.godot_name
45+
}
46+
47+
/// Ordinal value of this enum variant.
48+
pub const fn ord(&self) -> i32 {
49+
self.ord
50+
}
51+
52+
/// The enum value itself.
53+
pub const fn value(&self) -> T {
54+
self.value
55+
}
56+
}

godot-core/src/meta/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ mod uniform_object_deref;
5757
pub(crate) mod sealed;
5858

5959
pub mod error;
60+
pub mod inspect;
6061

6162
pub use args::*;
6263
pub use class_name::ClassName;

godot-core/src/obj/traits.rs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use crate::builder::ClassBuilder;
99
use crate::builtin::GString;
1010
use crate::init::InitLevel;
11+
use crate::meta::inspect::EnumConstant;
1112
use crate::meta::ClassName;
1213
use crate::obj::{bounds, Base, BaseMut, BaseRef, Bounds, Gd};
1314
#[cfg(since_api = "4.2")]
@@ -187,15 +188,54 @@ pub trait EngineEnum: Copy {
187188
.unwrap_or_else(|| panic!("ordinal {ord} does not map to any enumerator"))
188189
}
189190

190-
// The name of the enumerator, as it appears in Rust.
191-
//
192-
// If the value does not match one of the known enumerators, the empty string is returned.
191+
/// The name of the enumerator, as it appears in Rust.
192+
///
193+
/// Note that **this may not match the Rust constant name!** In case of multiple constants with the same ordinal value, this method returns
194+
/// the first one in the order of definition. For example, `LayoutDirection::LOCALE.as_str()` (ord 1) returns `"APPLICATION_LOCALE"`, because
195+
/// that happens to be the first constant with ordinal `1`. See [`all_constants()`][Self::all_constants] for a more robust and general
196+
/// approach to introspection of enum constants.
197+
///
198+
/// If the value does not match one of the known enumerators, the empty string is returned.
193199
fn as_str(&self) -> &'static str;
194200

195-
// The equivalent name of the enumerator, as specified in Godot.
196-
//
197-
// If the value does not match one of the known enumerators, the empty string is returned.
201+
/// The equivalent name of the enumerator, as specified in Godot.
202+
///
203+
/// If the value does not match one of the known enumerators, the empty string is returned.
198204
fn godot_name(&self) -> &'static str;
205+
206+
/// Returns a slice of distinct enum values.
207+
///
208+
/// This excludes MAX constants and deduplicates aliases, providing only meaningful enum values.
209+
///
210+
/// This enables iteration over distinct enum variants:
211+
/// ```no_run
212+
/// use godot::classes::window;
213+
/// use godot::obj::EngineEnum;
214+
///
215+
/// for mode in window::Mode::values() {
216+
/// println!("* {}: {}", mode.as_str(), mode.ord());
217+
/// }
218+
/// ```
219+
fn values() -> &'static [Self];
220+
221+
/// Returns metadata for all enum constants.
222+
///
223+
/// This includes all constants as they appear in the enum definition, including duplicates and MAX constants.
224+
///
225+
/// This enables complete introspection and debugging:
226+
/// ```no_run
227+
/// use godot::classes::window;
228+
/// use godot::obj::EngineEnum;
229+
///
230+
/// for constant in window::Mode::all_constants() {
231+
/// println!("* {}: {} (ord: {})",
232+
/// constant.rust_name(),
233+
/// constant.godot_name(),
234+
/// constant.ord()
235+
/// );
236+
/// }
237+
/// ```
238+
fn all_constants() -> &'static [EnumConstant<Self>];
199239
}
200240

201241
/// Auto-implemented for all engine-provided bitfields.

itest/rust/src/engine_tests/engine_enum_test.rs

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
use crate::framework::itest;
99

10-
use godot::global::{Key, KeyModifierMask};
10+
use godot::classes::{mesh, window};
11+
use godot::global::{InlineAlignment, Key, KeyModifierMask, Orientation};
1112
use godot::obj::{EngineBitfield, EngineEnum};
1213

1314
#[itest]
@@ -46,3 +47,135 @@ fn enum_with_masked_bitfield_from_ord() {
4647
// let back = Key::try_from_ord(31);
4748
// assert_eq!(back, None, "don't deserialize invalid bitmasked enum");
4849
}
50+
51+
#[itest]
52+
fn enum_values_class() {
53+
let expected_modes = [
54+
window::Mode::WINDOWED,
55+
window::Mode::MINIMIZED,
56+
window::Mode::MAXIMIZED,
57+
window::Mode::FULLSCREEN,
58+
window::Mode::EXCLUSIVE_FULLSCREEN,
59+
];
60+
61+
assert_eq!(window::Mode::values(), &expected_modes);
62+
}
63+
64+
#[itest]
65+
fn enum_values_global() {
66+
let expected_orientations = [Orientation::VERTICAL, Orientation::HORIZONTAL];
67+
68+
assert_eq!(Orientation::values(), &expected_orientations);
69+
}
70+
71+
#[itest]
72+
fn enum_values_duplicates() {
73+
// InlineAlignment has many duplicate ordinals, but values() should return only distinct ones
74+
// The order matches the declaration order in the JSON API, not ordinal order
75+
let expected = [
76+
(InlineAlignment::TOP_TO, 0, true),
77+
(InlineAlignment::CENTER_TO, 1, true),
78+
(InlineAlignment::BASELINE_TO, 3, true),
79+
(InlineAlignment::BOTTOM_TO, 2, true),
80+
(InlineAlignment::TO_TOP, 0, false), // duplicate of TOP_TO
81+
(InlineAlignment::TO_CENTER, 4, true),
82+
(InlineAlignment::TO_BASELINE, 8, true),
83+
(InlineAlignment::TO_BOTTOM, 12, true),
84+
(InlineAlignment::TOP, 0, false), // duplicate of TOP_TO
85+
(InlineAlignment::CENTER, 5, true),
86+
(InlineAlignment::BOTTOM, 14, true),
87+
(InlineAlignment::IMAGE_MASK, 3, false), // duplicate of BASELINE_TO
88+
(InlineAlignment::TEXT_MASK, 12, false), // duplicate of TO_BOTTOM
89+
];
90+
91+
let all_constants = InlineAlignment::all_constants();
92+
let mut expected_distinct_values = vec![];
93+
for ((value, ord, is_distinct), c) in expected.into_iter().zip(all_constants) {
94+
if is_distinct {
95+
assert_eq!(c.rust_name(), value.as_str()); // First distinct.
96+
expected_distinct_values.push(value);
97+
}
98+
99+
assert_eq!(c.value(), value);
100+
assert_eq!(c.ord(), ord);
101+
}
102+
103+
assert_eq!(InlineAlignment::values(), &expected_distinct_values);
104+
105+
// Some known duplicates.
106+
assert_eq!(InlineAlignment::TOP_TO, InlineAlignment::TO_TOP); // ord 0
107+
assert_eq!(InlineAlignment::TOP_TO, InlineAlignment::TOP); // ord 0
108+
assert_eq!(InlineAlignment::BASELINE_TO, InlineAlignment::IMAGE_MASK); // ord 3
109+
assert_eq!(InlineAlignment::TO_BOTTOM, InlineAlignment::TEXT_MASK); // ord 12
110+
}
111+
112+
#[itest]
113+
fn enum_values_max_excluded() {
114+
let expected_array_types = [
115+
mesh::ArrayType::VERTEX,
116+
mesh::ArrayType::NORMAL,
117+
mesh::ArrayType::TANGENT,
118+
mesh::ArrayType::COLOR,
119+
mesh::ArrayType::TEX_UV,
120+
mesh::ArrayType::TEX_UV2,
121+
mesh::ArrayType::CUSTOM0,
122+
mesh::ArrayType::CUSTOM1,
123+
mesh::ArrayType::CUSTOM2,
124+
mesh::ArrayType::CUSTOM3,
125+
mesh::ArrayType::BONES,
126+
mesh::ArrayType::WEIGHTS,
127+
mesh::ArrayType::INDEX,
128+
];
129+
130+
let array_types = mesh::ArrayType::values();
131+
assert_eq!(array_types, &expected_array_types);
132+
assert!(
133+
!array_types.contains(&mesh::ArrayType::MAX),
134+
"ArrayType::MAX should be excluded from values()"
135+
);
136+
137+
// However, it should still be present in all_constants().
138+
let all_constants = mesh::ArrayType::all_constants();
139+
assert!(
140+
all_constants
141+
.iter()
142+
.any(|c| c.value() == mesh::ArrayType::MAX),
143+
"ArrayType::MAX should be present in all_constants()"
144+
);
145+
}
146+
147+
#[itest]
148+
fn enum_all_constants() {
149+
let constants = InlineAlignment::all_constants();
150+
assert!(
151+
constants.len() > InlineAlignment::values().len(),
152+
"all_constants() should include duplicates"
153+
);
154+
155+
// Check one known constant.
156+
let first = constants[0];
157+
assert_eq!(first.rust_name(), "TOP_TO");
158+
assert_eq!(first.godot_name(), "INLINE_ALIGNMENT_TOP_TO");
159+
assert_eq!(first.ord(), 0);
160+
assert_eq!(first.value(), InlineAlignment::TOP_TO);
161+
162+
// Check specific constants at known indices, with equal ordinals.
163+
let known_a = constants[2];
164+
let known_b = constants[11];
165+
166+
assert_eq!(known_a.ord(), 3);
167+
assert_eq!(known_a.rust_name(), "BASELINE_TO");
168+
assert_eq!(known_a.godot_name(), "INLINE_ALIGNMENT_BASELINE_TO");
169+
assert_eq!(known_a.value(), InlineAlignment::BASELINE_TO);
170+
171+
assert_eq!(known_b.ord(), 3);
172+
assert_eq!(known_b.rust_name(), "IMAGE_MASK");
173+
assert_eq!(known_b.godot_name(), "INLINE_ALIGNMENT_IMAGE_MASK");
174+
assert_eq!(known_b.value(), InlineAlignment::IMAGE_MASK);
175+
176+
// "Front-end" values are equal, too.
177+
assert_eq!(
178+
InlineAlignment::IMAGE_MASK.ord(),
179+
InlineAlignment::BASELINE_TO.ord()
180+
);
181+
}

0 commit comments

Comments
 (0)