Skip to content

Commit c7e9af5

Browse files
committed
feat(core): support easier, type-safe deferred calls
1 parent 1d87d7c commit c7e9af5

File tree

5 files changed

+139
-32
lines changed

5 files changed

+139
-32
lines changed

godot-core/src/obj/call_deferred.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
use crate::builtin::{Callable, Variant};
8+
use crate::meta::UniformObjectDeref;
9+
use crate::obj::bounds::Declarer;
10+
use crate::obj::GodotClass;
11+
#[cfg(since_api = "4.2")]
12+
use crate::registry::signal::ToSignalObj;
13+
use godot_ffi::is_main_thread;
14+
use std::ops::DerefMut;
15+
16+
// Dummy traits to still allow bounds and imports.
17+
#[cfg(before_api = "4.2")]
18+
pub trait WithDeferredCall<T: GodotClass> {}
19+
20+
/// Trait that is automatically implemented for engine classes and user classes containing a `Base<T>` field.
21+
///
22+
/// This trait enables type safe deferred method calls.
23+
///
24+
/// # Usage
25+
///
26+
/// ```no_compile
27+
/// # use godot::prelude::*;
28+
/// # use godot::classes::CollisionShape2D;
29+
/// # use std::f32::consts::PI;
30+
/// fn some_fn(mut shape: Gd<CollisionShape2D>)
31+
/// {
32+
/// shape.apply_deferred(|shape_mut| shape_mut.rotate(PI))
33+
/// }
34+
/// ```
35+
#[cfg(since_api = "4.2")]
36+
pub trait WithDeferredCall<T: GodotClass> {
37+
/// Runs the given Closure deferred.
38+
///
39+
/// This can be a type-safe alternative to [`crate::classes::Object::call_deferred`]. This method must be used on the main thread.
40+
fn apply_deferred<F>(&mut self, rust_function: F)
41+
where
42+
F: FnMut(&mut T) + 'static;
43+
}
44+
45+
#[cfg(since_api = "4.2")]
46+
impl<T, S, D: Declarer> WithDeferredCall<T> for S
47+
where
48+
T: UniformObjectDeref<D, Declarer = D>,
49+
S: ToSignalObj<T>,
50+
{
51+
fn apply_deferred<'a, F>(&mut self, mut rust_function: F)
52+
where
53+
F: FnMut(&mut T) + 'static,
54+
{
55+
assert!(
56+
is_main_thread(),
57+
"apply_deferred must be called on main thread"
58+
);
59+
let mut this = self.to_signal_obj().clone();
60+
let callable = Callable::from_local_fn("apply_deferred", move |_| {
61+
rust_function(T::object_as_mut(&mut this).deref_mut());
62+
Ok(Variant::nil())
63+
});
64+
callable.call_deferred(&[]);
65+
}
66+
}

godot-core/src/obj/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//! * [`Gd`], a smart pointer that manages instances of Godot classes.
1313
1414
mod base;
15+
mod call_deferred;
1516
mod dyn_gd;
1617
mod gd;
1718
mod guards;
@@ -24,6 +25,7 @@ mod traits;
2425
pub(crate) mod rtti;
2526

2627
pub use base::*;
28+
pub use call_deferred::WithDeferredCall;
2729
pub use dyn_gd::DynGd;
2830
pub use gd::*;
2931
pub use guards::{BaseMut, BaseRef, DynGdMut, DynGdRef, GdMut, GdRef};
@@ -35,6 +37,7 @@ pub use traits::*;
3537

3638
pub mod bounds;
3739
pub mod script;
40+
3841
pub use bounds::private::Bounds;
3942

4043
// Do not re-export rtti here.

