diff --git a/godot-codegen/src/generator/classes.rs b/godot-codegen/src/generator/classes.rs index c46f2b53b..7b038971f 100644 --- a/godot-codegen/src/generator/classes.rs +++ b/godot-codegen/src/generator/classes.rs @@ -7,7 +7,9 @@ use crate::context::{Context, NotificationEnum}; use crate::generator::functions_common::{FnCode, FnDefinition, FnDefinitions}; use crate::generator::method_tables::MethodTableKey; -use crate::generator::{constants, docs, enums, functions_common, notifications, virtual_traits}; +use crate::generator::{ + constants, docs, enums, functions_common, notifications, signals, virtual_traits, +}; use crate::models::domain::{ ApiView, Class, ClassLike, ClassMethod, ExtensionApi, FnDirection, FnQualifier, Function, ModName, TyName, @@ -121,6 +123,8 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas builders, } = make_class_methods(class, &class.methods, &cfg_attributes, ctx); + let signal_types = signals::make_class_signals(class, &class.signals, ctx); + let enums = enums::make_enums(&class.enums, &cfg_attributes); let constants = constants::make_constants(&class.constants); let inherits_macro = format_ident!("unsafe_inherits_transitive_{}", class_name.rust_ty); @@ -133,14 +137,16 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas // Associated "sidecar" module is made public if there are other symbols related to the class, which are not // in top-level godot::classes module (notification enums are not in the sidecar, but in godot::classes::notify). // This checks if token streams (i.e. code) is empty. - let has_sidecar_module = !enums.is_empty() || !builders.is_empty(); + let has_sidecar_module = !enums.is_empty() || !builders.is_empty() || signal_types.is_some(); let class_doc = docs::make_class_doc( class_name, base_ident_opt, notification_enum.is_some(), has_sidecar_module, + signal_types.is_some(), ); + let module_doc = docs::make_module_doc(class_name); let virtual_trait = virtual_traits::make_virtual_methods_trait( @@ -256,6 +262,7 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas #builders #enums + #signal_types }; // note: TypePtr -> ObjectPtr conversion OK? diff --git a/godot-codegen/src/generator/docs.rs b/godot-codegen/src/generator/docs.rs index 2bb69267f..ae98fb6a1 100644 --- a/godot-codegen/src/generator/docs.rs +++ b/godot-codegen/src/generator/docs.rs @@ -9,6 +9,7 @@ //! //! Single module for documentation, rather than having it in each symbol-specific file, so it's easier to keep docs consistent. +use crate::generator::signals; use crate::models::domain::{ModName, TyName}; use crate::special_cases; use proc_macro2::Ident; @@ -18,6 +19,7 @@ pub fn make_class_doc( base_ident_opt: Option, has_notification_enum: bool, has_sidecar_module: bool, + has_signal_collection: bool, ) -> String { let TyName { rust_ty, godot_ty } = class_name; @@ -29,15 +31,28 @@ pub fn make_class_doc( .to_string() }; - let notify_line = if has_notification_enum { - format!("* [`{rust_ty}Notification`][crate::classes::notify::{rust_ty}Notification]: notification type\n") + let (sidecar_signal_lines, module_name); + if has_sidecar_module { + let module = ModName::from_godot(&class_name.godot_ty).rust_mod; + + sidecar_signal_lines = format!("* [`{module}`][crate::classes::{module}]: sidecar module with related enum/flag types\n"); + module_name = Some(module); + } else { + sidecar_signal_lines = String::new(); + module_name = None; + }; + + let signal_line = if has_signal_collection { + let signal_coll = signals::make_collection_name(class_name); + let module = module_name.expect("signal implies presence of sidecar module"); + + format!("* [`{signal_coll}`][crate::classes::{module}::{signal_coll}]: signal collection\n") } else { String::new() }; - let sidecar_line = if has_sidecar_module { - let module_name = ModName::from_godot(&class_name.godot_ty).rust_mod; - format!("* [`{module_name}`][crate::classes::{module_name}]: sidecar module with related enum/flag types\n") + let notify_line = if has_notification_enum { + format!("* [`{rust_ty}Notification`][crate::classes::notify::{rust_ty}Notification]: notification type\n") } else { String::new() }; @@ -59,8 +74,9 @@ pub fn make_class_doc( {inherits_line}\n\n\ \ Related symbols:\n\n\ - {sidecar_line}\ + {sidecar_signal_lines}\ * [`{trait_name}`][crate::classes::{trait_name}]: virtual methods\n\ + {signal_line}\ {notify_line}\ \n\n\ See also [Godot docs for `{godot_ty}`]({online_link}).\n\n{notes}", diff --git a/godot-codegen/src/generator/mod.rs b/godot-codegen/src/generator/mod.rs index 3debd075a..459cca199 100644 --- a/godot-codegen/src/generator/mod.rs +++ b/godot-codegen/src/generator/mod.rs @@ -25,6 +25,7 @@ pub mod lifecycle_builtins; pub mod method_tables; pub mod native_structures; pub mod notifications; +pub mod signals; pub mod utility_functions; pub mod virtual_traits; diff --git a/godot-codegen/src/generator/signals.rs b/godot-codegen/src/generator/signals.rs new file mode 100644 index 000000000..e94f9b441 --- /dev/null +++ b/godot-codegen/src/generator/signals.rs @@ -0,0 +1,225 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// Code duplication: while there is some overlap with godot-macros/signal.rs for #[signal] handling, this file here is quite a bit simpler, +// as it only deals with predefined signal definitions (no user-defined #[cfg], visibility, etc). On the other hand, there is documentation +// for these signals, and integration is slightly different due to lack of WithBaseField trait. Nonetheless, some parts could potentially +// be extracted into a future crate shared by godot-codegen and godot-macros. + +use crate::context::Context; +use crate::conv; +use crate::models::domain::{Class, ClassLike, ClassSignal, FnParam, RustTy, TyName}; +use crate::util::{ident, safe_ident}; +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote}; + +pub fn make_class_signals( + class: &Class, + signals: &[ClassSignal], + _ctx: &mut Context, +) -> Option { + if signals.is_empty() { + return None; + } + + let all_params: Vec = signals + .iter() + .map(|s| SignalParams::new(&s.parameters)) + .collect(); + + let signal_collection_struct = make_signal_collection(class, signals, &all_params); + + let signal_types = signals + .iter() + .zip(all_params.iter()) + .map(|(signal, params)| make_signal_individual_struct(signal, params)); + + let class_name = class.name(); + + Some(quote! { + #[cfg(since_api = "4.2")] + pub use signals::*; + + #[cfg(since_api = "4.2")] + mod signals { + use crate::obj::Gd; + use super::re_export::#class_name; + use super::*; + + #signal_collection_struct + #( #signal_types )* + } + }) +} + +// Used outside, to document class with links to this type. +pub fn make_collection_name(class_name: &TyName) -> Ident { + format_ident!("SignalsIn{}", class_name.rust_ty) +} + +fn make_individual_struct_name(signal_name: &str) -> Ident { + let signal_pascal_name = conv::to_pascal_case(signal_name); + format_ident!("Sig{}", signal_pascal_name) +} + +fn make_signal_collection( + class: &Class, + signals: &[ClassSignal], + params: &[SignalParams], +) -> TokenStream { + let class_name = class.name(); + let collection_struct_name = make_collection_name(class_name); + + let provider_methods = signals.iter().zip(params).map(|(sig, params)| { + let signal_name_str = &sig.name; + let signal_name = ident(&sig.name); + let individual_struct_name = make_individual_struct_name(&sig.name); + let provider_docs = format!("Signature: `({})`", params.formatted_types); + + quote! { + // Important to return lifetime 'c here, not '_. + #[doc = #provider_docs] + pub fn #signal_name(&mut self) -> #individual_struct_name<'c> { + #individual_struct_name { + typed: crate::registry::signal::TypedSignal::new(self.__gd.clone(), #signal_name_str) + } + } + } + }); + + let collection_docs = format!( + "A collection of signals for the [`{c}`][crate::classes::{c}] class.", + c = class_name.rust_ty + ); + + quote! { + #[doc = #collection_docs] + pub struct #collection_struct_name<'c> { + __gd: &'c mut Gd<#class_name>, + } + + impl<'c> #collection_struct_name<'c> { + #( #provider_methods )* + } + + impl crate::obj::WithSignals for #class_name { + type SignalCollection<'c> = #collection_struct_name<'c>; + #[doc(hidden)] + type __SignalObject<'c> = Gd<#class_name>; + + #[doc(hidden)] + fn __signals_from_external(external: &mut Gd) -> Self::SignalCollection<'_> { + Self::SignalCollection { + __gd: external, + } + } + } + } +} + +fn make_signal_individual_struct(signal: &ClassSignal, params: &SignalParams) -> TokenStream { + let individual_struct_name = make_individual_struct_name(&signal.name); + + let SignalParams { + param_list, + type_list, + name_list, + .. + } = params; + + let class_name = &signal.surrounding_class; + let class_ty = quote! { #class_name }; + let param_tuple = quote! { ( #type_list ) }; + let typed_name = format_ident!("Typed{}", individual_struct_name); + + // Embedded in `mod signals`. + quote! { + // Reduce tokens to parse by reusing this type definitions. + type #typed_name<'c> = crate::registry::signal::TypedSignal<'c, #class_ty, #param_tuple>; + + pub struct #individual_struct_name<'c> { + typed: #typed_name<'c>, + } + + impl<'c> #individual_struct_name<'c> { + pub fn emit(&mut self, #param_list) { + self.typed.emit_tuple( (#name_list) ); + } + } + + impl<'c> std::ops::Deref for #individual_struct_name<'c> { + type Target = #typed_name<'c>; + + fn deref(&self) -> &Self::Target { + &self.typed + } + } + + impl std::ops::DerefMut for #individual_struct_name<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.typed + } + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +struct SignalParams { + /// `name: Type, ...` + param_list: TokenStream, + + /// `Type, ...` -- for example inside a tuple type. + type_list: TokenStream, + + /// `name, ...` -- for example inside a tuple value. + name_list: TokenStream, + + /// `"name: Type, ..."` in nice format. + formatted_types: String, +} + +impl SignalParams { + fn new(params: &[FnParam]) -> Self { + use std::fmt::Write; + + let mut param_list = TokenStream::new(); + let mut type_list = TokenStream::new(); + let mut name_list = TokenStream::new(); + let mut formatted_types = String::new(); + let mut first = true; + + for param in params.iter() { + let param_name = safe_ident(¶m.name.to_string()); + let param_ty = ¶m.type_; + + param_list.extend(quote! { #param_name: #param_ty, }); + type_list.extend(quote! { #param_ty, }); + name_list.extend(quote! { #param_name, }); + + let formatted_ty = match param_ty { + RustTy::EngineClass { inner_class, .. } => format!("Gd<{inner_class}>"), + other => other.to_string(), + }; + + if first { + first = false; + } else { + write!(formatted_types, ", ").unwrap(); + } + + write!(formatted_types, "{}: {}", param_name, formatted_ty).unwrap(); + } + + Self { + param_list, + type_list, + name_list, + formatted_types, + } + } +} diff --git a/godot-codegen/src/models/domain.rs b/godot-codegen/src/models/domain.rs index 7e742bbfb..52f761216 100644 --- a/godot-codegen/src/models/domain.rs +++ b/godot-codegen/src/models/domain.rs @@ -166,6 +166,7 @@ pub struct Class { pub constants: Vec, pub enums: Vec, pub methods: Vec, + pub signals: Vec, } impl ClassLike for Class { @@ -414,8 +415,6 @@ pub struct ClassMethod { pub surrounding_class: TyName, } -impl ClassMethod {} - impl Function for ClassMethod { fn common(&self) -> &FunctionCommon { &self.common @@ -443,6 +442,14 @@ impl fmt::Display for ClassMethod { // ---------------------------------------------------------------------------------------------------------------------------------------------- +pub struct ClassSignal { + pub name: String, + pub parameters: Vec, + pub surrounding_class: TyName, +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + #[derive(Copy, Clone, Debug)] pub enum FnDirection { /// Godot -> Rust. diff --git a/godot-codegen/src/models/domain_mapping.rs b/godot-codegen/src/models/domain_mapping.rs index 3c884f9ac..306f28efb 100644 --- a/godot-codegen/src/models/domain_mapping.rs +++ b/godot-codegen/src/models/domain_mapping.rs @@ -8,14 +8,16 @@ use crate::context::Context; use crate::models::domain::{ BuildConfiguration, BuiltinClass, BuiltinMethod, BuiltinSize, BuiltinVariant, Class, - ClassCommons, ClassConstant, ClassConstantValue, ClassMethod, Constructor, Enum, Enumerator, - EnumeratorValue, ExtensionApi, FnDirection, FnParam, FnQualifier, FnReturn, FunctionCommon, - GodotApiVersion, ModName, NativeStructure, Operator, Singleton, TyName, UtilityFunction, + ClassCommons, ClassConstant, ClassConstantValue, ClassMethod, ClassSignal, Constructor, Enum, + Enumerator, EnumeratorValue, ExtensionApi, FnDirection, FnParam, FnQualifier, FnReturn, + FunctionCommon, GodotApiVersion, ModName, NativeStructure, Operator, Singleton, TyName, + UtilityFunction, }; use crate::models::json::{ JsonBuiltinClass, JsonBuiltinMethod, JsonBuiltinSizes, JsonClass, JsonClassConstant, JsonClassMethod, JsonConstructor, JsonEnum, JsonEnumConstant, JsonExtensionApi, JsonHeader, - JsonMethodReturn, JsonNativeStructure, JsonOperator, JsonSingleton, JsonUtilityFunction, + JsonMethodReturn, JsonNativeStructure, JsonOperator, JsonSignal, JsonSingleton, + JsonUtilityFunction, }; use crate::util::{get_api_level, ident, option_as_slice}; use crate::{conv, special_cases}; @@ -113,6 +115,14 @@ impl Class { }) .collect(); + let signals = option_as_slice(&json.signals) + .iter() + .filter_map(|s| { + let surrounding_class = &ty_name; + ClassSignal::from_json(s, surrounding_class, ctx) + }) + .collect(); + Some(Self { common: ClassCommons { name: ty_name, @@ -126,6 +136,7 @@ impl Class { constants, enums, methods, + signals, }) } } @@ -513,6 +524,24 @@ impl ClassMethod { } } +impl ClassSignal { + pub fn from_json( + json_signal: &JsonSignal, + surrounding_class: &TyName, + ctx: &mut Context, + ) -> Option { + if special_cases::is_signal_deleted(surrounding_class, json_signal) { + return None; + } + + Some(Self { + name: json_signal.name.clone(), + parameters: FnParam::new_range(&json_signal.arguments, ctx), + surrounding_class: surrounding_class.clone(), + }) + } +} + impl UtilityFunction { pub fn from_json(function: &JsonUtilityFunction, ctx: &mut Context) -> Option { if special_cases::is_utility_function_deleted(function, ctx) { diff --git a/godot-codegen/src/models/json.rs b/godot-codegen/src/models/json.rs index 0bc9502b0..16f37ca5a 100644 --- a/godot-codegen/src/models/json.rs +++ b/godot-codegen/src/models/json.rs @@ -80,7 +80,7 @@ pub struct JsonClass { pub enums: Option>, pub methods: Option>, // pub properties: Option>, - // pub signals: Option>, + pub signals: Option>, } #[derive(DeJson)] @@ -179,10 +179,9 @@ pub struct JsonProperty { } #[derive(DeJson)] -#[allow(dead_code)] pub struct JsonSignal { - name: String, - arguments: Option>, + pub name: String, + pub arguments: Option>, } #[derive(DeJson)] diff --git a/godot-codegen/src/special_cases/special_cases.rs b/godot-codegen/src/special_cases/special_cases.rs index 58f1b08d8..d55916437 100644 --- a/godot-codegen/src/special_cases/special_cases.rs +++ b/godot-codegen/src/special_cases/special_cases.rs @@ -25,8 +25,9 @@ use crate::conv::to_enum_type_uncached; use crate::models::domain::{Enum, RustTy, TyName}; -use crate::models::json::{JsonBuiltinMethod, JsonClassMethod, JsonUtilityFunction}; +use crate::models::json::{JsonBuiltinMethod, JsonClassMethod, JsonSignal, JsonUtilityFunction}; use crate::special_cases::codegen_special_cases; +use crate::util::option_as_slice; use crate::Context; use proc_macro2::Ident; // Deliberately private -- all checks must go through `special_cases`. @@ -536,6 +537,14 @@ pub fn is_builtin_method_deleted(_class_name: &TyName, method: &JsonBuiltinMetho codegen_special_cases::is_builtin_method_excluded(method) } +/// True if signal is absent from codegen (only when surrounding class is excluded). +pub fn is_signal_deleted(_class_name: &TyName, signal: &JsonSignal) -> bool { + // If any argument type (a class) is excluded. + option_as_slice(&signal.arguments) + .iter() + .any(|arg| codegen_special_cases::is_class_excluded(&arg.type_)) +} + /// True if builtin type is excluded (`NIL` or scalars) pub fn is_builtin_type_deleted(class_name: &TyName) -> bool { let name = class_name.godot_ty.as_str(); diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index 7ea0a488f..418873f34 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -711,12 +711,17 @@ where /// Access user-defined signals of this object. /// /// For classes that have at least one `#[signal]` defined, returns a collection of signal names. Each returned signal has a specialized - /// API for connecting and emitting signals in a type-safe way. This method is the equivalent of [`WithSignals::signals()`], but when - /// called externally (not from `self`). If you are within the `impl` of a class, use `self.signals()` directly instead. + /// API for connecting and emitting signals in a type-safe way. This method is the equivalent of [`WithUserSignals::signals()`], but when + /// called externally (not from `self`). Furthermore, this is also available for engine classes, not just user-defined ones. + /// + /// When you are within the `impl` of a class, use `self.signals()` directly instead. /// /// If you haven't already, read the [book chapter about signals](https://godot-rust.github.io/book/register/signals.html) for a /// walkthrough. - pub fn signals(&self) -> T::SignalCollection<'_> { + /// + /// [`WithUserSignals::signals()`]: crate::obj::WithUserSignals::signals() + #[cfg(since_api = "4.2")] + pub fn signals(&mut self) -> T::SignalCollection<'_> { T::__signals_from_external(self) } } diff --git a/godot-core/src/obj/traits.rs b/godot-core/src/obj/traits.rs index 02e72fcca..b37fd3a12 100644 --- a/godot-core/src/obj/traits.rs +++ b/godot-core/src/obj/traits.rs @@ -11,7 +11,6 @@ use crate::init::InitLevel; use crate::meta::ClassName; use crate::obj::{bounds, Base, BaseMut, BaseRef, Bounds, Gd}; use crate::storage::Storage; - use godot_ffi as sys; /// Makes `T` eligible to be managed by Godot and stored in [`Gd`][crate::obj::Gd] pointers. @@ -430,9 +429,41 @@ pub trait WithBaseField: GodotClass + Bounds { } } -pub trait WithSignals: WithBaseField { - type SignalCollection<'a>; +// Dummy traits to still allow bounds and imports. +#[cfg(before_api = "4.2")] +pub trait WithSignals: GodotClass {} +#[cfg(before_api = "4.2")] +pub trait WithUserSignals: WithSignals + WithBaseField {} + +/// Implemented for all classes with registered signals, both engine- and user-declared. +/// +/// This trait enables the [`Gd::signals()`] method. +/// +/// User-defined classes with `#[signal]` additionally implement [`WithUserSignals`]. +#[cfg(since_api = "4.2")] +pub trait WithSignals: GodotClass { + /// The associated struct listing all signals of this class. + /// + /// `'c` denotes the lifetime during which the class instance is borrowed and its signals can be modified. + type SignalCollection<'c>; + + /// Trait that allows [`TypedSignal`] to store a reference to the user object. + #[doc(hidden)] + #[expect(private_bounds)] + type __SignalObject<'c>: crate::registry::signal::SignalObj; + + /// Create from existing `Gd`, to enable `Gd::signals()`. + /// + /// Takes by reference and not value, to retain lifetime chain. + #[doc(hidden)] + fn __signals_from_external(external: &mut Gd) -> Self::SignalCollection<'_>; +} +/// Implemented for user-defined classes with at least one `#[signal]` declaration. +/// +/// Allows to access signals from within the class, as `self.signals()`. This requires a `Base` field. +#[cfg(since_api = "4.2")] +pub trait WithUserSignals: WithSignals + WithBaseField { /// Access user-defined signals of the current object `self`. /// /// For classes that have at least one `#[signal]` defined, returns a collection of signal names. Each returned signal has a specialized @@ -459,9 +490,6 @@ pub trait WithSignals: WithBaseField { /// | `emit(amount: i32)` | Emits the signal with the given arguments. | /// fn signals(&mut self) -> Self::SignalCollection<'_>; - - #[doc(hidden)] - fn __signals_from_external(external: &Gd) -> Self::SignalCollection<'_>; } /// Extension trait for all reference-counted classes. diff --git a/godot-core/src/registry/signal/connect_builder.rs b/godot-core/src/registry/signal/connect_builder.rs index f27477c60..11fcaff6e 100644 --- a/godot-core/src/registry/signal/connect_builder.rs +++ b/godot-core/src/registry/signal/connect_builder.rs @@ -8,7 +8,7 @@ use crate::builtin::{Callable, GString, Variant}; use crate::classes::object::ConnectFlags; use crate::meta; -use crate::obj::{bounds, Bounds, Gd, GodotClass, WithBaseField}; +use crate::obj::{bounds, Bounds, Gd, GodotClass, WithSignals}; use crate::registry::signal::{SignalReceiver, TypedSignal}; /// Type-state builder for customizing signal connections. @@ -54,7 +54,7 @@ use crate::registry::signal::{SignalReceiver, TypedSignal}; /// - [`done`][Self::done]: Finalize the connection. Consumes the builder and registers the signal with Godot. /// #[must_use] -pub struct ConnectBuilder<'ts, 'c, CSig: GodotClass, CRcv, Ps, GodotFn> { +pub struct ConnectBuilder<'ts, 'c, CSig: WithSignals, CRcv, Ps, GodotFn> { parent_sig: &'ts mut TypedSignal<'c, CSig, Ps>, data: BuilderData, @@ -63,7 +63,7 @@ pub struct ConnectBuilder<'ts, 'c, CSig: GodotClass, CRcv, Ps, GodotFn> { godot_fn: GodotFn, } -impl<'ts, 'c, CSig: WithBaseField, Ps: meta::ParamTuple> ConnectBuilder<'ts, 'c, CSig, (), Ps, ()> { +impl<'ts, 'c, CSig: WithSignals, Ps: meta::ParamTuple> ConnectBuilder<'ts, 'c, CSig, (), Ps, ()> { pub(super) fn new(parent_sig: &'ts mut TypedSignal<'c, CSig, Ps>) -> Self { ConnectBuilder { parent_sig, @@ -126,7 +126,7 @@ impl<'ts, 'c, CSig: WithBaseField, Ps: meta::ParamTuple> ConnectBuilder<'ts, 'c, } } -impl<'ts, 'c, CSig: WithBaseField, CRcv: GodotClass, Ps: meta::ParamTuple> +impl<'ts, 'c, CSig: WithSignals, CRcv: GodotClass, Ps: meta::ParamTuple> ConnectBuilder<'ts, 'c, CSig, Gd, Ps, ()> { /// **Stage 2:** method taking `&mut self`. @@ -197,7 +197,7 @@ impl<'ts, 'c, CSig: WithBaseField, CRcv: GodotClass, Ps: meta::ParamTuple> #[allow(clippy::needless_lifetimes)] // 'ts + 'c are used conditionally. impl<'ts, 'c, CSig, CRcv, Ps, GodotFn> ConnectBuilder<'ts, 'c, CSig, CRcv, Ps, GodotFn> where - CSig: WithBaseField, + CSig: WithSignals, Ps: meta::ParamTuple, GodotFn: FnMut(&[&Variant]) -> Result + 'static, { diff --git a/godot-core/src/registry/signal/typed_signal.rs b/godot-core/src/registry/signal/typed_signal.rs index fe44435e4..aea4a657d 100644 --- a/godot-core/src/registry/signal/typed_signal.rs +++ b/godot-core/src/registry/signal/typed_signal.rs @@ -7,15 +7,25 @@ use crate::builtin::{Callable, Variant}; use crate::classes::object::ConnectFlags; -use crate::obj::{bounds, Bounds, Gd, GodotClass, WithBaseField}; +use crate::obj::{bounds, Bounds, Gd, GodotClass, WithBaseField, WithSignals, WithUserSignals}; use crate::registry::signal::{make_callable_name, make_godot_fn, ConnectBuilder, SignalReceiver}; use crate::{classes, meta}; use std::borrow::Cow; use std::marker::PhantomData; +/// Indirection from [`TypedSignal`] to the actual Godot object. +/// +/// Needs to differentiate the two cases: +/// - `C` is a user object implementing `WithBaseField`, possibly having access from within the class. +/// - `C` is an engine object, so only accessible through `Gd`. +pub(crate) trait SignalObj { + fn with_object_mut(&mut self, f: impl FnOnce(&mut classes::Object)); + fn to_owned_object(&self) -> Gd; +} + /// Links to a Godot object, either via reference (for `&mut self` uses) or via `Gd`. #[doc(hidden)] -pub enum ObjectRef<'a, C: GodotClass> { +pub enum UserSignalObj<'a, C: GodotClass> { /// Helpful for emit: reuse `&mut self` from within the `impl` block, goes through `base_mut()` re-borrowing and thus allows re-entrant calls /// through Godot. Internal { obj_mut: &'a mut C }, @@ -24,25 +34,32 @@ pub enum ObjectRef<'a, C: GodotClass> { External { gd: Gd }, } -impl ObjectRef<'_, C> -where - C: WithBaseField, -{ +impl SignalObj for UserSignalObj<'_, C> { fn with_object_mut(&mut self, f: impl FnOnce(&mut classes::Object)) { match self { - ObjectRef::Internal { obj_mut } => f(obj_mut.base_mut().upcast_object_mut()), - ObjectRef::External { gd } => f(gd.upcast_object_mut()), + UserSignalObj::Internal { obj_mut } => f(obj_mut.base_mut().upcast_object_mut()), + UserSignalObj::External { gd } => f(gd.upcast_object_mut()), } } - fn to_owned(&self) -> Gd { + fn to_owned_object(&self) -> Gd { match self { - ObjectRef::Internal { obj_mut } => WithBaseField::to_gd(*obj_mut), - ObjectRef::External { gd } => gd.clone(), + UserSignalObj::Internal { obj_mut } => WithBaseField::to_gd(*obj_mut), + UserSignalObj::External { gd } => gd.clone(), } } } +impl SignalObj for Gd { + fn with_object_mut(&mut self, f: impl FnOnce(&mut classes::Object)) { + f(self.upcast_object_mut()); + } + + fn to_owned_object(&self) -> Gd { + self.clone() + } +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- /// Type-safe version of a Godot signal. @@ -50,9 +67,9 @@ where /// Short-lived type, only valid in the scope of its surrounding object type `C`, for lifetime `'c`. The generic argument `Ps` represents /// the parameters of the signal, thus ensuring the type safety. /// -/// The [`WithSignals::signals()`][crate::obj::WithSignals::signals] collection returns multiple signals with distinct, code-generated types, -/// but they all implement `Deref` and `DerefMut` to `TypedSignal`. This allows you to either use the concrete APIs of the generated types, -/// or the more generic ones of `TypedSignal`. +/// The [`WithSignals::SignalCollection`] struct returns multiple signals with distinct, code-generated types, but they all implement +/// `Deref` and `DerefMut` to `TypedSignal`. This allows you to either use the concrete APIs of the generated types, or the more generic +/// ones of `TypedSignal`. /// /// # Connecting a signal to a receiver /// Receiver functions are functions that are called when a signal is emitted. You can connect a signal in many different ways: @@ -69,16 +86,16 @@ where /// /// # More information /// See the [Signals](https://godot-rust.github.io/book/register/signals.html) chapter in the book for a detailed introduction and examples. -pub struct TypedSignal<'c, C: GodotClass, Ps> { +pub struct TypedSignal<'c, C: WithSignals, Ps> { /// In Godot, valid signals (unlike funcs) are _always_ declared in a class and become part of each instance. So there's always an object. - owner: ObjectRef<'c, C>, + owner: C::__SignalObject<'c>, name: Cow<'static, str>, _signature: PhantomData, } -impl<'c, C: WithBaseField, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> { +impl<'c, C: WithSignals, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> { #[doc(hidden)] - pub fn new(owner: ObjectRef<'c, C>, name: &'static str) -> Self { + pub fn new(owner: C::__SignalObject<'c>, name: &'static str) -> Self { Self { owner, name: Cow::Borrowed(name), @@ -87,7 +104,7 @@ impl<'c, C: WithBaseField, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> { } pub(crate) fn receiver_object(&self) -> Gd { - self.owner.to_owned() + self.owner.to_owned_object() } /// Emit the signal with the given parameters. @@ -124,24 +141,6 @@ impl<'c, C: WithBaseField, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> { self.inner_connect_godot_fn::(godot_fn); } - /// Connect a method (member function) with `&mut self` as the first parameter. - /// - /// To connect to methods on other objects, use [`connect_obj()`][Self::connect_obj]. \ - /// If you need a `&self` receiver, cross-thread signals or connect flags, use [`connect_builder()`][Self::connect_builder]. - pub fn connect_self(&mut self, mut function: F) - where - for<'c_rcv> F: SignalReceiver<&'c_rcv mut C, Ps>, - { - let mut gd = self.owner.to_owned(); - let godot_fn = make_godot_fn(move |args| { - let mut instance = gd.bind_mut(); - let instance = &mut *instance; - function.call(instance, args); - }); - - self.inner_connect_godot_fn::(godot_fn); - } - /// Connect a method (member function) with any `Gd` (not `self`) as the first parameter. /// /// To connect to methods on the same object that declares the `#[signal]`, use [`connect_self()`][Self::connect_self]. \ @@ -212,3 +211,23 @@ impl<'c, C: WithBaseField, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> { crate::builtin::Signal::from_object_signal(&self.receiver_object(), &*self.name) } } + +impl TypedSignal<'_, C, Ps> { + /// Connect a method (member function) with `&mut self` as the first parameter. + /// + /// To connect to methods on other objects, use [`connect_obj()`][Self::connect_obj]. \ + /// If you need a `&self` receiver, cross-thread signals or connect flags, use [`connect_builder()`][Self::connect_builder]. + pub fn connect_self(&mut self, mut function: F) + where + for<'c_rcv> F: SignalReceiver<&'c_rcv mut C, Ps>, + { + let mut gd = self.owner.to_owned_object(); + let godot_fn = make_godot_fn(move |args| { + let mut instance = gd.bind_mut(); + let instance = &mut *instance; + function.call(instance, args); + }); + + self.inner_connect_godot_fn::(godot_fn); + } +} diff --git a/godot-core/src/task/futures.rs b/godot-core/src/task/futures.rs index 6a49da027..fdad85056 100644 --- a/godot-core/src/task/futures.rs +++ b/godot-core/src/task/futures.rs @@ -17,7 +17,7 @@ use crate::builtin::{Callable, RustCallable, Signal, Variant}; use crate::classes::object::ConnectFlags; use crate::meta::sealed::Sealed; use crate::meta::ParamTuple; -use crate::obj::{EngineBitfield, Gd, GodotClass, WithBaseField}; +use crate::obj::{EngineBitfield, Gd, GodotClass, WithSignals}; use crate::registry::signal::TypedSignal; pub(crate) use crate::impl_dynamic_send; @@ -307,7 +307,7 @@ impl Signal { } } -impl TypedSignal<'_, C, R> { +impl TypedSignal<'_, C, R> { /// Creates a fallible future for this signal. /// /// The future will resolve the next time the signal is emitted. @@ -325,7 +325,7 @@ impl TypedSignal<'_, C, R> { } } -impl IntoFuture for &TypedSignal<'_, C, R> { +impl IntoFuture for &TypedSignal<'_, C, R> { type Output = R; type IntoFuture = SignalFuture; diff --git a/godot-macros/src/class/data_models/signal.rs b/godot-macros/src/class/data_models/signal.rs index db1e4e20b..499f5efa7 100644 --- a/godot-macros/src/class/data_models/signal.rs +++ b/godot-macros/src/class/data_models/signal.rs @@ -5,6 +5,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +// Some duplication with godot-codegen/signals.rs; see comments there. + use crate::util::bail; use crate::{util, ParseResult}; use proc_macro2::{Ident, TokenStream}; @@ -186,7 +188,7 @@ fn make_signal_registration(details: &SignalDetails, class_name_obj: &TokenStrea #[derive(Default)] struct SignalCollection { /// The individual `my_signal()` accessors, returning concrete signal types. - collection_methods: Vec, + provider_methods: Vec, /// The actual signal definitions, including both `struct` and `impl` blocks. individual_structs: Vec, @@ -203,7 +205,7 @@ impl SignalCollection { .. } = details; - self.collection_methods.push(quote! { + self.provider_methods.push(quote! { // Deliberately not #[doc(hidden)] for IDE completion. #(#signal_cfg_attrs)* // Note: this could be `pub` always and would still compile (maybe warning with the following message). @@ -211,7 +213,7 @@ impl SignalCollection { // // However, it would still lead to a compile error when declaring the individual signal struct `pub` (or any other // visibility that exceeds the class visibility). So, we can as well declare the visibility here. - #vis_marker fn #signal_name(self) -> #individual_struct_name<'a> { + #vis_marker fn #signal_name(self) -> #individual_struct_name<'c> { #individual_struct_name { typed: ::godot::register::TypedSignal::new(self.__internal_obj, #signal_name_str) } @@ -272,8 +274,8 @@ fn make_signal_individual_struct(details: &SignalDetails) -> TokenStream { } #(#signal_cfg_attrs)* - impl<'a> std::ops::Deref for #individual_struct_name<'a> { - type Target = ::godot::register::TypedSignal<'a, #class_name, #param_tuple>; + impl<'c> std::ops::Deref for #individual_struct_name<'c> { + type Target = ::godot::register::TypedSignal<'c, #class_name, #param_tuple>; fn deref(&self) -> &Self::Target { &self.typed @@ -296,35 +298,39 @@ fn make_signal_collection(class_name: &Ident, collection: SignalCollection) -> O } let collection_struct_name = format_ident!("__godot_Signals_{}", class_name); - let collection_struct_methods = &collection.collection_methods; + let collection_struct_methods = &collection.provider_methods; let individual_structs = collection.individual_structs; let code = quote! { #[allow(non_camel_case_types)] #[doc(hidden)] // Only on struct, not methods, to allow completion in IDEs. - pub struct #collection_struct_name<'a> { + pub struct #collection_struct_name<'c> { // To allow external call in the future (given Gd, not self), this could be an enum with either BaseMut or &mut Gd/&mut T. #[doc(hidden)] // Necessary because it's in the same scope as the user-defined class, so appearing in IDE completion. - __internal_obj: ::godot::register::ObjectRef<'a, #class_name> + __internal_obj: ::godot::register::UserSignalObj<'c, #class_name> } - impl<'a> #collection_struct_name<'a> { + impl<'c> #collection_struct_name<'c> { #( #collection_struct_methods )* } impl ::godot::obj::WithSignals for #class_name { - type SignalCollection<'a> = #collection_struct_name<'a>; + type SignalCollection<'c> = #collection_struct_name<'c>; + #[doc(hidden)] + type __SignalObject<'c> = ::godot::register::UserSignalObj<'c, Self>; - fn signals(&mut self) -> Self::SignalCollection<'_> { + #[doc(hidden)] + fn __signals_from_external(external: &mut Gd) -> Self::SignalCollection<'_> { Self::SignalCollection { - __internal_obj: ::godot::register::ObjectRef::Internal { obj_mut: self } + __internal_obj: ::godot::register::UserSignalObj::External { gd: external.clone() } } } + } - #[doc(hidden)] - fn __signals_from_external(external: &Gd) -> Self::SignalCollection<'_> { + impl ::godot::obj::WithUserSignals for #class_name { + fn signals(&mut self) -> Self::SignalCollection<'_> { Self::SignalCollection { - __internal_obj: ::godot::register::ObjectRef::External { gd: external.clone() } + __internal_obj: ::godot::register::UserSignalObj::Internal { obj_mut: self } } } } diff --git a/godot/src/prelude.rs b/godot/src/prelude.rs index 904ec3d39..fea22fceb 100644 --- a/godot/src/prelude.rs +++ b/godot/src/prelude.rs @@ -36,4 +36,5 @@ pub use super::obj::EngineEnum as _; pub use super::obj::NewAlloc as _; pub use super::obj::NewGd as _; pub use super::obj::WithBaseField as _; // base(), base_mut(), to_gd() -pub use super::obj::WithSignals as _; // signals() +pub use super::obj::WithSignals as _; // Gd::signals() +pub use super::obj::WithUserSignals as _; // self.signals() diff --git a/itest/rust/src/builtin_tests/containers/signal_test.rs b/itest/rust/src/builtin_tests/containers/signal_test.rs index 1d11a6547..edb2def6d 100644 --- a/itest/rust/src/builtin_tests/containers/signal_test.rs +++ b/itest/rust/src/builtin_tests/containers/signal_test.rs @@ -7,13 +7,13 @@ use crate::framework::itest; use godot::builtin::{GString, Signal, StringName}; -use godot::classes::{Object, RefCounted}; +use godot::classes::{Node, Object, RefCounted}; use godot::meta::ToGodot; -use godot::obj::{Base, Gd, InstanceId, NewAlloc, NewGd, WithSignals}; +use godot::obj::{Base, Gd, InstanceId, NewAlloc, NewGd}; use godot::register::{godot_api, GodotClass}; use godot::sys; use godot::sys::Global; -use std::cell::Cell; +use std::cell::{Cell, RefCell}; use std::rc::Rc; #[itest] @@ -79,7 +79,7 @@ fn signal_symbols_internal() { #[cfg(since_api = "4.2")] #[itest] fn signal_symbols_external() { - let emitter = Emitter::new_alloc(); + let mut emitter = Emitter::new_alloc(); let mut sig = emitter.signals().signal_int(); // Local function; deliberately use a !Send type. @@ -126,7 +126,7 @@ fn signal_symbols_external() { #[cfg(since_api = "4.2")] #[itest] fn signal_symbols_external_builder() { - let emitter = Emitter::new_alloc(); + let mut emitter = Emitter::new_alloc(); let mut sig = emitter.signals().signal_int(); // Self-modifying method. @@ -197,7 +197,7 @@ fn signal_symbols_external_builder() { fn signal_symbols_sync() { use std::sync::{Arc, Mutex}; - let emitter = Emitter::new_alloc(); + let mut emitter = Emitter::new_alloc(); let mut sig = emitter.signals().signal_int(); let sync_tracker = Arc::new(Mutex::new(0)); @@ -219,6 +219,56 @@ fn signal_symbols_sync() { emitter.free(); } +#[cfg(since_api = "4.2")] +#[itest] +fn signal_symbols_engine(ctx: &crate::framework::TestContext) { + // Add node to tree, to test Godot signal interactions. + let mut node = Node::new_alloc(); + ctx.scene_tree.clone().add_child(&node); + + // Deliberately declare here, because there was a bug with wrong lifetime, which would not compile due to early-dropped temporary. + let mut signals_in_node = node.signals(); + let mut renamed = signals_in_node.renamed(); + let mut entered = signals_in_node.child_entered_tree(); + + let renamed_count = Rc::new(Cell::new(0)); + let entered_tracker = Rc::new(RefCell::new(None)); + { + let renamed_count = renamed_count.clone(); + let entered_tracker = entered_tracker.clone(); + + entered + .connect_builder() + .function(move |node| { + *entered_tracker.borrow_mut() = Some(node); + }) + .done(); + + renamed.connect(move || renamed_count.set(renamed_count.get() + 1)); + } + + // Apply changes, triggering signals. + node.set_name("new name"); + let child = Node::new_alloc(); + node.add_child(&child); + + // Verify that signals were emitted. + let entered_node = entered_tracker.take(); + assert_eq!(renamed_count.get(), 1, "Emit failed: Node::renamed"); + assert_eq!( + entered_node, + Some(child), + "Emit failed: Node::child_entered_tree" + ); + + // Manually emit a signal a 2nd time. + node.signals().renamed().emit(); + assert_eq!(renamed_count.get(), 2, "Manual emit failed: Node::renamed"); + + // Remove from tree for other tests. + node.free(); +} + #[itest] fn signal_construction_and_id() { let mut object = RefCounted::new_gd(); @@ -250,6 +300,7 @@ use emitter::Emitter; mod emitter { use super::*; + use godot::obj::WithUserSignals; #[derive(GodotClass)] #[class(init, base=Object)] @@ -319,6 +370,7 @@ struct Receiver { last_received: Cell, base: Base, } + #[godot_api] impl Receiver { fn last_received(&self) -> LastReceived { diff --git a/itest/rust/src/engine_tests/async_test.rs b/itest/rust/src/engine_tests/async_test.rs index 3985abf2c..5f6d3c7db 100644 --- a/itest/rust/src/engine_tests/async_test.rs +++ b/itest/rust/src/engine_tests/async_test.rs @@ -177,16 +177,16 @@ fn resolver_callabable_equality() { #[itest(async)] fn async_typed_signal() -> TaskHandle { - let object = AsyncRefCounted::new_gd(); - let object_ref = object.clone(); + let mut object = AsyncRefCounted::new_gd(); + let mut copy = object.clone(); let task_handle = task::spawn(async move { - let (result,) = object.signals().custom_signal().deref().await; + let (result,) = copy.signals().custom_signal().deref().await; assert_eq!(result, 66); }); - object_ref.signals().custom_signal().emit(66); + object.signals().custom_signal().emit(66); task_handle }