diff --git a/godot-core/src/classes/match_class.rs b/godot-core/src/classes/match_class.rs index 3c60f7a69..5b710ddca 100644 --- a/godot-core/src/classes/match_class.rs +++ b/godot-core/src/classes/match_class.rs @@ -26,6 +26,8 @@ /// # // Hack to keep amount of SELECTED_CLASSES limited: /// # type InputEventMouseButton = InputEventAction; /// # type InputEventMouseMotion = InputEventAction; +/// # type InputEventKey = InputEventAction; +/// # trait InputEventTrait: 'static {} /// // Basic syntax. /// let event: Gd = some_input(); /// @@ -33,6 +35,7 @@ /// button @ InputEventMouseButton => 1, /// motion @ InputEventMouseMotion => 2, /// action @ InputEventAction => 3, +/// key @ InputEventKey => 4, /// _ => 0, // Fallback. /// }; /// @@ -50,6 +53,13 @@ /// // Qualified types supported: /// action @ godot::classes::InputEventAction => 3, /// +/// // If you're only interested in the class and not the object, +/// // discard it with either `_` or `_variable`: +/// _ @ InputEventKey => 4, +/// +/// // Dynamic dispatch supported: +/// dynamic @ dyn InputEventTrait => 5, +/// /// // Fallback with variable -- retrieves original Gd. /// original => 0, /// // Can also be used with mut: @@ -57,7 +67,7 @@ /// // If the match arms have type (), we can also omit the fallback branch. /// }; /// -/// // event_type is now 0, 1, 2, or 3 +/// // event_type is now 0, 1, 2, 3, 4 or 5. /// ``` /// /// # Expression and control flow @@ -79,6 +89,16 @@ macro_rules! match_class { #[doc(hidden)] #[macro_export] macro_rules! match_class_muncher { + // mut variable @ dyn Trait => { ... }. + ($subject:ident, mut $var:ident @ dyn $Tr:path => $block:expr, $($rest:tt)*) => {{ + match $subject.try_dynify::() { + Ok(mut $var) => $block, + Err(__obj) => { + $crate::match_class_muncher!(__obj, $($rest)*) + } + } + }}; + // mut variable @ Class => { ... }. ($subject:ident, mut $var:ident @ $Ty:ty => $block:expr, $($rest:tt)*) => {{ match $subject.try_cast::<$Ty>() { @@ -89,6 +109,16 @@ macro_rules! match_class_muncher { } }}; + // variable @ dyn Trait => { ... }. + ($subject:ident, $var:ident @ dyn $Tr:path => $block:expr, $($rest:tt)*) => {{ + match $subject.try_dynify::() { + Ok($var) => $block, + Err(__obj) => { + $crate::match_class_muncher!(__obj, $($rest)*) + } + } + }}; + // variable @ Class => { ... }. ($subject:ident, $var:ident @ $Ty:ty => $block:expr, $($rest:tt)*) => {{ match $subject.try_cast::<$Ty>() { @@ -99,6 +129,26 @@ macro_rules! match_class_muncher { } }}; + // _ @ dyn Trait => { ... }. + ($subject:ident, _ @ dyn $Tr:path => $block:expr, $($rest:tt)*) => {{ + match $subject.try_dynify::() { + Ok(_) => $block, + Err(__obj) => { + $crate::match_class_muncher!(__obj, $($rest)*) + } + } + }}; + + // _ @ Class => { ... }. + ($subject:ident, _ @ $Ty:ty => $block:expr, $($rest:tt)*) => {{ + match $subject.try_cast::<$Ty>() { + Ok(_) => $block, + Err(__obj) => { + $crate::match_class_muncher!(__obj, $($rest)*) + } + } + }}; + // mut variable => { ... }. ($subject:ident, mut $var:ident => $block:expr $(,)?) => {{ let mut $var = $subject; diff --git a/godot-core/src/obj/dyn_gd.rs b/godot-core/src/obj/dyn_gd.rs index 6e9adb09c..4e15f0bc8 100644 --- a/godot-core/src/obj/dyn_gd.rs +++ b/godot-core/src/obj/dyn_gd.rs @@ -131,6 +131,37 @@ use std::{fmt, ops}; /// // Now work with the abstract object as usual. /// ``` /// +/// Any `Gd` where `T` is an engine class can attempt conversion to `DynGd` with [`Gd::try_dynify()`] as well. +/// +/// ```no_run +/// # use godot::prelude::*; +/// # use godot::classes::Node2D; +/// # // ShapeCast2D is marked as experimental and thus not included in the doctests. +/// # // We use this mock to showcase some real-world usage. +/// # struct FakeShapeCastCollider2D {} +/// +/// # impl FakeShapeCastCollider2D { +/// # fn get_collider(&self, _idx: i32) -> Option> { Some(Node2D::new_alloc()) } +/// # } +/// +/// trait Pushable { /* ... */ } +/// +/// # let my_shapecast = FakeShapeCastCollider2D {}; +/// # let idx = 1; +/// // We can try to convert `Gd` into `DynGd`. +/// let node: Option> = +/// my_shapecast.get_collider(idx).and_then( +/// |obj| obj.try_dynify().ok() +/// ); +/// +/// // An object is returned after failed conversion, similarly to `Gd::try_cast()`. +/// # let some_node = Node::new_alloc(); +/// match some_node.try_dynify::() { +/// Ok(dyn_gd) => (), +/// Err(some_node) => godot_warn!("Failed to convert {some_node} into dyn Pushable!"), +/// } +/// ``` +/// /// When converting from Godot back into `DynGd`, we say that the `dyn Health` trait object is _re-enriched_. /// /// godot-rust achieves this thanks to the registration done by `#[godot_dyn]`: the library knows for which classes `Health` is implemented, @@ -541,7 +572,10 @@ where D: ?Sized + 'static, { fn try_from_godot(via: Self::Via) -> Result { - try_dynify_object(via) + match try_dynify_object(via) { + Ok(dyn_gd) => Ok(dyn_gd), + Err((from_godot_err, obj)) => Err(from_godot_err.into_error(obj)), + } } } diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index b84923306..bfd05353e 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -22,6 +22,7 @@ use crate::obj::{ OnEditor, RawGd, WithSignals, }; use crate::private::{callbacks, PanicPayload}; +use crate::registry::class::try_dynify_object; use crate::registry::property::{object_export_element_type_string, Export, Var}; use crate::{classes, out}; @@ -512,6 +513,21 @@ impl Gd { DynGd::::from_gd(self) } + /// Tries to upgrade to a `DynGd` pointer, enabling the `D` abstraction. + /// + /// If `T`'s dynamic class doesn't implement `AsDyn`, `Err(self)` is returned, meaning you can reuse the original + /// object for further casts. + pub fn try_dynify(self) -> Result, Self> + where + T: GodotClass + Bounds, + D: ?Sized + 'static, + { + match try_dynify_object(self) { + Ok(dyn_gd) => Ok(dyn_gd), + Err((_convert_err, obj)) => Err(obj), + } + } + /// Returns a callable referencing a method from this object named `method_name`. /// /// This is shorter syntax for [`Callable::from_object_method(self, method_name)`][Callable::from_object_method]. diff --git a/godot-core/src/registry/class.rs b/godot-core/src/registry/class.rs index 3500d789d..351a53e47 100644 --- a/godot-core/src/registry/class.rs +++ b/godot-core/src/registry/class.rs @@ -11,7 +11,7 @@ use std::{any, ptr}; use crate::classes::ClassDb; use crate::init::InitLevel; -use crate::meta::error::{ConvertError, FromGodotError}; +use crate::meta::error::FromGodotError; use crate::meta::ClassName; use crate::obj::{cap, DynGd, Gd, GodotClass}; use crate::private::{ClassPlugin, PluginItem}; @@ -324,14 +324,14 @@ pub fn auto_register_rpcs(object: &mut T) { /// lifted, but would need quite a bit of extra machinery to work. pub(crate) fn try_dynify_object( mut object: Gd, -) -> Result, ConvertError> { +) -> Result, (FromGodotError, Gd)> { let typeid = any::TypeId::of::(); let trait_name = sys::short_type_name::(); // Iterate all classes that implement the trait. let dyn_traits_by_typeid = global_dyn_traits_by_typeid(); let Some(relations) = dyn_traits_by_typeid.get(&typeid) else { - return Err(FromGodotError::UnregisteredDynTrait { trait_name }.into_error(object)); + return Err((FromGodotError::UnregisteredDynTrait { trait_name }, object)); }; // TODO maybe use 2nd hashmap instead of linear search. @@ -348,7 +348,7 @@ pub(crate) fn try_dynify_object( class_name: object.dynamic_class_string().to_string(), }; - Err(error.into_error(object)) + Err((error, object)) } /// Responsible for creating hint_string for [`DynGd`][crate::obj::DynGd] properties which works with [`PropertyHint::NODE_TYPE`][crate::global::PropertyHint::NODE_TYPE] or [`PropertyHint::RESOURCE_TYPE`][crate::global::PropertyHint::RESOURCE_TYPE]. diff --git a/itest/rust/src/engine_tests/match_class_test.rs b/itest/rust/src/engine_tests/match_class_test.rs index 1278ca269..1a0b0e635 100644 --- a/itest/rust/src/engine_tests/match_class_test.rs +++ b/itest/rust/src/engine_tests/match_class_test.rs @@ -59,6 +59,29 @@ fn match_class_basic_mut_dispatch() { to_free.free(); } +#[itest] +fn match_class_basic_unnamed_dispatch() { + let node3d = Node3D::new_alloc(); + let obj: Gd = node3d.upcast(); + let to_free = obj.clone(); + + let result = match_class! { obj, + node @ Node2D => { + require_node2d(&node); + 1 + }, + _ @ Node3D => 2, + node @ Node => { + require_node(&node); + 3 + }, + _ => 4 // No comma. + }; + + assert_eq!(result, 2); + to_free.free(); +} + #[itest] fn match_class_shadowed_by_more_general() { let node2d = Node2D::new_alloc(); @@ -172,13 +195,16 @@ fn match_class_unit_type() { let mut val = 0; match_class! { obj, + _ @ Node3D => { + val = 1; + }, mut node @ Node2D => { require_mut_node2d(&mut node); - val = 1; + val = 2; }, node @ Node => { require_node(&node); - val = 2; + val = 3; }, // No need for _ branch since all branches return (). } @@ -191,3 +217,88 @@ fn match_class_unit_type() { // Nothing. }; } + +#[itest] +fn match_class_dyn_dispatch() { + // Test complex inline expression. + let result = match_class! { + ExampleRefCounted2::new_gd().upcast::(), + ref_counted_1 @ dyn ExampleTraitFetch1 => ref_counted_1.dyn_bind().fetch(), + ref_counted_2 @ dyn ExampleTraitFetch2 => ref_counted_2.dyn_bind().fetch(), + _ignored => 3, + }; + + assert_eq!(result, 2); +} + +#[itest] +fn match_class_mut_dyn_dispatch() { + // Test complex inline expression. + let mut result = 0; + match_class! { + ExampleRefCounted1::new_gd().upcast::(), + mut ref_counted_1 @ dyn ExampleTraitMut1 => ref_counted_1.dyn_bind_mut().mutate(&mut result), + mut ref_counted_2 @ dyn ExampleTraitMut2 => ref_counted_2.dyn_bind_mut().mutate(&mut result), + }; + + assert_eq!(result, 1); +} + +#[itest] +fn match_class_unnamed_dyn_dispatch() { + // Test complex inline expression. + let result = match_class! { + ExampleRefCounted1::new_gd().upcast::(), + _ @ dyn ExampleTraitFetch1 => 1, + ref_counted_2 @ dyn ExampleTraitFetch2 => ref_counted_2.dyn_bind().fetch(), + _ignored => 3, + }; + + assert_eq!(result, 1); +} + +// Example traits and nodes to use in the match class dynify testing. + +trait ExampleTraitFetch1: 'static { + fn fetch(&self) -> i32 { + 1 + } +} + +trait ExampleTraitFetch2: 'static { + fn fetch(&self) -> i32 { + 2 + } +} + +trait ExampleTraitMut1: 'static { + fn mutate(&mut self, value: &mut i32) { + *value = 1; + } +} + +trait ExampleTraitMut2: 'static { + fn mutate(&mut self, value: &mut i32) { + *value = 2; + } +} + +#[derive(GodotClass)] +#[class(init)] +struct ExampleRefCounted1 {} + +#[godot_dyn] +impl ExampleTraitFetch1 for ExampleRefCounted1 {} + +#[godot_dyn] +impl ExampleTraitMut1 for ExampleRefCounted1 {} + +#[derive(GodotClass)] +#[class(init)] +struct ExampleRefCounted2 {} + +#[godot_dyn] +impl ExampleTraitFetch2 for ExampleRefCounted2 {} + +#[godot_dyn] +impl ExampleTraitMut2 for ExampleRefCounted2 {} diff --git a/itest/rust/src/object_tests/dyn_gd_test.rs b/itest/rust/src/object_tests/dyn_gd_test.rs index d96949278..8feff57ca 100644 --- a/itest/rust/src/object_tests/dyn_gd_test.rs +++ b/itest/rust/src/object_tests/dyn_gd_test.rs @@ -342,6 +342,71 @@ fn dyn_gd_variant_conversions() { node.free(); } +#[itest] +fn dyn_gd_object_conversions() { + let node = foreign::NodeHealth::new_alloc().upcast::(); + let original_id = node.instance_id(); + + // Convert to different levels of DynGd: + let back: DynGd = node + .try_dynify() + .expect("Gd::try_dynify() should succeed.") + .cast(); + assert_eq!(back.dyn_bind().get_hitpoints(), 100); + assert_eq!(back.instance_id(), original_id); + + let obj = back.into_gd().upcast::(); + let back: DynGd = + obj.try_dynify().expect("Gd::try_dynify() should succeed."); + assert_eq!(back.dyn_bind().get_hitpoints(), 100); + assert_eq!(back.instance_id(), original_id); + + // Back to NodeHealth. + let node = back.cast::(); + assert_eq!(node.bind().get_hitpoints(), 100); + assert_eq!(node.instance_id(), original_id); + + // Convert to different DynGd. + let obj = node.into_gd().upcast::(); + let back: DynGd> = + obj.try_dynify().expect("Gd::try_dynify() should succeed."); + assert_eq!(back.dyn_bind().get_id_dynamic(), original_id); + + let obj = back.into_gd().upcast::(); + let back: DynGd> = + obj.try_dynify().expect("Gd::try_dynify() should succeed."); + assert_eq!(back.dyn_bind().get_id_dynamic(), original_id); + + back.free() +} + +#[itest] +fn dyn_gd_object_conversion_failures() { + // Unregistered trait conversion failure. + trait UnrelatedTrait {} + + let node = foreign::NodeHealth::new_alloc().upcast::(); + let original_id = node.instance_id(); + let back = node.try_dynify::(); + let node = back.expect_err("Gd::try_dynify() should have failed"); + + // `Gd::try_dynify()` should return the original instance on failure, similarly to `Gd::try_cast()`. + assert_eq!(original_id, node.instance_id()); + + // Unimplemented trait conversion failures. + let back = node.try_dynify::>(); + let node = back.expect_err("Gd::try_dynify() should have failed"); + assert_eq!(original_id, node.instance_id()); + + let obj = RefCounted::new_gd(); + let original_id = obj.instance_id(); + let back = obj.try_dynify::(); + let obj = back.expect_err("Gd::try_dynify() should have failed"); + assert_eq!(original_id, obj.instance_id()); + + node.free(); +} + #[itest] fn dyn_gd_store_in_godot_array() { let a = Gd::from_object(RefcHealth { hp: 33 }).into_dyn();