diff --git a/godot-core/src/obj/call_deferred.rs b/godot-core/src/obj/call_deferred.rs new file mode 100644 index 000000000..eb03e59ab --- /dev/null +++ b/godot-core/src/obj/call_deferred.rs @@ -0,0 +1,74 @@ +/* + * 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 crate::builtin::{Callable, Variant}; +use crate::meta::UniformObjectDeref; +use crate::obj::bounds::Declarer; +use crate::obj::GodotClass; +#[cfg(since_api = "4.2")] +use crate::registry::signal::ToSignalObj; +use godot_ffi::is_main_thread; +use std::ops::DerefMut; + +// Dummy traits to still allow bounds and imports. +#[cfg(before_api = "4.2")] +pub trait WithDeferredCall {} + +/// Trait that is automatically implemented for engine classes and user classes containing a `Base` field. +/// +/// This trait enables type safe deferred method calls. +/// +/// # Usage +/// +/// ```no_run +/// # use godot::prelude::*; +/// # use std::f32::consts::PI; +/// fn some_fn(mut node: Gd) +/// { +/// node.apply_deferred(|shape_mut| shape_mut.rotate(PI)) +/// } +/// ``` +#[cfg(since_api = "4.2")] +pub trait WithDeferredCall { + /// Runs the given Closure deferred. + /// + /// This is a type-safe alternative to [`Object::call_deferred`][crate::classes::Object::call_deferred]. + /// + /// # Panics + /// If called outside the main thread. + fn apply_deferred(&mut self, rust_function: F) + where + F: FnOnce(&mut T) + 'static; +} + +#[cfg(since_api = "4.2")] +impl WithDeferredCall for S +where + T: UniformObjectDeref, + S: ToSignalObj, + D: Declarer, +{ + fn apply_deferred<'a, F>(&mut self, rust_function: F) + where + F: FnOnce(&mut T) + 'static, + { + assert!( + is_main_thread(), + "`apply_deferred` must be called on the main thread" + ); + let mut rust_fn_once = Some(rust_function); + let mut this = self.to_signal_obj().clone(); + let callable = Callable::from_local_fn("apply_deferred", move |_| { + let rust_fn_once = rust_fn_once + .take() + .expect("rust_fn_once was already consumed"); + let mut this_mut = T::object_as_mut(&mut this); + rust_fn_once(this_mut.deref_mut()); + Ok(Variant::nil()) + }); + callable.call_deferred(&[]); + } +} diff --git a/godot-core/src/obj/mod.rs b/godot-core/src/obj/mod.rs index cf1c24a44..81df936c2 100644 --- a/godot-core/src/obj/mod.rs +++ b/godot-core/src/obj/mod.rs @@ -12,6 +12,7 @@ //! * [`Gd`], a smart pointer that manages instances of Godot classes. mod base; +mod call_deferred; mod casts; mod dyn_gd; mod gd; @@ -25,6 +26,7 @@ mod traits; pub(crate) mod rtti; pub use base::*; +pub use call_deferred::WithDeferredCall; pub use dyn_gd::DynGd; pub use gd::*; pub use guards::{BaseMut, BaseRef, DynGdMut, DynGdRef, GdMut, GdRef}; diff --git a/godot-core/src/registry/signal/typed_signal.rs b/godot-core/src/registry/signal/typed_signal.rs index 87277b132..a7563f71f 100644 --- a/godot-core/src/registry/signal/typed_signal.rs +++ b/godot-core/src/registry/signal/typed_signal.rs @@ -16,6 +16,7 @@ use std::borrow::Cow; use std::marker::PhantomData; use std::ops::DerefMut; +// TODO(v0.4): find more general name for trait. /// Object part of the signal receiver (handler). /// /// Functionality overlaps partly with [`meta::AsObjectArg`] and [`meta::AsArg`]. Can however not directly be replaced diff --git a/godot-core/src/task/async_runtime.rs b/godot-core/src/task/async_runtime.rs index 133b1bf6d..e97035742 100644 --- a/godot-core/src/task/async_runtime.rs +++ b/godot-core/src/task/async_runtime.rs @@ -95,7 +95,6 @@ pub fn spawn(future: impl Future + 'static) -> TaskHandle { // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`. // // Once thread-safe futures are possible the restriction can be lifted. - #[cfg(not(wasm_nothreads))] assert!( crate::init::is_main_thread(), "godot_task() can only be used on the main thread" diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index 858c53371..fbbfd375f 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -435,9 +435,16 @@ pub fn main_thread_id() -> std::thread::ThreadId { /// /// # Panics /// - If it is called before the engine bindings have been initialized. -#[cfg(not(wasm_nothreads))] pub fn is_main_thread() -> bool { - std::thread::current().id() == main_thread_id() + #[cfg(not(wasm_nothreads))] + { + std::thread::current().id() == main_thread_id() + } + + #[cfg(wasm_nothreads)] + { + true + } } // ---------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/godot/src/prelude.rs b/godot/src/prelude.rs index fea22fceb..c13498123 100644 --- a/godot/src/prelude.rs +++ b/godot/src/prelude.rs @@ -36,5 +36,6 @@ 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::WithDeferredCall as _; pub use super::obj::WithSignals as _; // Gd::signals() pub use super::obj::WithUserSignals as _; // self.signals() diff --git a/itest/rust/src/object_tests/call_deferred_test.rs b/itest/rust/src/object_tests/call_deferred_test.rs new file mode 100644 index 000000000..50a3b7cf0 --- /dev/null +++ b/itest/rust/src/object_tests/call_deferred_test.rs @@ -0,0 +1,110 @@ +/* + * 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 crate::framework::itest; +use godot::obj::WithBaseField; +use godot::prelude::*; +use godot::task::{SignalFuture, TaskHandle}; +use std::ops::DerefMut; + +const ACCEPTED_NAME: &str = "touched"; + +#[derive(GodotClass)] +#[class(init,base=Node2D)] +struct DeferredTestNode { + base: Base, +} + +#[godot_api] +impl DeferredTestNode { + #[signal] + fn test_completed(name: StringName); + + #[func] + fn accept(&mut self) { + self.base_mut().set_name(ACCEPTED_NAME); + } + + fn to_assertion_task(&self) -> TaskHandle { + assert_ne!( + self.base().get_name().to_string(), + ACCEPTED_NAME, + "accept evaluated synchronously" + ); + + let test_will_succeed: SignalFuture<(StringName,)> = + Signal::from_object_signal(&self.to_gd(), "test_completed").to_future(); + + godot::task::spawn(async move { + let (name,) = test_will_succeed.await; + + assert_eq!(name.to_string(), ACCEPTED_NAME); + }) + } +} + +#[godot_api] +impl INode2D for DeferredTestNode { + fn process(&mut self, _delta: f64) { + let name = self.base().get_name(); + self.signals().test_completed().emit(&name); + self.base_mut().queue_free(); + } + + fn ready(&mut self) { + self.base_mut().set_name("verify") + } +} + +#[itest(async)] +fn call_deferred_untyped(ctx: &crate::framework::TestContext) -> TaskHandle { + let mut test_node = DeferredTestNode::new_alloc(); + ctx.scene_tree.clone().add_child(&test_node); + + // this is called through godot binding and therefore requires #[func] on the method + test_node.call_deferred("accept", &[]); + + let handle = test_node.bind().to_assertion_task(); + handle +} + +#[itest(async)] +fn call_deferred_godot_class(ctx: &crate::framework::TestContext) -> TaskHandle { + let mut test_node = DeferredTestNode::new_alloc(); + ctx.scene_tree.clone().add_child(&test_node); + + let mut gd_mut = test_node.bind_mut(); + // Explicitly check that this can be invoked on &mut T. + let godot_class_ref: &mut DeferredTestNode = gd_mut.deref_mut(); + godot_class_ref.apply_deferred(DeferredTestNode::accept); + drop(gd_mut); + + let handle = test_node.bind().to_assertion_task(); + handle +} + +#[itest(async)] +fn call_deferred_gd_user_class(ctx: &crate::framework::TestContext) -> TaskHandle { + let mut test_node = DeferredTestNode::new_alloc(); + ctx.scene_tree.clone().add_child(&test_node); + + test_node.apply_deferred(DeferredTestNode::accept); + + let handle = test_node.bind().to_assertion_task(); + handle +} + +#[itest(async)] +fn call_deferred_gd_engine_class(ctx: &crate::framework::TestContext) -> TaskHandle { + let test_node = DeferredTestNode::new_alloc(); + ctx.scene_tree.clone().add_child(&test_node); + + let mut node = test_node.clone().upcast::(); + node.apply_deferred(|that_node| that_node.set_name(ACCEPTED_NAME)); + + let handle = test_node.bind().to_assertion_task(); + handle +} diff --git a/itest/rust/src/object_tests/mod.rs b/itest/rust/src/object_tests/mod.rs index 0c7f5486e..7924aebb1 100644 --- a/itest/rust/src/object_tests/mod.rs +++ b/itest/rust/src/object_tests/mod.rs @@ -8,6 +8,9 @@ mod base_test; mod class_name_test; mod class_rename_test; +// Test code depends on task API, Godot 4.2+. +#[cfg(since_api = "4.2")] +mod call_deferred_test; mod dyn_gd_test; mod dynamic_call_test; mod enum_test;