-
-
Notifications
You must be signed in to change notification settings - Fork 198
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
base: master
Are you sure you want to change the base?
Conversation
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.
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-946 |
There are different ways how I could imagine users to benefit from a
Anecdote: there was an There's also the question, should the notify callback receive information about a) name of the field, b) type of the value, c) the value itself? Probably not all of them (since that defeats genericity over fields), but I could imagine that someone having 10 bool fields would like to know which one was toggled. Can we not achieve the same by providing a more flexible callback for #[derive(GodotClass)]
#[class(base=Node2D, init, tool)]
pub struct HealthBar {
#[var(get, set = custom_set)]
width: f32,
#[var(get, set = custom_set)]
height: f32,
}
impl HealthBar {
// old way:
fn hardcoded_set(value: f32) {...}
// new possible way:
fn custom_set<T>(value: PropertyUpdate<T>) {
// may have meta-info about property name, variant etc
}
} I'm not sure if The advantage of such a mechanism as opposed to |
That's some good points. If you have a generalized struct PropertyUpdate {
backing_field: &mut T,
new_value: T,
field_name: String
} then user-code could look like this: fn custom_set<T>(value: PropertyUpdate<T>) {
// pre-set checks
// actually write to backing field
*value.backing_field = value.new_value;
// post-set actions
if value.field_name == "a" || value.field_name == "b" { /* ... */ }
} That's pseudo code written outside the editor, I'm not sure how to handle that with regard to lifetimes etc; especially since you very likely want An struct PropertyUpdate {
prev_value: T,
new_value: T,
field_name: String
}
fn custom_set<T>(&mut self, value: PropertyUpdate<T>) {
if value.field_name == "a" || value.field_name == "b" { /* ... */ }
} I'll see if I can spend some time of my life learning more about rust lifetimes |
Yes, you're correct that referencing However, it's possible that Or, even more flexible, it could return the exclusive reference to the field, given an exclusive reference to the struct: // C = surrounding class, T = field type
struct PropertyUpdate<'a, C, T> {
new_value: T,
field_name: &'a str, // might also be &'a StringName, depending on what's available
get_field_mut: fn(&mut C) -> &mut T,
}
impl<'a, C, T> PropertyUpdate<'a, C, T> {
// Slightly more convenient access + encapsulation.
pub fn default_set(&self, this: &mut C) {
let field: &mut T = (self.get_field_mut)(this);
*field = self.new_value;
// TODO see if we need to move out, or can have &T reference...
}
// For cases where someone modifies the input first (e.g. clamp to a range).
pub fn set(&self, this: &mut C, new_value: T) {
let field: &mut T = (self.get_field_mut)(this);
*field = new_value;
}
} And then: fn custom_set<T>(&mut self, update: PropertyUpdate<T>) {
// pre-set checks
// actually write to backing field
update.default_set(self);
// alternatively, if someone changes T first
let corrected_value = clamp(update.value());
update.set(self, corrected_value);
// post-set actions
...
} |
I'd argue that equally common as "notify on changed" (after-set) logic is "validate before update" (before-set) or "adjust before update" (like bringing a value into a valid range). Having a dedicated callback for each possible workflow may quickly get out of hand. And if someone has a slightly different requirement (e.g. take not just input, but also previous value into account), then the user is again stuck 🙂 so a more general design may be more powerful, even if it means a bit more code for defining such a "rich setter" once. |
With your example it can be done like so: impl HealthBar {
/// Sets attribute, then queue rerender.
fn set_then_queue_rerender<T: PartialEq>(
&mut self,
t: T,
f: impl FnOnce(&mut Self) -> &mut T,
) {
let p = f(self);
if *p != t {
*p = t;
self.base_mut().queue_rerender();
}
}
}
#[godot_api]
impl HealthBar {
// Example
#[func]
pub fn set_filled(&mut self, new_value: f32) {
self.set_then_queue_rerender(new_value, |v| &mut v.filled);
}
} Not the cleanest, but it does abstract after set behavior. I'm also curious with custom property. For example, in my code there are "phantom" properties (returns zero value and ignore setter) where it's custom getter/setter is pretty expensive (serialization/deserialization with caching). Will it call getter for every setter? |
@0x53A What do you think about my comment? 🙂 It looks like you added a commit implementing something like this, could you elaborate a bit? |
yep it's basically what it looks like - after you demonstrated how to implement it using a function to return the exclusive reference to the field, implementing it was easy. So now the bikeshedding can begin. (Quick note, I'll clean up the implementation next week.) I believe we are in agreement that something like
Question 1, what should be the user-facing attribute name? I actually kinda like Question 2, what should be the signature of the user-defined callback function f? A, a single struct with both a reference to the field and the new value // defined by gdext
pub struct PropertyUpdate<'a, C, T> {
pub new_value: T,
pub field_name: &'a str,
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;
}
}
// ------------------
// defined by the user
fn custom_set<T>(&mut self, update: PropertyUpdate<Self, T>) {
// pre-set checks
update.set(self);
// or, alternatively ...
// update.set_custom(some_other_value);
// post-set actions
} B, a struct with just the reference to the field, and a second argument for the new value // defined by gdext
pub struct PropertyRef<'a, C, T> {
pub field_name: &'a str,
pub get_field_mut: fn(&mut C) -> &mut T,
}
impl<C, T> PropertyRef<'_, C, T> {
pub fn set(self, obj: &mut C, new_value: T) {
*(self.get_field_mut)(obj) = new_value;
}
}
// ------------------
// defined by the user
fn custom_set<T>(&mut self, field_ref:PropertyRef<Self, T>, new_value: T) {
// pre-set checks
field_ref.set(self, new_value);
// post-set actions
} I very slightly prefer B, because it does not distinguish between setting the value as passed from godot, or setting a custom value; but I could also count that exact same argument as pro A, because maybe you do want to emphasize whether you're just passing through the value, or modifying it inside the setter. Question 3, should only I'd prefer b, "both", but I can see your argument, "why stop at two". |
Not sure if I have a great name though. Some thoughts:
I'll write a separate answer about the rest, as it's going to be a bit involved... |
Hm, that is a harder problem than I initially assumed 😅 My first instinct was to tend towards (A), because it's easier to get one parameter to have the right type than two: fn custom_set<T>(&mut self, field_ref: PropertyRef<Self, Vector2>, new_value: Vector2i)
// During refactor, updated one type but not the other -> maybe weird compile error?
// Also less repetition + more integrated functionality in PropertyUpdate. However, there's one problem:
It is technically possible to work with public fields and encourage destructuring: fn custom_set<T>(&mut self, update: PropertyUpdate<Self, T>) {
let PropertyUpdate { new_value, get_field_mut, .. } = update;
let new_value = adjust(new_value);
*get_field_mut(self) = new_value;
}
// or:
fn custom_set<T>(&mut self, update: PropertyUpdate<Self, T>) {
update.set_with(|new_value| adjust(new_value));
// set_with takes self, i.e. update is no longer accessible after here.
} Comparison of (A) and (B)In summary, the trade-offs are: (B)
Possible API: // assuming no public fields
impl PropertyRef<'a, C, T> {
fn set(&mut self, class: &mut C, new_value: T);
fn get(&self, class: &C) -> T;
fn field_name(&self) -> &'a str;
} (A)
Possible API: // assuming no public fields
impl PropertyRef<'a, C, T> {
fn set(self, class: &mut C, different_value: T);
fn set_unchanged(self, class: &mut C);
fn set_with(self, class: &mut C, update: impl FnOnce(T) -> T);
fn field_name(&self) -> &'a str;
}
// however forcing user code into closures can be annoying/limiting... Other aspectsOne thing we haven't considered yet: in the future I'd like to provide a ZST dummy field like gdnative's I'm not sure how I feel about public fields; it takes away our freedoms of changing implementation, optimizing things, etc. At least the struct should be ConclusionI don't see one approach (A) or (B) that is clearly superior, both have their trade-offs. But (B) feels slightly simpler as in "less overengineered", and at the same time more flexible. It comes at a cost of a bit more verbosity (repetition of Opinions? |
About the naming, it might be possible to overload Unrelated to this issue, would you be to open to me changing the way Currently the name of I propose the following: The setter is always generated, with the Godot name That is, for the user code pub struct MyNode {
#[var(get, set=set_foo)]
foo: bool,
}
impl MyNode {
fn set_foo(&mut self, new_value: bool) { /* */ }
} The compiler would generate: impl MyNode {
#[doc(hidden)]
#[func(rename=set_foo)]
fn __internal_set_foo(&mut self, new_value: bool) {
self.set_foo(new_value);
} That would also solve #863 Yes, this is a big breaking change, but I believe it is straight better, and it's still early enough to do it. |
I found that adding complexity and brittleness for tiny naming tweaks is rarely worth it -- we tend to overestimate how big of an impact these things have. Especially as long as things are consistent. In this particular case, having distinct names can even be good for documentation purposes -- one immediately sees that the receiving end isn't a regular setter, but a generalized one. Also, I'd rather have bad naming than extra complexity 😉
You're right, it's a runtime error in Godot:
Interesting idea! One issue is that now we taught users to put
It solves it by having a second function that's registered with a different name, thus exposing two identical APIs to Godot. I'm not sure if this is intended -- as a user, if I rename my setter, I'd expect to have it renamed and not copied. This would also pollute Godot docs, for example 🤔 So I think both #863 and avoiding runtime errors (or moving them to compile time) are worthwhile goals, but we should maybe iterate a bit on the way we solve them 🙂 Edit: see my comment in #863 (comment) for a potential idea. We can gladly continue discussing there, as I think the two problems are closely related. |
I feel like the easiest way to deal with the name problem is to just call it |
I've noticed that I have the following pattern, where I need to do some action after one of many vars has been changed:
That's ... quite verbose.
Instead I'd like to propose the following:
Here
notify
is a new option. If it is set, the autogenerated setter looks like this:This currently requires that the type of the property implements
Eq
. Another option would be to unconditionally call the notify fn.Is this something that makes sense to include in gdext for you? I'm totally fine if you say no and close this PR.
If yes, what do you think about the design, w.r.t. naming and equality?