Skip to content

Allow disconnection of type-safe signals #1198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 22 additions & 14 deletions godot-core/src/registry/signal/connect_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::meta;
use crate::meta::InParamTuple;
use crate::obj::{bounds, Bounds, Gd, GodotClass, WithSignals};
use crate::registry::signal::signal_receiver::{IndirectSignalReceiver, SignalReceiver};
use crate::registry::signal::{ToSignalObj, TypedSignal};
use crate::registry::signal::{ConnectHandle, ToSignalObj, TypedSignal};

/// Builder for customizing signal connections.
///
Expand Down Expand Up @@ -125,15 +125,15 @@ where
fn inner_connect_godot_fn<F>(
self,
godot_fn: impl FnMut(&[&Variant]) -> Result<Variant, ()> + 'static,
) {
) -> ConnectHandle {
let callable_name = match &self.data.callable_name {
Some(user_provided_name) => user_provided_name,
None => &make_callable_name::<F>(),
};

let callable = Callable::from_local_fn(callable_name, godot_fn);
self.parent_sig
.inner_connect_untyped(&callable, self.data.connect_flags);
.inner_connect_untyped(callable, self.data.connect_flags)
}
}

Expand All @@ -154,7 +154,7 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> ConnectBuilder<'_, '_, C, Ps> {
/// [`connect_other_gd()`][Self::connect_other_gd].
/// - If you need [`connect flags`](ConnectFlags), call [`flags()`](Self::flags) before this.
/// - If you need cross-thread signals, use [`connect_sync()`](#method.connect_sync) instead (requires feature "experimental-threads").
pub fn connect<F>(self, mut function: F)
pub fn connect<F>(self, mut function: F) -> ConnectHandle
where
for<'c_rcv> F: SignalReceiver<(), Ps>,
for<'c_rcv> IndirectSignalReceiver<'c_rcv, (), Ps, F>: From<&'c_rcv mut F>,
Expand All @@ -165,7 +165,7 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> ConnectBuilder<'_, '_, C, Ps> {
.call((), args);
});

self.inner_connect_godot_fn::<F>(godot_fn);
self.inner_connect_godot_fn::<F>(godot_fn)
}

/// Connect a method with `&mut self` as the first parameter (user classes only).
Expand All @@ -176,7 +176,7 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> ConnectBuilder<'_, '_, C, Ps> {
/// - To connect to methods on other objects, use [`connect_other_mut()`][Self::connect_other_mut].
/// - If you need [connect flags](ConnectFlags), call [`flags()`](Self::flags) before this.
/// - If you need cross-thread signals, use [`connect_sync()`](#method.connect_sync) instead (requires feature `experimental-threads`).
pub fn connect_self_mut<F>(self, mut function: F)
pub fn connect_self_mut<F>(self, mut function: F) -> ConnectHandle
where
C: Bounds<Declarer = bounds::DeclUser>,
for<'c_rcv> F: SignalReceiver<&'c_rcv mut C, Ps>,
Expand All @@ -191,7 +191,7 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> ConnectBuilder<'_, '_, C, Ps> {
.call(&mut *guard, args);
});

self.inner_connect_godot_fn::<F>(godot_fn);
self.inner_connect_godot_fn::<F>(godot_fn)
}

/// Connect a method with `&mut Gd<Self>` as the first parameter (user + engine classes).
Expand All @@ -202,7 +202,7 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> ConnectBuilder<'_, '_, C, Ps> {
/// - To connect to methods on other objects, use [`connect_other_gd()`][Self::connect_other_gd].
/// - If you need [connect flags](ConnectFlags), call [`flags()`](Self::flags) before this.
/// - If you need cross-thread signals, use [`connect_sync()`](#method.connect_sync) instead (requires feature `experimental-threads`).
pub fn connect_self_gd<F>(self, mut function: F)
pub fn connect_self_gd<F>(self, mut function: F) -> ConnectHandle
where
F: SignalReceiver<Gd<C>, Ps>,
for<'c_rcv> IndirectSignalReceiver<'c_rcv, Gd<C>, Ps, F>: From<&'c_rcv mut F>,
Expand All @@ -215,7 +215,7 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> ConnectBuilder<'_, '_, C, Ps> {
.call(gd.clone(), args);
});

self.inner_connect_godot_fn::<F>(godot_fn);
self.inner_connect_godot_fn::<F>(godot_fn)
}

