Skip to content

Commit a4993f8

Browse files
committed
Class icons
- register via #[class(icon = EXPR)] - requires api 4.4+, uses GDExtensionClassCreationInfo4.icon_path - global_icon_strings(): GlobalGuard that stores icons by ClassId
1 parent b2a2a58 commit a4993f8

File tree

4 files changed

+64
-1
lines changed

4 files changed

+64
-1
lines changed

godot-core/src/private.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ pub trait You_forgot_the_attribute__godot_api {}
186186

187187
pub struct ClassConfig {
188188
pub is_tool: bool,
189+
pub icon: Option<&'static str>,
189190
}
190191

191192
// ----------------------------------------------------------------------------------------------------------------------------------------------

godot-core/src/registry/class.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ fn global_dyn_traits_by_typeid() -> GlobalGuard<'static, HashMap<any::TypeId, Ve
5353
lock_or_panic(&DYN_TRAITS_BY_TYPEID, "dyn traits")
5454
}
5555

56+
#[cfg(since_api = "4.4")]
57+
fn global_icon_strings() -> GlobalGuard<'static, HashMap<ClassId, crate::builtin::GString>> {
58+
static ICON_STRINGS_BY_NAME: Global<HashMap<ClassId, crate::builtin::GString>> =
59+
Global::default();
60+
61+
lock_or_panic(&ICON_STRINGS_BY_NAME, "icon strings (by name)")
62+
}
63+
5664
// ----------------------------------------------------------------------------------------------------------------------------------------------
5765

