Skip to content

Commit

Permalink
add new option 'notify=callback_fn' to the #[var] attribute
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
0x53A committed Nov 7, 2024
1 parent 0e9878b commit f8fbd6d
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 5 deletions.
53 changes: 50 additions & 3 deletions godot-macros/src/class/data_models/field_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,39 @@ 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.
#[derive(Default, Clone, Debug)]
pub struct FieldVar {
pub getter: GetterSetter,
pub setter: GetterSetter,
pub notify: Option<Ident>,
pub hint: FieldHint,
pub usage_flags: UsageFlags,
}

fn parse_notify(parser: &mut KvParser, key: &str) -> ParseResult<Option<Ident>> {
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.
///
Expand All @@ -36,12 +57,17 @@ impl FieldVar {
pub(crate) fn new_from_kv(parser: &mut KvParser) -> ParseResult<Self> {
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 {
Expand Down Expand Up @@ -69,6 +95,7 @@ impl FieldVar {
Ok(FieldVar {
getter,
setter,
notify,
hint,
usage_flags,
})
Expand Down Expand Up @@ -112,12 +139,13 @@ impl GetterSetter {
&self,
class_name: &Ident,
kind: GetSet,
notify: Option<Ident>,
field: &Field,
) -> Option<GetterSetterImpl> {
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))
Expand All @@ -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.
Expand All @@ -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<Ident>, field: &Field) -> Self {
let Field {
name: field_name,
ty: field_type,
Expand All @@ -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
};
}
}
Expand Down
5 changes: 3 additions & 2 deletions godot-macros/src/class/data_models/property.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
);
Expand Down
37 changes: 37 additions & 0 deletions itest/rust/src/object_tests/property_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

0 comments on commit f8fbd6d

Please sign in to comment.