/// Connect a method with any `&mut OtherC` as the first parameter (user classes only).
Expand All @@ -231,7 +231,11 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> ConnectBuilder<'_, '_, C, Ps> {
/// - To connect to methods on the object that owns this signal, use [`connect_self_mut()`][Self::connect_self_mut].
/// - If you need [connect flags](ConnectFlags), call [`flags()`](Self::flags) before this.
/// - If you need cross-thread signals, use [`connect_sync()`](#method.connect_sync) instead (requires feature "experimental-threads").
pub fn connect_other_mut<F, OtherC>(self, object: &impl ToSignalObj<OtherC>, mut method: F)
pub fn connect_other_mut<F, OtherC>(
self,
object: &impl ToSignalObj<OtherC>,
mut method: F,
) -> ConnectHandle
where
OtherC: GodotClass + Bounds<Declarer = bounds::DeclUser>,
for<'c_rcv> F: SignalReceiver<&'c_rcv mut OtherC, Ps>,
Expand All @@ -246,7 +250,7 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> ConnectBuilder<'_, '_, C, Ps> {
.call(&mut *guard, args);
});

self.inner_connect_godot_fn::<F>(godot_fn);
self.inner_connect_godot_fn::<F>(godot_fn)
}

/// Connect a method with any `&mut Gd<OtherC>` as the first parameter (user + engine classes).
Expand All @@ -260,7 +264,11 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> ConnectBuilder<'_, '_, C, Ps> {
/// - To connect to methods on the object that owns this signal, use [`connect_self_gd()`][Self::connect_self_gd].
/// - If you need [connect flags](ConnectFlags), call [`flags()`](Self::flags) before this.
/// - If you need cross-thread signals, use [`connect_sync()`](#method.connect_sync) instead (requires feature "experimental-threads").
pub fn connect_other_gd<F, OtherC>(self, object: &impl ToSignalObj<OtherC>, mut method: F)
pub fn connect_other_gd<F, OtherC>(
self,
object: &impl ToSignalObj<OtherC>,
mut method: F,
) -> ConnectHandle
where
OtherC: GodotClass,
F: SignalReceiver<Gd<OtherC>, Ps>,
Expand All @@ -274,7 +282,7 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> ConnectBuilder<'_, '_, C, Ps> {
.call(gd.clone(), args);
});

self.inner_connect_godot_fn::<F>(godot_fn);
self.inner_connect_godot_fn::<F>(godot_fn)
}

/// Connect to this signal using a thread-safe function, allows the signal to be called across threads.
Expand Down Expand Up @@ -304,6 +312,6 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> ConnectBuilder<'_, '_, C, Ps> {

let callable = Callable::from_sync_fn(callable_name, godot_fn);
self.parent_sig
.inner_connect_untyped(&callable, self.data.connect_flags);
.inner_connect_untyped(callable, self.data.connect_flags);
}
}
60 changes: 60 additions & 0 deletions godot-core/src/registry/signal/connect_handle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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/.
*/

use std::borrow::Cow;

use crate::builtin::Callable;
use crate::classes::Object;
use crate::obj::Gd;

/// Handle representing a typed signal connection to a receiver.
///
/// Returned by connections made by the `connect_*` methods of
/// [`TypedSignal`][crate::registry::signal::TypedSignal] and [`ConnectBuilder`][crate::registry::signal::ConnectBuilder].
///
/// Connections managed by a handle can be disconnected using [`disconnect()`][Self::disconnect].
pub struct ConnectHandle {
receiver_object: Gd<Object>,
signal_name: Cow<'static, str>,
callable: Callable,
}

impl ConnectHandle {
// Should only be invoked by connect_* methods.
pub(super) fn new(
receiver_object: Gd<Object>,
signal_name: Cow<'static, str>,
callable: Callable,
) -> Self {
Self {
receiver_object,
signal_name,
callable,
}
}

/// Disconnects the signal from the connected callable.
///
/// Panics (Debug)
/// If the connection does not exist. Use [`is_connected()`][Self::is_connected] to make sure the connection exists.
pub fn disconnect(mut self) {
debug_assert!(self.is_connected());

self.receiver_object
.disconnect(&*self.signal_name, &self.callable);
}

/// Whether the handle represents a valid connection.
///
/// Returns false if the signals and callables managed by this handle have been disconnected in any other way than by using
/// [`disconnect()`][Self::disconnect] -- e.g. through [`Signal::disconnect()`][crate::builtin::Signal::disconnect] or
/// [`Object::disconnect()`][crate::classes::Object::disconnect].
pub fn is_connected(&self) -> bool {
self.receiver_object
.is_connected(&*self.signal_name, &self.callable)
}
}
3 changes: 3 additions & 0 deletions godot-core/src/registry/signal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@
// Whole module only available in Godot 4.2+.

mod connect_builder;
mod connect_handle;
mod signal_object;
mod signal_receiver;
mod typed_signal;

use crate::builtin::{GString, Variant};
use crate::meta;
pub(crate) use connect_builder::*;
pub(crate) use connect_handle::*;
pub(crate) use signal_object::*;
pub(crate) use typed_signal::*;