5866
/// Represents a class which is currently loaded and retained in memory.
@@ -426,6 +434,7 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) {
426434
is_instantiable,
427435
reference_fn,
428436
unreference_fn,
437+
icon,
429438
}) => {
430439
c.parent_class_name = Some(base_class_name);
431440
c.default_virtual_fn = default_get_virtual_fn;
@@ -458,6 +467,26 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) {
458467
.expect("duplicate: recreate_instance_func (def)");
459468

460469
c.godot_params.is_exposed = sys::conv::bool_to_sys(!is_internal);
470+
#[cfg(before_api = "4.4")]
471+
let _ = icon; // mark used
472+
#[cfg(since_api = "4.4")]
473+
if let Some(icon_path) = icon {
474+
// Convert to GString and store in global map to keep it alive for program lifetime
475+
let icon_gstring = crate::builtin::GString::from(icon_path);
476+
477+
let mut icon_map = global_icon_strings();
478+
icon_map.insert(c.class_name, icon_gstring);
479+
480+
// Get pointer after insertion, while lock is still held
481+
// SAFETY: The GString is stored in a static HashMap, so the pointer remains valid
482+
// even after the lock guard is dropped. We must retrieve the pointer while holding
483+
// the lock to ensure the HashMap isn't being modified concurrently.
484+
let icon_ptr = icon_map.get(&c.class_name).unwrap().string_sys()
485+
as sys::GDExtensionConstStringPtr;
486+
487+
// Set the pointer in godot_params
488+
c.godot_params.icon_path = icon_ptr;
489+
}
461490

462491
#[cfg(before_api = "4.3")]
463492
let _ = is_tool; // mark used

godot-core/src/registry/plugin.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@ pub struct Struct {
195195

196196
/// Whether the class has a default constructor.
197197
pub(crate) is_instantiable: bool,
198+
199+
/// Icon path from `#[class(icon = "path")]`.
200+
pub(crate) icon: Option<&'static str>,
198201
}
199202

200203
impl Struct {
@@ -214,6 +217,7 @@ impl Struct {
214217
is_editor_plugin: false,
215218
is_internal: false,
216219
is_instantiable: false,
220+
icon: None,
217221
// While Godot doesn't do anything with these callbacks for non-RefCounted classes, we can avoid instantiating them in Rust.
218222
reference_fn: refcounted.then_some(callbacks::reference::<T>),
219223
unreference_fn: refcounted.then_some(callbacks::unreference::<T>),
@@ -252,6 +256,12 @@ impl Struct {
252256
self
253257
}
254258

259+
#[cfg(since_api = "4.4")]
260+
pub fn with_icon(mut self, icon: &'static str) -> Self {
261+
self.icon = Some(icon);
262+
self
263+
}
264+
255265
pub fn with_editor_plugin(mut self) -> Self {
256266
self.is_editor_plugin = true;
257267
self

godot-macros/src/class/derive_godot_class.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
103103
class_name,
104104
&struct_cfg.base_ty,
105105
struct_cfg.is_tool,
106+
struct_cfg.icon.as_ref(),
106107
&fields.all_fields,
107108
);
108109

@@ -146,6 +147,13 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
146147
modifiers.push(quote! { with_tool })
147148
}
148149

150+
// Handle icon separately since it takes an argument (can't use the modifiers pattern)
151+
let icon_modifier = if let Some(icon) = &struct_cfg.icon {
152+
quote! { .with_icon(#icon) }
153+
} else {
154+
quote! {}
155+
};
156+
149157
// Declares a "funcs collection" struct that, for holds a constant for each #[func].
150158
// That constant maps the Rust name (constant ident) to the Godot registered name (string value).
151159
let funcs_collection_struct_name = format_funcs_collection_struct(class_name);
@@ -198,7 +206,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
198206
#struct_docs_registration
199207
::godot::sys::plugin_add!(#prv::__GODOT_PLUGIN_REGISTRY; #prv::ClassPlugin::new::<#class_name>(
200208
#prv::PluginItem::Struct(
201-
#prv::Struct::new::<#class_name>()#(.#modifiers())*
209+
#prv::Struct::new::<#class_name>()#(.#modifiers())*#icon_modifier
202210
)
203211
));
204212

@@ -303,6 +311,7 @@ struct ClassAttributes {
303311
is_tool: bool,
304312
is_internal: bool,
305313
rename: Option<Ident>,
314+
icon: Option<TokenStream>,
306315
deprecations: Vec<TokenStream>,
307316
}
308317

@@ -421,6 +430,7 @@ fn make_user_class_impl(
421430
class_name: &Ident,
422431
trait_base_class: &Ident,
423432
is_tool: bool,
433+
icon: Option<&TokenStream>,
424434
all_fields: &[Field],
425435
) -> (TokenStream, bool) {
426436
#[cfg(feature = "codegen-full")]
@@ -480,12 +490,19 @@ fn make_user_class_impl(
480490
None
481491
};
482492

493+
let icon = if let Some(expr) = icon {
494+
quote! { Some(#expr) }
495+
} else {
496+
quote! { None }
497+
};
498+
483499
let user_class_impl = quote! {
484500
impl ::godot::obj::UserClass for #class_name {
485501
#[doc(hidden)]
486502
fn __config() -> ::godot::private::ClassConfig {
487503
::godot::private::ClassConfig {
488504
is_tool: #is_tool,
505+
icon: #icon,
489506
}
490507
}
491508

@@ -510,6 +527,7 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult<ClassAttribute
510527
let mut is_tool = false;
511528
let mut is_internal = false;
512529
let mut rename: Option<Ident> = None;
530+
let mut icon: Option<TokenStream> = None;
513531
let mut deprecations = vec![];
514532

515533
// #[class] attribute on struct
@@ -542,6 +560,10 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult<ClassAttribute
542560
// #[class(rename = NewName)]
543561
rename = parser.handle_ident("rename")?;
544562

563+
// #[class(icon = "PATH")]
564+
if let Some(expr) = parser.handle_expr("icon")? {
565+
icon = Some(expr);
566+
}
545567
// #[class(internal)]
546568
// Named "internal" following Godot terminology: https://github.com/godotengine/godot-cpp/blob/master/include/godot_cpp/core/class_db.hpp#L327
547569
if parser.handle_alone("internal")? {
@@ -583,6 +605,7 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult<ClassAttribute
583605
is_tool,
584606
is_internal,
585607
rename,
608+
icon,
586609
deprecations,
587610
})
588611
}

0 commit comments

Comments
 (0)