Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[proposal] add 'notify' to '#[var]' #946

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/full-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ name: Full CI
# Runs before merging. Rebases on master to make sure CI passes for latest integration, not only for the PR at the time of creation.

on:
# allow manually triggering the workflow (https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_dispatch)
workflow_dispatch:
merge_group:
# push:

Expand Down
2 changes: 2 additions & 0 deletions godot-core/src/meta/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ mod signature;
mod traits;

pub mod error;
pub mod property_update;

pub use as_arg::*;
pub use class_name::ClassName;
pub use godot_convert::{FromGodot, GodotConvert, ToGodot};
pub use property_update::PropertyUpdate;
#[cfg(feature = "codegen-full")]
pub use rpc_config::RpcConfig;
pub use traits::{ArrayElement, GodotType, PackedArrayElement};
Expand Down
14 changes: 14 additions & 0 deletions godot-core/src/meta/property_update.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
pub struct PropertyUpdate<'a, C, T> {
pub new_value: T,
pub field_name: &'a str, // might also be &'a StringName, depending on what's available
pub get_field_mut: fn(&mut C) -> &mut T,
}

impl<C, T> PropertyUpdate<'_, C, T> {
pub fn set(self, obj: &mut C) {
*(self.get_field_mut)(obj) = self.new_value;
}
pub fn set_custom(self, obj: &mut C, value: T) {
*(self.get_field_mut)(obj) = value;
}
}
149 changes: 142 additions & 7 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,67 @@ 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'"
),
},
},
}
}

fn parse_setter_ex(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 'setter_ex = callback_fn'"
)
}
// `notify = expr`
Some(value) => match value.ident() {
Ok(ident) => Ok(Some(ident)),
Err(_) => bail!(
parser.span(),
"The correct syntax is 'setter_ex = callback_fn'"
),
},
},
}
}

impl FieldVar {
/// Parse a `#[var]` attribute to a `FieldVar` struct.
///
Expand All @@ -36,12 +85,32 @@ 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 notify = parse_notify(parser, "notify")?;
let setter_ex = parse_setter_ex(parser, "set_ex")?;

if getter.is_omitted() && setter.is_omitted() {
if getter.is_omitted() && setter.is_omitted() && setter_ex.is_none() {
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'"
);
}

if setter_ex.is_some() && !setter.is_omitted() {
return bail!(
parser.span(),
"You may not use 'set' and 'set_ex' at the same time, remove one"
);
}

if let Some(ident) = setter_ex {
setter = GetterSetter::Ex(ident);
}

let hint = parser.handle_ident("hint")?;

let hint = if let Some(hint) = hint {
Expand Down Expand Up @@ -69,6 +138,7 @@ impl FieldVar {
Ok(FieldVar {
getter,
setter,
notify,
hint,
usage_flags,
})
Expand All @@ -86,6 +156,9 @@ pub enum GetterSetter {

/// Getter/setter is handwritten by the user, and here is its identifier.
Custom(Ident),

/// only applicable to setter. A generic setter that takes 'PropertyUpdate<C, T>' is handwritten by the user
Ex(Ident),
}

impl GetterSetter {
Expand All @@ -112,36 +185,48 @@ 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))
}
GetterSetter::Ex(function_name) => {
assert!(matches!(kind, GetSet::SetEx(_)));
Some(GetterSetterImpl::from_generated_impl(
class_name, kind, notify, field,
))
}
}
}

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.
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum GetSet {
Get,
Set,
SetEx(Ident),
}

impl GetSet {
pub fn prefix(&self) -> &'static str {
match self {
GetSet::Get => "get_",
GetSet::Set => "set_",
GetSet::Set | GetSet::SetEx(_) => "set_",
}
}
}
Expand All @@ -154,7 +239,12 @@ 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,9 +269,54 @@ impl GetterSetterImpl {
signature = quote! {
fn #function_name(&mut self, #field_name: <#field_type as ::godot::meta::GodotConvert>::Via)
};
function_body = quote! {

let function_body_set = quote! {
<#field_type as ::godot::register::property::Var>::set_property(&mut self.#field_name, #field_name);
};

function_body = match notify {
Some(ident) => {
quote! {
let prev_value = self.#field_name;
#function_body_set
if prev_value != self.#field_name {
self.#ident();
}
}
}
None => function_body_set,
};
}
GetSet::SetEx(callback_fn_ident) => {
signature = quote! {
fn #function_name(&mut self, #field_name: <#field_type as ::godot::meta::GodotConvert>::Via)
};

let field_name_string_constant = field_name.to_string();

let function_body_set = quote! {

let new_value = ::godot::meta::FromGodot::from_godot(#field_name);
let property_update = ::godot::meta::PropertyUpdate {
new_value: new_value,
field_name: #field_name_string_constant,
get_field_mut: |c: &mut #class_name|&mut c.#field_name
};
self.#callback_fn_ident(property_update);
};

function_body = match notify {
Some(ident) => {
quote! {
let prev_value = self.#field_name;
#function_body_set
if prev_value != self.#field_name {
self.#ident();
}
}
}
None => function_body_set,
};
}
}

Expand Down
11 changes: 8 additions & 3 deletions godot-macros/src/class/data_models/property.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

//! Parsing the `var` and `export` attributes on fields.

use crate::class::{Field, FieldVar, Fields, GetSet, GetterSetterImpl, UsageFlags};
use crate::class::{Field, FieldVar, Fields, GetSet, GetterSetter, GetterSetterImpl, UsageFlags};
use proc_macro2::{Ident, TokenStream};
use quote::quote;

Expand Down 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,16 @@ 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_kind = match &setter {
GetterSetter::Ex(ident) => GetSet::SetEx(ident.clone()),
_ => GetSet::Set,
};
let setter_name = make_getter_setter(
setter.to_impl(class_name, GetSet::Set, field),
setter.to_impl(class_name, setter_kind, notify, field),
&mut getter_setter_impls,
&mut export_tokens,
);
Expand Down
79 changes: 79 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,82 @@ 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().b, 5);
assert_eq!(class.bind().call_count, 2);

class.free();
}

// ---------------------------------------------------------------

#[derive(GodotClass)]
#[class(base=Node, init)]
struct SetExTest {
#[var(set_ex = custom_set)]
a: i32,
#[var(set_ex = custom_set)]
b: i32,

pub call_count: u32,
}

impl SetExTest {
fn custom_set<T>(&mut self, update: godot::meta::property_update::PropertyUpdate<Self, T>) {
// pre-set checks

update.set(self);

// post-set actions
self.call_count += 1;
}
}

#[itest]
fn test_var_set_ex() {
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().b, 5);
assert_eq!(class.bind().call_count, 2);

class.free();
}

// ---------------------------------------------------------------