Skip to content

Commit 08ccba9

Browse files
committed
Class icons and default class icons
1 parent b2a2a58 commit 08ccba9

File tree

8 files changed

+143
-8
lines changed

8 files changed

+143
-8
lines changed

godot-core/src/init/mod.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use std::sync::atomic::{AtomicBool, Ordering};
99

1010
use godot_ffi as sys;
11+
use sys::Global;
1112
use sys::GodotFfi;
1213

1314
use crate::builtin::{GString, StringName};
@@ -79,6 +80,15 @@ pub unsafe fn __gdext_load_library<E: ExtensionLibrary>(
7980
crate::private::set_gdext_hook(move || std::thread::current().id() == main_thread);
8081
}
8182

83+
// Write the extension default icon path in a `Global`.
84+
// Will be provided to Godot during class registration (if not empty) and no class icon is provided.
85+
// Empty by default. Resets on deinitialization, see `gdext_on_level_deinit()`
86+
#[cfg(since_api = "4.4")]
87+
{
88+
let mut icon = DEFAULT_ICON.lock();
89+
*icon = E::default_icon();
90+
}
91+
8292
// Currently no way to express failure; could be exposed to E if necessary.
8393
// No early exit, unclear if Godot still requires output parameters to be set.
8494
let success = true;
@@ -117,6 +127,8 @@ pub unsafe fn __gdext_load_library<E: ExtensionLibrary>(
117127
is_success.unwrap_or(0)
118128
}
119129

130+
pub(crate) static DEFAULT_ICON: Global<&'static str> = Global::new(|| "");
131+
120132
static LEVEL_SERVERS_CORE_LOADED: AtomicBool = AtomicBool::new(false);
121133

122134
unsafe extern "C" fn ffi_initialize_layer<E: ExtensionLibrary>(
@@ -240,6 +252,9 @@ fn gdext_on_level_deinit(level: InitLevel) {
240252
crate::meta::cleanup();
241253
}
242254

255+
// Reset the extension default icon to be empty again.
256+
*crate::init::DEFAULT_ICON.lock() = "";
257+
243258
// SAFETY: called after all other logic, so no concurrent access.
244259
// TODO: multithreading must make sure other threads are joined/stopped here.
245260
unsafe {
@@ -332,6 +347,14 @@ pub unsafe trait ExtensionLibrary {
332347
InitLevel::Scene
333348
}
334349

350+
/// Default icon resource path for classes that don't specify one.
351+
///
352+
/// This is used as a fallback when a class doesn't provide an icon, for example `#[class(icon = "...")]`.
353+
#[cfg(since_api = "4.4")]
354+
fn default_icon() -> &'static str {
355+
"" // Default implementation: no icon.
356+
}
357+
335358
/// Custom logic when a certain initialization stage is loaded.
336359
///
337360
/// This will be invoked for stages >= [`Self::min_level()`], in ascending order. Use `if` or `match` to hook to specific stages.

godot-core/src/obj/traits.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ where
4444
Self::class_id()
4545
}
4646

47+
/// Class icon resource path. Will use this icon if available.
48+
///
49+
/// You can also set an icon via `#[class(icon = "path/to/icon.svg")]`.
50+
fn icon() -> &'static str {
51+
""
52+
}
53+
4754
/// Initialization level, during which this class should be initialized with Godot.
4855
///
4956
/// The default is a good choice in most cases; override only if you have very specific initialization requirements.

godot-core/src/registry/class.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ fn global_dyn_traits_by_typeid() -> GlobalGuard<'static, HashMap<any::TypeId, Ve
6060
/// Besides the name, this type holds information relevant for the deregistration of the class.
6161
pub struct LoadedClass {
6262
name: ClassId,
63+
64+
// Class icon needs to be retained for registered for class lifetime, this is not accessed directly.
65+
#[cfg(since_api = "4.4")]
66+
#[allow(unused)]
67+
icon: crate::builtin::GString,
68+
6369
is_editor_plugin: bool,
6470
}
6571

