diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index e35b65bae..3297ccdfd 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -8,6 +8,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use godot_ffi as sys; +use sys::Global; use sys::GodotFfi; use crate::builtin::{GString, StringName}; @@ -79,6 +80,15 @@ pub unsafe fn __gdext_load_library( crate::private::set_gdext_hook(move || std::thread::current().id() == main_thread); } + // Write the extension default icon path in a `Global`. + // Will be provided to Godot during class registration (if not empty) and no class icon is provided. + // Empty by default. Resets on deinitialization, see `gdext_on_level_deinit()`. + #[cfg(since_api = "4.4")] + { + let mut icon = DEFAULT_ICON.lock(); + *icon = E::default_icon(); + } + // Currently no way to express failure; could be exposed to E if necessary. // No early exit, unclear if Godot still requires output parameters to be set. let success = true; @@ -117,6 +127,8 @@ pub unsafe fn __gdext_load_library( is_success.unwrap_or(0) } +pub(crate) static DEFAULT_ICON: Global<&'static str> = Global::new(|| ""); + static LEVEL_SERVERS_CORE_LOADED: AtomicBool = AtomicBool::new(false); unsafe extern "C" fn ffi_initialize_layer( @@ -240,6 +252,9 @@ fn gdext_on_level_deinit(level: InitLevel) { crate::meta::cleanup(); } + // Reset the extension default icon to be empty again. + *crate::init::DEFAULT_ICON.lock() = ""; + // SAFETY: called after all other logic, so no concurrent access. // TODO: multithreading must make sure other threads are joined/stopped here. unsafe { @@ -332,6 +347,14 @@ pub unsafe trait ExtensionLibrary { InitLevel::Scene } + /// Default icon resource path for classes that don't specify one. + /// + /// This is used as a fallback when a class doesn't provide an icon, for example `#[class(icon = "...")]`. + #[cfg(since_api = "4.4")] + fn default_icon() -> &'static str { + "" // Default implementation: no icon. + } + /// Custom logic when a certain initialization stage is loaded. /// /// This will be invoked for stages >= [`Self::min_level()`], in ascending order. Use `if` or `match` to hook to specific stages. diff --git a/godot-core/src/obj/traits.rs b/godot-core/src/obj/traits.rs index 9d522259c..5647d3590 100644 --- a/godot-core/src/obj/traits.rs +++ b/godot-core/src/obj/traits.rs @@ -44,6 +44,13 @@ where Self::class_id() } + /// Class icon resource path. Will use this icon if available. + /// + /// You can also set an icon via `#[class(icon = "path/to/icon.svg")]`. + fn icon() -> &'static str { + "" + } + /// Initialization level, during which this class should be initialized with Godot. /// /// The default is a good choice in most cases; override only if you have very specific initialization requirements. diff --git a/godot-core/src/registry/class.rs b/godot-core/src/registry/class.rs index 1715313ec..a163f3b83 100644 --- a/godot-core/src/registry/class.rs +++ b/godot-core/src/registry/class.rs @@ -60,6 +60,12 @@ fn global_dyn_traits_by_typeid() -> GlobalGuard<'static, HashMap()); let godot_params = GodotCreationInfo { + icon_path: ptr::null(), to_string_func: Some(callbacks::to_string::), notification_func: Some(callbacks::on_notification::), reference_func: Some(callbacks::reference::), @@ -167,6 +176,7 @@ pub(crate) fn register_class< register_class_raw(ClassRegistrationInfo { class_name: T::class_id(), + icon_path: crate::builtin::GString::from(T::icon()), parent_class_name: Some(T::Base::class_id()), register_methods_constants_fn: None, register_properties_fn: None, @@ -258,8 +268,10 @@ fn register_classes_and_dyn_traits( let loaded_class = LoadedClass { name: class_name, + icon: info.icon_path.clone(), is_editor_plugin: info.is_editor_plugin, }; + let metadata = ClassMetadata {}; // Transpose Class->Trait relations to Trait->Class relations. @@ -426,6 +438,7 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { is_instantiable, reference_fn, unreference_fn, + icon, }) => { c.parent_class_name = Some(base_class_name); c.default_virtual_fn = default_get_virtual_fn; @@ -466,6 +479,24 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { c.godot_params.is_runtime = sys::conv::bool_to_sys(crate::private::is_class_runtime(is_tool)); } + + #[cfg(before_api = "4.4")] + let _ = icon; // mark used + #[cfg(since_api = "4.4")] + { + let chosen = if !icon.is_empty() { + icon + } else { + // Default icon from `ExtensionLibrary::default_class_icon()`. + *crate::init::DEFAULT_ICON.lock() + }; + + // It's possible that there's no icon. + if !chosen.is_empty() { + c.icon_path = crate::builtin::GString::from(chosen); + c.godot_params.icon_path = c.icon_path.string_sys(); + } + } } PluginItem::InherentImpl(InherentImpl { @@ -664,6 +695,7 @@ fn lock_or_panic(global: &'static Global, ctx: &str) -> GlobalGuard<'stati fn default_registration_info(class_name: ClassId) -> ClassRegistrationInfo { ClassRegistrationInfo { class_name, + icon_path: crate::builtin::GString::new(), parent_class_name: None, register_methods_constants_fn: None, register_properties_fn: None, diff --git a/godot-core/src/registry/plugin.rs b/godot-core/src/registry/plugin.rs index f03da5fae..b71e3eb7a 100644 --- a/godot-core/src/registry/plugin.rs +++ b/godot-core/src/registry/plugin.rs @@ -195,6 +195,9 @@ pub struct Struct { /// Whether the class has a default constructor. pub(crate) is_instantiable: bool, + + /// Icon path from `#[class(icon = EXPR)]`. + pub(crate) icon: &'static str, } impl Struct { @@ -214,6 +217,7 @@ impl Struct { is_editor_plugin: false, is_internal: false, is_instantiable: false, + icon: T::icon(), // While Godot doesn't do anything with these callbacks for non-RefCounted classes, we can avoid instantiating them in Rust. reference_fn: refcounted.then_some(callbacks::reference::), unreference_fn: refcounted.then_some(callbacks::unreference::), @@ -252,6 +256,12 @@ impl Struct { self } + #[cfg(since_api = "4.4")] + pub fn with_icon(mut self, icon: &'static str) -> Self { + self.icon = icon; + self + } + pub fn with_editor_plugin(mut self) -> Self { self.is_editor_plugin = true; self diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index e2e0db80c..14fc3427b 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -41,7 +41,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { let mut fields = parse_fields(named_fields, struct_cfg.init_strategy)?; if struct_cfg.is_editor_plugin() { - modifiers.push(quote! { with_editor_plugin }) + modifiers.push(quote! { with_editor_plugin() }) } let mut deprecations = std::mem::take(&mut struct_cfg.deprecations); @@ -58,7 +58,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { let class_name_allocation = quote! { ClassId::__alloc_next_unicode(#class_name_str) }; if struct_cfg.is_internal { - modifiers.push(quote! { with_internal }) + modifiers.push(quote! { with_internal() }) } let base_ty = &struct_cfg.base_ty; let prv = quote! { ::godot::private }; @@ -113,7 +113,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { match struct_cfg.init_strategy { InitStrategy::Generated => { godot_init_impl = make_godot_init_impl(class_name, &fields); - modifiers.push(quote! { with_generated::<#class_name> }); + modifiers.push(quote! { with_generated::<#class_name>() }); } InitStrategy::UserDefined => { let fn_name = format_ident!("class_{}_must_have_an_init_method", class_name); @@ -131,19 +131,24 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { // Workaround for https://github.com/godot-rust/gdext/issues/874 before Godot 4.5. #[cfg(before_api = "4.5")] - modifiers.push(quote! { with_generated_no_default::<#class_name> }); + modifiers.push(quote! { with_generated_no_default::<#class_name>() }); } }; if is_instantiable { - modifiers.push(quote! { with_instantiable }); + modifiers.push(quote! { with_instantiable() }); } if has_default_virtual { - modifiers.push(quote! { with_default_get_virtual_fn::<#class_name> }); + modifiers.push(quote! { with_default_get_virtual_fn::<#class_name>() }); } if struct_cfg.is_tool { - modifiers.push(quote! { with_tool }) + modifiers.push(quote! { with_tool() }) + } + + #[cfg(since_api = "4.4")] + if let Some(icon) = &struct_cfg.icon { + modifiers.push(quote! { with_icon(#icon) }) } // Declares a "funcs collection" struct that, for holds a constant for each #[func]. @@ -198,8 +203,9 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { #struct_docs_registration ::godot::sys::plugin_add!(#prv::__GODOT_PLUGIN_REGISTRY; #prv::ClassPlugin::new::<#class_name>( #prv::PluginItem::Struct( - #prv::Struct::new::<#class_name>()#(.#modifiers())* + #prv::Struct::new::<#class_name>()#(.#modifiers)* ) + )); #prv::class_macros::#inherits_macro_ident!(#class_name); @@ -303,6 +309,7 @@ struct ClassAttributes { is_tool: bool, is_internal: bool, rename: Option, + icon: Option, deprecations: Vec, } @@ -510,6 +517,7 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult = None; + let mut icon: Option = None; let mut deprecations = vec![]; // #[class] attribute on struct @@ -542,6 +550,11 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult ParseResult, +} + +#[derive(GodotClass)] +#[class(init, base=Node, tool, icon = ICON)] +struct ClassWithIconNode { + base: Base, +} + +#[itest] +fn class_icon_registers() { + let instance1 = ClassWithIconRefCounted::new_gd(); + let instance2 = ClassWithIconNode::new_alloc(); + + assert!(instance1.is_instance_valid()); + assert!(instance2.is_instance_valid()); + + instance2.free(); +} diff --git a/itest/rust/src/register_tests/mod.rs b/itest/rust/src/register_tests/mod.rs index 3af883be2..c61723dd9 100644 --- a/itest/rust/src/register_tests/mod.rs +++ b/itest/rust/src/register_tests/mod.rs @@ -14,6 +14,10 @@ mod multiple_impl_blocks_test; mod naming_tests; mod option_ffi_test; mod register_docs_test; + +#[cfg(since_api = "4.4")] +mod icon_test; + #[cfg(feature = "codegen-full")] mod rpc_test; mod var_test;