Skip to content

Commit

Permalink
Merge #841
Browse files Browse the repository at this point in the history
841: Add `get`/`set` to `property` attribute to specify custom getter/setter r=Bromeon a=Bogay

Fix #547 


Co-authored-by: bogay <pojay11523@gmail.com>
Co-authored-by: Jan Haller <bromeon@gmail.com>
  • Loading branch information
3 people committed Feb 6, 2022
2 parents 1f3b19d + 44bb3f9 commit 5efb347
Show file tree
Hide file tree
Showing 6 changed files with 628 additions and 71 deletions.
110 changes: 110 additions & 0 deletions gdnative-core/src/export/property.rs
@@ -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
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

0 comments on commit 5efb347

Please sign in to comment.