// Used in `godot` crate.
pub mod re_export {
pub use super::connect_builder::ConnectBuilder;
pub use super::connect_handle::ConnectHandle;
pub use super::signal_receiver::IndirectSignalReceiver;
pub use super::signal_receiver::SignalReceiver;
pub use super::typed_signal::TypedSignal;
Expand Down
32 changes: 20 additions & 12 deletions godot-core/src/registry/signal/typed_signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

use super::{make_callable_name, make_godot_fn, ConnectBuilder, SignalObject};
use super::{make_callable_name, make_godot_fn, ConnectBuilder, ConnectHandle, SignalObject};
use crate::builtin::{Callable, Variant};
use crate::classes::object::ConnectFlags;
use crate::meta;
Expand Down Expand Up @@ -139,27 +139,34 @@ impl<'c, C: WithSignals, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> {
fn inner_connect_godot_fn<F>(
&self,
godot_fn: impl FnMut(&[&Variant]) -> Result<Variant, ()> + 'static,
) {
) -> ConnectHandle {
let callable_name = make_callable_name::<F>();
let callable = Callable::from_local_fn(&callable_name, godot_fn);
self.inner_connect_untyped(&callable, None);
self.inner_connect_untyped(callable, None)
}

/// Connect an untyped callable, with optional flags.
///
/// Used by [`inner_connect_godot_fn`] and `ConnectBuilder::connect_sync`.
pub(super) fn inner_connect_untyped(&self, callable: &Callable, flags: Option<ConnectFlags>) {
pub(super) fn inner_connect_untyped(
&self,
callable: Callable,
flags: Option<ConnectFlags>,
) -> ConnectHandle {
use crate::obj::EngineBitfield;

let signal_name = self.name.as_ref();

self.object.to_owned_object().with_object_mut(|obj| {
let mut c = obj.connect_ex(signal_name, callable);
let mut owned_object = self.object.to_owned_object();
owned_object.with_object_mut(|obj| {
let mut c = obj.connect_ex(signal_name, &callable);
if let Some(flags) = flags {
c = c.flags(flags.ord() as u32);
}
c.done();
});

ConnectHandle::new(owned_object, self.name.clone(), callable)
}

pub(crate) fn to_untyped(&self) -> crate::builtin::Signal {
Expand All @@ -179,7 +186,7 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> TypedSignal<'_, C, Ps> {
///
/// - To connect to a method on the object that owns this signal, use [`connect_self()`][Self::connect_self].
/// - If you need [`connect flags`](ConnectFlags) or cross-thread signals, use [`builder()`][Self::builder].
pub fn connect<F>(&self, mut function: F)
pub fn connect<F>(&self, mut function: F) -> ConnectHandle
where
for<'c_rcv> F: SignalReceiver<(), Ps> + 'static,
for<'c_rcv> IndirectSignalReceiver<'c_rcv, (), Ps, F>: From<&'c_rcv mut F>,
Expand All @@ -190,14 +197,14 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> TypedSignal<'_, C, Ps> {
.call((), args);
});

self.inner_connect_godot_fn::<F>(godot_fn);
self.inner_connect_godot_fn::<F>(godot_fn)
}

/// Connect a method (member function) with `&mut self` as the first parameter.
///
/// - To connect to methods on other objects, use [`connect_other()`][Self::connect_other].
/// - If you need [`connect flags`](ConnectFlags) or cross-thread signals, use [`builder()`][Self::builder].
pub fn connect_self<F, Declarer>(&self, mut function: F)
pub fn connect_self<F, Declarer>(&self, mut function: F) -> ConnectHandle
where
for<'c_rcv> F: SignalReceiver<&'c_rcv mut C, Ps> + 'static,
for<'c_rcv> IndirectSignalReceiver<'c_rcv, &'c_rcv mut C, Ps, F>: From<&'c_rcv mut F>,
Expand All @@ -212,7 +219,7 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> TypedSignal<'_, C, Ps> {
.call(target_mut, args);
});

self.inner_connect_godot_fn::<F>(godot_fn);
self.inner_connect_godot_fn::<F>(godot_fn)
}

/// Connect a method (member function) with any `&mut OtherC` as the first parameter, where
Expand All @@ -231,7 +238,8 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> TypedSignal<'_, C, Ps> {
&self,
object: &impl ToSignalObj<OtherC>,
mut method: F,
) where
) -> ConnectHandle
where
OtherC: UniformObjectDeref<Declarer>,
for<'c_rcv> F: SignalReceiver<&'c_rcv mut OtherC, Ps> + 'static,
for<'c_rcv> IndirectSignalReceiver<'c_rcv, &'c_rcv mut OtherC, Ps, F>: From<&'c_rcv mut F>,
Expand All @@ -246,6 +254,6 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> TypedSignal<'_, C, Ps> {
.call(target_mut, args);
});

self.inner_connect_godot_fn::<F>(godot_fn);
self.inner_connect_godot_fn::<F>(godot_fn)
}
}
Loading
Loading