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

Add get/set to property attribute to specify custom getter/setter #841

Merged
merged 3 commits into from
Feb 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
110 changes: 110 additions & 0 deletions gdnative-core/src/export/property.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! Property registration.
use std::marker::PhantomData;

use accessor::{Getter, RawGetter, RawSetter, Setter};
use invalid_accessor::{InvalidGetter, InvalidSetter};
Expand Down Expand Up @@ -322,6 +323,115 @@ impl PropertyUsage {
}
}

/// Placeholder type for exported properties with no backing field.
///
/// This is the go-to type whenever you want to expose a getter/setter to GDScript, which
/// does not directly map to a field in your struct. Instead of adding a useless field
/// of the corresponding type (which needs initialization, extra space, etc.), you can use
/// an instance of this type as a placeholder.
///
/// `Property` is a zero-sized type (ZST) which has exactly one value: `Property::default()`.
/// It implements most of the basic traits, which allows its enclosing struct to remain
/// composable and derive those traits itself.
///
/// ## When to use `Property<T>` instead of `T`
///
/// The following table shows which combinations of `#[property]` attributes and field types are allowed.
/// In this context, `get` and `set` behave symmetrically, so only one of the combinations is listed.
/// Furthermore, `get_ref` can be used in place of `get`, when it appears with a path.
///
/// Field type ➡ <br> Attributes ⬇ | bare `T` | `Property<T>`
/// ------------------------------------------|-------------------------------|-----------------------------
/// `#[property]` | ✔️ default get + set | ❌️
/// `#[property(get, set)]` _(same as above)_ | ✔️ default get + set | ❌️
/// `#[property(get)]` | ✔️ default get (no set) | ❌️
/// `#[property(get="path")]` | ⚠️ custom get (no set) | ✔️ custom get (no set)
/// `#[property(get="path", set)]` | ✔️ custom get, default set | ❌️
/// `#[property(get="path", set="path")]` | ⚠️ custom get + set | ✔️ custom get + set
///
/// "⚠️" means that this attribute combination is allowed for bare `T`, but you should consider
/// using `Property<T>`.
///
/// Since there is no default `get` or `set` in these cases, godot-rust will never access the field
/// directly. In other words, you are not really exporting _that field_, but linking its name and type
/// (but not its value) to the specified get/set methods.
///
/// To decide when to use which:
/// * If you access your field as-is on the Rust side, use bare `T`.<br>
/// With a `Property<T>` field on the other hand, you would need to _additionally_ add a `T` backing field.
/// * If you don't need a backing field, use `Property<T>`.<br>
/// This is the case whenever you compute a result dynamically, or map values between Rust and GDScript
/// representations.
///
/// ## Examples
///
/// Read/write accessible:
/// ```no_run
/// # use gdnative::prelude::*;
/// #[derive(NativeClass)]
/// # #[no_constructor]
/// struct MyObject {
/// #[property]
/// color: Color,
/// }
/// ```
///
/// Read-only:
/// ```no_run
/// # use gdnative::prelude::*;
/// #[derive(NativeClass)]
/// # #[no_constructor]
/// struct MyObject {
/// #[property(get)]
/// hitpoints: f32,
/// }
/// ```
///
/// Read-write, with validating setter:
/// ```no_run
/// # use gdnative::prelude::*;
/// # fn validate(s: &String) -> bool { true }
/// #[derive(NativeClass)]
/// # #[no_constructor]
/// struct MyObject {
/// #[property(get, set = "Self::set_name")]
/// player_name: String,
/// }
///
/// #[methods]
/// impl MyObject {
/// fn set_name(&mut self, _owner: TRef<Reference>, name: String) {
/// if validate(&name) {
/// self.player_name = name;
/// }
/// }
/// }
/// ```
///
/// Write-only, no backing field, custom setter:
/// ```no_run
/// # use gdnative::prelude::*;
/// #[derive(NativeClass)]
/// # #[no_constructor]
/// struct MyObject {
/// #[property(set = "Self::set_password")]
/// password: Property<String>,
/// }
///
/// #[methods]
/// impl MyObject {
/// fn set_password(&mut self, _owner: TRef<Reference>, password: String) {
/// // securely hash and store password
/// }
/// }
/// ```

// Note: traits are mostly implemented to enable deriving the same traits on the enclosing struct.
#[derive(Copy, Clone, Debug, Default, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct Property<T> {
_marker: PhantomData<T>,
}

mod impl_export {
use super::*;

Expand Down
25 changes: 21 additions & 4 deletions gdnative-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,21 @@ pub fn profiled(meta: TokenStream, input: TokenStream) -> TokenStream {
/// Call hook methods with `self` and `owner` before and/or after the generated property
/// accessors.
///
/// - `get` / `get_ref` / `set`
///
/// Configure getter/setter for property. All of them can accept a path to specify a custom
/// property accessor. For example, `#[property(get = "Self::my_getter")]` will use
/// `Self::my_getter` as the getter.
///
/// The difference of `get` and `get_ref` is that `get` will register the getter with
/// `with_getter` function, which means your getter should return an owned value `T`, but
/// `get_ref` use `with_ref_getter` to register getter. In this case, your custom getter
/// should return a shared reference `&T`.
///
/// Situations with custom getters/setters and no backing fields require the use of the
/// type [`Property<T>`][gdnative::export::Property]. Consult its documentation for
/// a deeper elaboration of property exporting.
///
/// - `no_editor`
///
/// Hides the property from the editor. Does not prevent it from being sent over network or saved in storage.
Expand Down Expand Up @@ -379,19 +394,21 @@ pub fn derive_native_class(input: TokenStream) -> TokenStream {
let derive_input = syn::parse_macro_input!(input as DeriveInput);

// Implement NativeClass for the input
native_script::derive_native_class(&derive_input).map_or_else(
let derived = native_script::derive_native_class(&derive_input).map_or_else(
|err| {
// Silence the other errors that happen because NativeClass is not implemented
let empty_nativeclass = native_script::impl_empty_nativeclass(&derive_input);
let err = err.to_compile_error();

TokenStream::from(quote! {
quote! {
#empty_nativeclass
#err
})
}
},
std::convert::identity,
)
);

TokenStream::from(derived)
}

#[proc_macro_derive(ToVariant, attributes(variant))]
Expand Down