@@ -97,6 +103,8 @@ struct ClassRegistrationInfo {
97103
/// Godot low-level class creation parameters.
98104
godot_params: GodotCreationInfo,
99105

106+
icon_path: crate::builtin::GString,
107+
100108
#[allow(dead_code)] // Currently unused; may be useful for diagnostics in the future.
101109
init_level: InitLevel,
102110
is_editor_plugin: bool,
@@ -149,6 +157,7 @@ pub(crate) fn register_class<
149157
out!("Manually register class {}", std::any::type_name::<T>());
150158

151159
let godot_params = GodotCreationInfo {
160+
icon_path: ptr::null(),
152161
to_string_func: Some(callbacks::to_string::<T>),
153162
notification_func: Some(callbacks::on_notification::<T>),
154163
reference_func: Some(callbacks::reference::<T>),
@@ -167,6 +176,7 @@ pub(crate) fn register_class<
167176

168177
register_class_raw(ClassRegistrationInfo {
169178
class_name: T::class_id(),
179+
icon_path: crate::builtin::GString::from(T::icon()),
170180
parent_class_name: Some(T::Base::class_id()),
171181
register_methods_constants_fn: None,
172182
register_properties_fn: None,
@@ -258,8 +268,10 @@ fn register_classes_and_dyn_traits(
258268

259269
let loaded_class = LoadedClass {
260270
name: class_name,
271+
icon: info.icon_path.clone(),
261272
is_editor_plugin: info.is_editor_plugin,
262273
};
274+
263275
let metadata = ClassMetadata {};
264276

265277
// Transpose Class->Trait relations to Trait->Class relations.
@@ -426,6 +438,7 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) {
426438
is_instantiable,
427439
reference_fn,
428440
unreference_fn,
441+
icon,
429442
}) => {
430443
c.parent_class_name = Some(base_class_name);
431444
c.default_virtual_fn = default_get_virtual_fn;
@@ -466,6 +479,24 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) {
466479
c.godot_params.is_runtime =
467480
sys::conv::bool_to_sys(crate::private::is_class_runtime(is_tool));
468481
}
482+
483+
#[cfg(before_api = "4.4")]
484+
let _ = icon; // mark used
485+
#[cfg(since_api = "4.4")]
486+
{
487+
let chosen = if !icon.is_empty() {
488+
icon
489+
} else {
490+
// Default icon from `ExtensionLibrary::default_class_icon()`.
491+
*crate::init::DEFAULT_ICON.lock()
492+
};
493+
494+
// It's possible that there's no icon.
495+
if !chosen.is_empty() {
496+
c.icon_path = crate::builtin::GString::from(chosen);
497+
c.godot_params.icon_path = c.icon_path.string_sys();
498+
}
499+
}
469500
}
470501

471502
PluginItem::InherentImpl(InherentImpl {
@@ -664,6 +695,7 @@ fn lock_or_panic<T>(global: &'static Global<T>, ctx: &str) -> GlobalGuard<'stati
664695
fn default_registration_info(class_name: ClassId) -> ClassRegistrationInfo {
665696
ClassRegistrationInfo {
666697
class_name,
698+
icon_path: crate::builtin::GString::new(),
667699
parent_class_name: None,
668700
register_methods_constants_fn: None,
669701
register_properties_fn: None,

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 = EXPR)]`.
200+
pub(crate) icon: &'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: T::icon(),
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 = 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: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
4141
let mut fields = parse_fields(named_fields, struct_cfg.init_strategy)?;
4242

4343
if struct_cfg.is_editor_plugin() {
44-
modifiers.push(quote! { with_editor_plugin })
44+
modifiers.push(quote! { with_editor_plugin() })
4545
}
4646

4747
let mut deprecations = std::mem::take(&mut struct_cfg.deprecations);
@@ -58,7 +58,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
5858
let class_name_allocation = quote! { ClassId::__alloc_next_unicode(#class_name_str) };
5959

6060
if struct_cfg.is_internal {
61-
modifiers.push(quote! { with_internal })
61+
modifiers.push(quote! { with_internal() })
6262
}
6363
let base_ty = &struct_cfg.base_ty;
6464
let prv = quote! { ::godot::private };
@@ -113,7 +113,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
113113
match struct_cfg.init_strategy {
114114
InitStrategy::Generated => {
115115
godot_init_impl = make_godot_init_impl(class_name, &fields);
116-
modifiers.push(quote! { with_generated::<#class_name> });
116+
modifiers.push(quote! { with_generated::<#class_name>() });
117117
}
118118
InitStrategy::UserDefined => {
119119
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<TokenStream> {
131131

132132
// Workaround for https://github.com/godot-rust/gdext/issues/874 before Godot 4.5.
133133
#[cfg(before_api = "4.5")]
134-
modifiers.push(quote! { with_generated_no_default::<#class_name> });
134+
modifiers.push(quote! { with_generated_no_default::<#class_name>() });
135135
}
136136
};
137137
if is_instantiable {
138-
modifiers.push(quote! { with_instantiable });
138+
modifiers.push(quote! { with_instantiable() });
139139
}
140140

141141
if has_default_virtual {
142-
modifiers.push(quote! { with_default_get_virtual_fn::<#class_name> });
142+
modifiers.push(quote! { with_default_get_virtual_fn::<#class_name>() });
143143
}
144144

145145
if struct_cfg.is_tool {
146-
modifiers.push(quote! { with_tool })
146+
modifiers.push(quote! { with_tool() })
147+
}
148+
149+
#[cfg(since_api = "4.4")]
150+
if let Some(icon) = &struct_cfg.icon {
151+
modifiers.push(quote! { with_icon(#icon) })
147152
}
148153

149154
// 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<TokenStream> {
198203
#struct_docs_registration
199204
::godot::sys::plugin_add!(#prv::__GODOT_PLUGIN_REGISTRY; #prv::ClassPlugin::new::<#class_name>(
200205
#prv::PluginItem::Struct(
201-
#prv::Struct::new::<#class_name>()#(.#modifiers())*
206+
#prv::Struct::new::<#class_name>()#(.#modifiers)*
202207
)
208+
203209
));
204210

205211
#prv::class_macros::#inherits_macro_ident!(#class_name);
@@ -303,6 +309,7 @@ struct ClassAttributes {
303309
is_tool: bool,
304310
is_internal: bool,
305311
rename: Option<Ident>,
312+
icon: Option<TokenStream>,
306313
deprecations: Vec<TokenStream>,
307314
}
308315

@@ -510,6 +517,7 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult<ClassAttribute
510517
let mut is_tool = false;
511518
let mut is_internal = false;
512519
let mut rename: Option<Ident> = None;
520+
let mut icon: Option<TokenStream> = None;
513521
let mut deprecations = vec![];
514522

515523
// #[class] attribute on struct
@@ -542,6 +550,11 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult<ClassAttribute
542550
// #[class(rename = NewName)]
543551
rename = parser.handle_ident("rename")?;
544552

553+
// #[class(icon = "PATH")]
554+
if let Some(expr) = parser.handle_expr("icon")? {
555+
icon = Some(expr);
556+
}
557+
545558
// #[class(internal)]
546559
// Named "internal" following Godot terminology: https://github.com/godotengine/godot-cpp/blob/master/include/godot_cpp/core/class_db.hpp#L327
547560
if parser.handle_alone("internal")? {
@@ -583,6 +596,7 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult<ClassAttribute
583596
is_tool,
584597
is_internal,
585598
rename,
599+
icon,
586600
deprecations,
587601
})
588602
}

godot-macros/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,17 @@ use crate::util::{bail, ident, KvParser};
484484
/// Even though this class is a `Node` and it has an init function, it still won't show up in the editor as a node you can add to a scene
485485
/// because we have added a `hidden` key to the class. This will also prevent it from showing up in documentation.
486486
///
487+
/// ## Class Icon
488+
///
489+
/// You can set a class icon by providing a valid resource path using `#[class(icon = EXPR)]`.
490+
///
491+
/// ```
492+
/// # use godot::prelude::*;
493+
/// #[derive(GodotClass)]
494+
/// #[class(base=Node, init, icon = "res://icon.svg")]
495+
/// pub struct Foo {}
496+
/// ```
497+
///
487498
/// # Further field customization
488499
///
489500
/// ## Fine-grained inference hints
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
use godot::prelude::*;
9+
10+
use crate::framework::itest;
11+
12+
const ICON: &str = "res://icons/icon.svg";
13+
#[derive(GodotClass)]
14+
#[class(init, base=RefCounted, icon = ICON)]
15+
struct ClassWithIconRefCounted {
16+
base: Base<RefCounted>,
17+
}
18+
19+
#[derive(GodotClass)]
20+
#[class(init, base=Node, tool, icon = ICON)]
21+
struct ClassWithIconNode {
22+
base: Base<Node>,
23+
}
24+
25+
#[itest]
26+
fn class_icon_registers() {
27+
let instance1 = ClassWithIconRefCounted::new_gd();
28+
let instance2 = ClassWithIconNode::new_alloc();
29+
30+
assert!(instance1.is_instance_valid());
31+
assert!(instance2.is_instance_valid());
32+
33+
instance2.free();
34+
}

itest/rust/src/register_tests/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ mod multiple_impl_blocks_test;
1414
mod naming_tests;
1515
mod option_ffi_test;
1616
mod register_docs_test;
17+
18+
#[cfg(since_api = "4.4")]
19+
mod icon_test;
20+
1721
#[cfg(feature = "codegen-full")]
1822
mod rpc_test;
1923
mod var_test;

0 commit comments

Comments
 (0)