From f8fbd6d62a394c69cd578c59c242c7ecb302e637 Mon Sep 17 00:00:00 2001 From: Lukas Rieger Date: Thu, 7 Nov 2024 02:28:06 +0100 Subject: [PATCH] add new option 'notify=callback_fn' to the #[var] attribute it can only be used in conjunction with an autogenerated setter. the callback_fn is a 'fn(&[mut ]self) { }', which will be called whenever the backing value of the var changes. --- .../src/class/data_models/field_var.rs | 53 +++++++++++++++++-- .../src/class/data_models/property.rs | 5 +- itest/rust/src/object_tests/property_test.rs | 37 +++++++++++++ 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/godot-macros/src/class/data_models/field_var.rs b/godot-macros/src/class/data_models/field_var.rs index 267ae0de9..74918a500 100644 --- a/godot-macros/src/class/data_models/field_var.rs +++ b/godot-macros/src/class/data_models/field_var.rs @@ -12,7 +12,7 @@ use crate::class::{ into_signature_info, make_existence_check, make_method_registration, Field, FieldHint, FuncDefinition, }; -use crate::util::KvParser; +use crate::util::{bail, KvParser}; use crate::{util, ParseResult}; /// Store info from `#[var]` attribute. @@ -20,10 +20,31 @@ use crate::{util, ParseResult}; pub struct FieldVar { pub getter: GetterSetter, pub setter: GetterSetter, + pub notify: Option, pub hint: FieldHint, pub usage_flags: UsageFlags, } +fn parse_notify(parser: &mut KvParser, key: &str) -> ParseResult> { + match parser.handle_any(key) { + // No `notify` argument + None => Ok(None), + Some(value) => match value { + // `notify` without value is an error + None => { + bail!(parser.span(), "The correct syntax is 'notify = callback_fn'") + }, + // `notify = expr` + Some(value) => { + match value.ident() { + Ok(ident) => Ok(Some(ident)), + Err(_) => bail!(parser.span(), "The correct syntax is 'notify = callback_fn'") + } + } + } + } +} + impl FieldVar { /// Parse a `#[var]` attribute to a `FieldVar` struct. /// @@ -36,12 +57,17 @@ impl FieldVar { pub(crate) fn new_from_kv(parser: &mut KvParser) -> ParseResult { let mut getter = GetterSetter::parse(parser, "get")?; let mut setter = GetterSetter::parse(parser, "set")?; + let mut notify = parse_notify(parser, "notify")?; if getter.is_omitted() && setter.is_omitted() { getter = GetterSetter::Generated; setter = GetterSetter::Generated; } + if notify.is_some() && !setter.is_generated() { + return bail!(parser.span(), "When using 'notify', the property must also use an autogenerated 'set'"); + } + let hint = parser.handle_ident("hint")?; let hint = if let Some(hint) = hint { @@ -69,6 +95,7 @@ impl FieldVar { Ok(FieldVar { getter, setter, + notify, hint, usage_flags, }) @@ -112,12 +139,13 @@ impl GetterSetter { &self, class_name: &Ident, kind: GetSet, + notify: Option, field: &Field, ) -> Option { match self { GetterSetter::Omitted => None, GetterSetter::Generated => Some(GetterSetterImpl::from_generated_impl( - class_name, kind, field, + class_name, kind, notify, field, )), GetterSetter::Custom(function_name) => { Some(GetterSetterImpl::from_custom_impl(function_name)) @@ -128,6 +156,10 @@ impl GetterSetter { pub fn is_omitted(&self) -> bool { matches!(self, GetterSetter::Omitted) } + + pub fn is_generated(&self) -> bool { + matches!(self, GetterSetter::Generated) + } } /// Used to determine whether a [`GetterSetter`] is supposed to be a getter or setter. @@ -154,7 +186,7 @@ pub struct GetterSetterImpl { } impl GetterSetterImpl { - fn from_generated_impl(class_name: &Ident, kind: GetSet, field: &Field) -> Self { + fn from_generated_impl(class_name: &Ident, kind: GetSet, notify: Option, field: &Field) -> Self { let Field { name: field_name, ty: field_type, @@ -179,8 +211,23 @@ impl GetterSetterImpl { signature = quote! { fn #function_name(&mut self, #field_name: <#field_type as ::godot::meta::GodotConvert>::Via) }; + + let notify_block = match notify { + Some(ident) => { + quote! { + if self.#field_name != #field_name { + self.#ident(); + } + } + }, + None => { + quote! { } + } + }; + function_body = quote! { <#field_type as ::godot::register::property::Var>::set_property(&mut self.#field_name, #field_name); + #notify_block }; } } diff --git a/godot-macros/src/class/data_models/property.rs b/godot-macros/src/class/data_models/property.rs index cae0defc9..15839984b 100644 --- a/godot-macros/src/class/data_models/property.rs +++ b/godot-macros/src/class/data_models/property.rs @@ -68,6 +68,7 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream { let FieldVar { getter, setter, + notify, hint, mut usage_flags, } = var; @@ -134,12 +135,12 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream { }; let getter_name = make_getter_setter( - getter.to_impl(class_name, GetSet::Get, field), + getter.to_impl(class_name, GetSet::Get, None, field), &mut getter_setter_impls, &mut export_tokens, ); let setter_name = make_getter_setter( - setter.to_impl(class_name, GetSet::Set, field), + setter.to_impl(class_name, GetSet::Set, notify, field), &mut getter_setter_impls, &mut export_tokens, ); diff --git a/itest/rust/src/object_tests/property_test.rs b/itest/rust/src/object_tests/property_test.rs index a8d5fd2ad..fcc8e46e6 100644 --- a/itest/rust/src/object_tests/property_test.rs +++ b/itest/rust/src/object_tests/property_test.rs @@ -450,3 +450,40 @@ fn override_export() { fn check_property(property: &Dictionary, key: &str, expected: impl ToGodot) { assert_eq!(property.get_or_nil(key), expected.to_variant()); } + + +// --------------------------------------------------------------- + +#[derive(GodotClass)] +#[class(base=Node, init)] +struct NotifyTest { + #[var(notify = on_change)] + a: i32, + #[var(notify = on_change)] + b: i32, + + pub call_count: u32 +} + +impl NotifyTest { + fn on_change(&mut self) { + self.call_count += 1; + } +} + +#[itest] +fn test_var_notify() { + let mut class = NotifyTest::new_alloc(); + + assert_eq!(class.bind().call_count, 0); + + class.call("set_a", &[3.to_variant()]); + assert_eq!(class.bind().a, 3); + assert_eq!(class.bind().call_count, 1); + + + class.call("set_b", &[5.to_variant()]); + assert_eq!(class.bind().a, 5); + assert_eq!(class.bind().call_count, 2); +} +