Skip to content

Commit 8c511a5

Browse files
committed
Class icons definable by macro
1 parent b2a2a58 commit 8c511a5

File tree

8 files changed

+169
-3
lines changed

8 files changed

+169
-3
lines changed

godot-core/src/meta/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,10 @@ pub(crate) use reexport_crate::*;
102102
/// Must not use meta facilities (e.g. `ClassId`) after this call.
103103
pub(crate) unsafe fn cleanup() {
104104
class_id::cleanup();
105+
106+
#[cfg(since_api = "4.4")]
107+
{
108+
let mut icons = crate::registry::class::global_icons_by_name();
109+
*icons = std::collections::HashMap::new();
110+
}
105111
}

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: 24 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+
pub(crate) fn global_icons_by_name(
58+
) -> GlobalGuard<'static, HashMap<ClassId, crate::builtin::GString>> {
59+
static ICONS_BY_NAME: Global<HashMap<ClassId, crate::builtin::GString>> = Global::default();
60+
61+
lock_or_panic(&ICONS_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;
@@ -466,6 +475,21 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) {
466475
c.godot_params.is_runtime =
467476
sys::conv::bool_to_sys(crate::private::is_class_runtime(is_tool));
468477
}
478+
479+
#[cfg(before_api = "4.4")]
480+
let _ = icon; // mark used
481+
#[cfg(since_api = "4.4")]
482+
if let Some(icon_path) = icon {
483+
// Convert to GString and store in global map to keep it alive for program lifetime.
484+
let icon_gstring = crate::builtin::GString::from(icon_path);
485+
let mut icon_map = global_icons_by_name();
486+
icon_map.insert(c.class_name, icon_gstring);
487+
488+
let icon_ptr: sys::GDExtensionConstStringPtr =
489+
icon_map.get(&c.class_name).unwrap().string_sys();
490+
491+
c.godot_params.icon_path = icon_ptr;
492+
}
469493
}
470494

471495
PluginItem::InherentImpl(InherentImpl {

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: 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: 25 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+
TokenStream::new()
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,11 @@ 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+
}
567+
545568
// #[class(internal)]
546569
// Named "internal" following Godot terminology: https://github.com/godotengine/godot-cpp/blob/master/include/godot_cpp/core/class_db.hpp#L327
547570
if parser.handle_alone("internal")? {
@@ -583,6 +606,7 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult<ClassAttribute
583606
is_tool,
584607
is_internal,
585608
rename,
609+
icon,
586610
deprecations,
587611
})
588612
}

godot-macros/src/lib.rs

Lines changed: 24 additions & 2 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
@@ -561,11 +572,22 @@ use crate::util::{bail, ident, KvParser};
561572
alias = "export",
562573
alias = "tool",
563574
alias = "rename",
564-
alias = "internal"
575+
alias = "internal",
576+
alias = "icon"
565577
)]
566578
#[proc_macro_derive(
567579
GodotClass,
568-
attributes(class, base, hint, var, export, export_group, export_subgroup, init)
580+
attributes(
581+
class,
582+
base,
583+
hint,
584+
var,
585+
export,
586+
export_group,
587+
export_subgroup,
588+
init,
589+
icon
590+
)
569591
)]
570592
pub fn derive_godot_class(input: TokenStream) -> TokenStream {
571593
translate(input, class::derive_godot_class)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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 REF_COUNTED_ICON: &str = "res://icons/ref_counted_icon.svg";
13+
#[derive(GodotClass)]
14+
#[class(init, base=RefCounted, icon = REF_COUNTED_ICON)]
15+
struct ClassWithIconRefCounted {
16+
base: Base<RefCounted>,
17+
}
18+
19+
#[derive(GodotClass)]
20+
#[class(init, base=Node, icon = "INVALID!")]
21+
struct ClassWithInvalidIconNode {
22+
base: Base<Node>,
23+
}
24+
25+
#[derive(GodotClass)]
26+
#[class(init, base=Node, icon = "res://icons/node_icon.svg")]
27+
struct ClassWithIconNode {
28+
base: Base<Node>,
29+
}
30+
31+
#[derive(GodotClass)]
32+
#[class(init, tool, base=RefCounted, icon = "res://icons/tool_icon.svg")]
33+
struct ToolClassWithIcon {
34+
base: Base<RefCounted>,
35+
}
36+
37+
#[derive(GodotClass)]
38+
#[class(init, base=RefCounted, icon = "res://icons/another_icon.svg")]
39+
struct AnotherClassWithIcon {
40+
base: Base<RefCounted>,
41+
}
42+
43+
#[itest]
44+
fn class_with_icon_refcounted_registers() {
45+
let instance = ClassWithIconRefCounted::new_gd();
46+
assert!(instance.is_instance_valid());
47+
}
48+
49+
#[itest]
50+
fn class_with_invalid_icon_refcounted_registers() {
51+
let instance = ClassWithInvalidIconNode::new_alloc();
52+
assert!(instance.is_instance_valid());
53+
}
54+
55+
#[itest]
56+
fn class_with_icon_node_registers() {
57+
let instance = ClassWithIconNode::new_alloc();
58+
assert!(instance.is_instance_valid());
59+
instance.free();
60+
}
61+
62+
#[itest]
63+
fn tool_class_with_icon_registers() {
64+
let instance = ToolClassWithIcon::new_gd();
65+
assert!(instance.is_instance_valid());
66+
}
67+
68+
#[itest]
69+
fn multiple_classes_with_different_icons_register() {
70+
let instance1 = ClassWithIconRefCounted::new_gd();
71+
let instance2 = AnotherClassWithIcon::new_gd();
72+
73+
assert!(instance1.is_instance_valid());
74+
assert!(instance2.is_instance_valid());
75+
}

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)