godot-ffi/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,6 @@ pub fn main_thread_id() -> std::thread::ThreadId {
435435
///
436436
/// # Panics
437437
/// - If it is called before the engine bindings have been initialized.
438-
///
439438
pub fn is_main_thread() -> bool {
440439
#[cfg(not(wasm_nothreads))]
441440
{

godot/src/prelude.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,6 @@ pub use super::obj::EngineEnum as _;
3636
pub use super::obj::NewAlloc as _;
3737
pub use super::obj::NewGd as _;
3838
pub use super::obj::WithBaseField as _; // base(), base_mut(), to_gd()
39+
pub use super::obj::WithDeferredCall as _;
3940
pub use super::obj::WithSignals as _; // Gd::signals()
4041
pub use super::obj::WithUserSignals as _; // self.signals()

itest/rust/src/object_tests/call_deferred_test.rs

Lines changed: 69 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,71 +4,109 @@
44
* License, v. 2.0. If a copy of the MPL was not distributed with this
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
7-
87
use crate::framework::itest;
9-
use crate::object_tests::call_deferred_test::TestState::{Accepted, Initial};
8+
use godot::obj::WithBaseField;
109
use godot::prelude::*;
1110
use godot::task::{SignalFuture, TaskHandle};
11+
use std::ops::DerefMut;
1212

13-
#[derive(GodotConvert, Var, Export, Clone, PartialEq, Debug)]
14-
#[godot(via = GString)]
15-
enum TestState {
16-
Initial,
17-
Accepted,
18-
}
13+
const ACCEPTED_NAME: &str = "touched";
1914

2015
#[derive(GodotClass)]
21-
#[class(base=Node)]
16+
#[class(init,base=Node2D)]
2217
struct DeferredTestNode {
23-
base: Base<Node>,
24-
state: TestState,
18+
base: Base<Node2D>,
2519
}
2620

2721
#[godot_api]
2822
impl DeferredTestNode {
2923
#[signal]
30-
fn test_completed(state: TestState);
24+
fn test_completed(name: StringName);
3125

3226
#[func]
33-
fn accept(&mut self) {
34-
self.state = Accepted;
27+
fn gd_accept(&mut self) {
28+
self.rust_accept();
29+
}
30+
31+
fn rust_accept(&mut self) {
32+
self.base_mut().set_name(ACCEPTED_NAME);
3533
}
3634

3735
fn as_expectation_task(&self) -> TaskHandle {
38-
assert_eq!(Initial, self.state, "accept evaluated synchronously");
36+
assert_ne!(
37+
self.base().get_name().to_string(),
38+
ACCEPTED_NAME,
39+
"accept evaluated synchronously"
40+
);
3941

40-
let test_will_succeed: SignalFuture<(Variant,)> =
42+
let test_will_succeed: SignalFuture<(StringName,)> =
4143
Signal::from_object_signal(&self.to_gd(), "test_completed").to_future();
44+
4245
godot::task::spawn(async move {
43-
let (final_state,) = test_will_succeed.await;
44-
let final_state: TestState = final_state.to();
46+
let (name,) = test_will_succeed.await;
4547

46-
assert_eq!(Accepted, final_state);
48+
assert_eq!(name.to_string(), ACCEPTED_NAME);
4749
})
4850
}
4951
}
5052

5153
#[godot_api]
52-
impl INode for DeferredTestNode {
53-
fn init(base: Base<Self::Base>) -> Self {
54-
Self {
55-
base,
56-
state: Initial,
57-
}
54+
impl INode2D for DeferredTestNode {
55+
fn process(&mut self, _delta: f64) {
56+
let name = self.base().get_name();
57+
self.signals().test_completed().emit(&name);
58+
self.base_mut().queue_free();
5859
}
5960

60-
fn process(&mut self, _delta: f64) {
61-
let args = vslice![self.state];
62-
self.base_mut().emit_signal("test_completed", args);
61+
fn ready(&mut self) {
62+
self.base_mut().set_name("verify")
6363
}
6464
}
6565

6666
#[itest(async)]
67-
fn calls_method_names_deferred(ctx: &crate::framework::TestContext) -> TaskHandle {
67+
fn call_deferred_untyped(ctx: &crate::framework::TestContext) -> TaskHandle {
68+
let mut test_node = DeferredTestNode::new_alloc();
69+
ctx.scene_tree.clone().add_child(&test_node);
70+
71+
test_node.call_deferred("gd_accept", &[]);
72+
73+
let handle = test_node.bind().as_expectation_task();
74+
handle
75+
}
76+
77+
#[itest(async)]
78+
fn call_deferred_godot_class(ctx: &crate::framework::TestContext) -> TaskHandle {
79+
let mut test_node = DeferredTestNode::new_alloc();
80+
ctx.scene_tree.clone().add_child(&test_node);
81+
82+
let mut gd_mut = test_node.bind_mut();
83+
// Explicitly check that this can be invoked on &mut T.
84+
let godot_class_ref: &mut DeferredTestNode = gd_mut.deref_mut();
85+
godot_class_ref.apply_deferred(DeferredTestNode::rust_accept);
86+
drop(gd_mut);
87+
88+
let handle = test_node.bind().as_expectation_task();
89+
handle
90+
}
91+
92+
#[itest(async)]
93+
fn call_deferred_gd_user_class(ctx: &crate::framework::TestContext) -> TaskHandle {
6894
let mut test_node = DeferredTestNode::new_alloc();
6995
ctx.scene_tree.clone().add_child(&test_node);
70-
71-
test_node.call_deferred("accept", &[]);
96+
97+
test_node.apply_deferred(DeferredTestNode::rust_accept);
98+
99+
let handle = test_node.bind().as_expectation_task();
100+
handle
101+
}
102+
103+
#[itest(async)]
104+
fn call_deferred_gd_engine_class(ctx: &crate::framework::TestContext) -> TaskHandle {
105+
let test_node = DeferredTestNode::new_alloc();
106+
ctx.scene_tree.clone().add_child(&test_node);
107+
108+
let mut node = test_node.clone().upcast::<Node>();
109+
node.apply_deferred(|that_node| that_node.set_name(ACCEPTED_NAME));
72110

73111
let handle = test_node.bind().as_expectation_task();
74112
handle

0 commit comments

Comments
 (0)