Skip to content

Commit 5fbf8fc

Browse files
committed
GdExtension and class icons defined by macro attributes
1 parent b2a2a58 commit 5fbf8fc

File tree

6 files changed

+134
-1
lines changed

6 files changed

+134
-1
lines changed

godot-core/src/init/mod.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@ struct InitUserData {
2727
main_loop_callbacks: sys::GDExtensionMainLoopCallbacks,
2828
}
2929

30+
#[derive(Copy, Clone, Default)]
31+
pub struct ExtensionConfig {
32+
pub icon: Option<&'static str>,
33+
}
34+
35+
impl ExtensionConfig {
36+
pub const fn new() -> Self {
37+
Self { icon: None }
38+
}
39+
40+
pub const fn with_icon(mut self, icon: &'static str) -> Self {
41+
self.icon = Some(icon);
42+
self
43+
}
44+
}
45+
3046
#[cfg(since_api = "4.5")]
3147
unsafe extern "C" fn startup_func<E: ExtensionLibrary>() {
3248
E::on_stage_init(InitStage::MainLoop);
@@ -117,6 +133,11 @@ pub unsafe fn __gdext_load_library<E: ExtensionLibrary>(
117133
is_success.unwrap_or(0)
118134
}
119135

136+
/// This contains metadata declared through [`#[gdextension]`](attr.gdextension.html)
137+
pub fn extension_config() -> ExtensionConfig {
138+
crate::private::extension_config()
139+
}
140+
120141
static LEVEL_SERVERS_CORE_LOADED: AtomicBool = AtomicBool::new(false);
121142

122143
unsafe extern "C" fn ffi_initialize_layer<E: ExtensionLibrary>(
@@ -289,6 +310,18 @@ fn gdext_on_level_deinit(level: InitLevel) {
289310
/// Note that this only changes the name. You cannot provide your own function -- use the [`on_level_init()`][ExtensionLibrary::on_level_init]
290311
/// hook for custom startup logic.
291312
///
313+
/// # Extension metadata
314+
/// Provide additional metadata directly on the attribute. For example, on Godot 4.4+ you can assign the icon shown in the editor's
315+
/// extension list like so:
316+
/// ```no_run
317+
/// # use godot::init::*;
318+
/// struct MyExtension;
319+
///
320+
/// #[gdextension(icon = "res://addons/my_extension/icon.svg")]
321+
/// unsafe impl ExtensionLibrary for MyExtension {}
322+
/// ```
323+
/// The values provided here are exposed via [`extension_config()`] for tooling and build scripts.
324+
///
292325
/// # Availability of Godot APIs during init and deinit
293326
// Init order: see also special_cases.rs > classify_codegen_level().
294327
/// Godot loads functionality gradually during its startup routines, and unloads it during shutdown. As a result, Godot classes are only

godot-core/src/private.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ static ERROR_PRINT_LEVEL: atomic::AtomicU8 = atomic::AtomicU8::new(2);
5757
sys::plugin_registry!(pub __GODOT_PLUGIN_REGISTRY: ClassPlugin);
5858
#[cfg(all(since_api = "4.3", feature = "register-docs"))]
5959
sys::plugin_registry!(pub __GODOT_DOCS_REGISTRY: DocsPlugin);
60+
sys::plugin_registry!(pub __GODOT_EXTENSION_CONFIG_REGISTRY: crate::init::ExtensionConfig);
6061

6162
// ----------------------------------------------------------------------------------------------------------------------------------------------
6263
// Call error handling
@@ -151,6 +152,11 @@ pub(crate) fn iterate_plugins(mut visitor: impl FnMut(&ClassPlugin)) {
151152
sys::plugin_foreach!(__GODOT_PLUGIN_REGISTRY; visitor);
152153
}
153154

155+
pub(crate) fn extension_config() -> crate::init::ExtensionConfig {
156+
let guard = __GODOT_EXTENSION_CONFIG_REGISTRY.lock().unwrap();
157+
guard.last().copied().unwrap_or_default()
158+
}
159+
154160
#[cfg(all(since_api = "4.3", feature = "register-docs"))]
155161
pub(crate) fn iterate_docs_plugins(mut visitor: impl FnMut(&DocsPlugin)) {
156162
sys::plugin_foreach!(__GODOT_DOCS_REGISTRY; visitor);
@@ -186,6 +192,7 @@ pub trait You_forgot_the_attribute__godot_api {}
186192

187193
pub struct ClassConfig {
188194
pub is_tool: bool,
195+
pub icon: Option<&'static str>,
189196
}
190197

191198
// ----------------------------------------------------------------------------------------------------------------------------------------------

godot-core/src/registry/class.rs

Lines changed: 46 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
@@ -559,6 +588,23 @@ fn register_class_raw(mut info: ClassRegistrationInfo) {
559588
info.godot_params.get_virtual_func = info.user_virtual_fn.or(info.default_virtual_fn);
560589
}
561590

591+
// If no class-level icon was set, use the extension-level icon as fallback (if available)
592+
#[cfg(since_api = "4.4")]
593+
if info.godot_params.icon_path.is_null() {
594+
if let Some(extension_icon) = crate::init::extension_config().icon {
595+
let icon_gstring = crate::builtin::GString::from(extension_icon);
596+
597+
// SAFETY: The GString is stored in a static HashMap, so the pointer remains valid
598+
// even after the lock guard is dropped. We must retrieve the pointer while holding
599+
// the lock to ensure the HashMap isn't being modified concurrently.
600+
let mut icon_map = global_icon_strings();
601+
let icon_ptr = icon_gstring.string_sys() as sys::GDExtensionConstStringPtr;
602+
icon_map.insert(class_name, icon_gstring);
603+
604+
info.godot_params.icon_path = icon_ptr;
605+
}
606+
}
607+
562608
// The explicit () type notifies us if Godot API ever adds a return type.
563609
let registration_failed = unsafe {
564610
// Try to register class...

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
}

godot-macros/src/gdextension.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub fn attribute_gdextension(item: venial::Item) -> ParseResult<TokenStream> {
2929
let mut parser = KvParser::parse_required(&drained_attributes, "gdextension", &impl_decl)?;
3030
let entry_point = parser.handle_ident("entry_point")?;
3131
let entry_symbol = parser.handle_ident("entry_symbol")?;
32+
let icon = parser.handle_expr("icon")?;
3233
parser.finish()?;
3334

3435
if entry_point.is_some() && entry_symbol.is_some() {
@@ -50,7 +51,20 @@ pub fn attribute_gdextension(item: venial::Item) -> ParseResult<TokenStream> {
5051

5152
let impl_ty = &impl_decl.self_ty;
5253

54+
let mut modifiers = Vec::new();
55+
if let Some(icon) = icon {
56+
modifiers.push(quote! { with_icon(#icon) });
57+
}
58+
59+
let extension_config_registration = quote! {
60+
::godot::sys::plugin_add!(
61+
::godot::private::__GODOT_EXTENSION_CONFIG_REGISTRY;
62+
::godot::init::ExtensionConfig::new()#( .#modifiers )*
63+
);
64+
};
65+
5366
Ok(quote! {
67+
#extension_config_registration
5468
#deprecation
5569
#impl_decl
5670

0 commit comments

Comments
 (0)