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 OnReady::node() + #[init(node = "...")] attribute #807

Merged
merged 1 commit into from
Jul 27, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 72 additions & 8 deletions godot-core/src/obj/onready.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

use crate::builtin::NodePath;
use crate::classes::Node;
use crate::meta::GodotConvert;
use crate::obj::{Gd, GodotClass, Inherits};
use crate::registry::property::{PropertyHintInfo, Var};
use std::mem;

Expand All @@ -17,9 +20,10 @@ use std::mem;
///
/// `OnReady<T>` should always be used as a field. There are two modes to use it:
///
/// 1. **Automatic mode, using [`new()`](Self::new).**<br>
/// Before `ready()` is called, all `OnReady` fields constructed with `new()` are automatically initialized, in the order of
/// declaration. This means that you can safely access them in `ready()`.<br><br>
/// 1. **Automatic mode, using [`new()`](OnReady::new), [`from_base_fn()`](OnReady::from_base_fn) or
/// [`node()`](OnReady::<Gd<T>>::node).**<br>
/// Before `ready()` is called, all `OnReady` fields constructed with the above methods are automatically initialized,
/// in the order of declaration. This means that you can safely access them in `ready()`.<br><br>
/// 2. **Manual mode, using [`manual()`](Self::manual).**<br>
/// These fields are left uninitialized until you call [`init()`][Self::init] on them. This is useful if you need more complex
/// initialization scenarios than a closure allows. If you forget initialization, a panic will occur on first access.
Expand All @@ -36,21 +40,27 @@ use std::mem;
/// [option]: std::option::Option
/// [lazy]: https://docs.rs/once_cell/1/once_cell/unsync/struct.Lazy.html
///
/// # Example
/// # Requirements
/// - The class must have an explicit `Base` field (i.e. `base: Base<Node>`).
/// - The class must inherit `Node` (otherwise `ready()` would not exist anyway).
///
/// # Example - user-defined `init`
/// ```
/// use godot::prelude::*;
///
/// #[derive(GodotClass)]
/// #[class(base = Node)]
/// struct MyClass {
/// base: Base<Node>,
/// auto: OnReady<i32>,
/// manual: OnReady<i32>,
/// }
///
/// #[godot_api]
/// impl INode for MyClass {
/// fn init(_base: Base<Node>) -> Self {
/// fn init(base: Base<Node>) -> Self {
/// Self {
/// base,
/// auto: OnReady::new(|| 11),
/// manual: OnReady::manual(),
/// }
Expand All @@ -65,10 +75,54 @@ use std::mem;
/// assert_eq!(*self.manual, 22);
/// }
/// }
/// ```
///
/// # Example - macro-generated `init`
/// ```
/// use godot::prelude::*;
///
/// #[derive(GodotClass)]
/// #[class(init, base = Node)]
/// struct MyClass {
/// base: Base<Node>,
/// #[init(node = "ChildPath")]
/// auto: OnReady<Gd<Node2D>>,
/// #[init(default = OnReady::manual())]
/// manual: OnReady<i32>,
/// }
///
/// #[godot_api]
/// impl INode for MyClass {
/// fn ready(&mut self) {
/// // self.node is now ready with the node found at path `ChildPath`.
/// assert_eq!(self.auto.get_name(), "ChildPath".into());
///
/// // self.manual needs to be initialized manually.
/// self.manual.init(22);
/// assert_eq!(*self.manual, 22);
/// }
/// }
/// ```
pub struct OnReady<T> {
state: InitState<T>,
}

impl<T: GodotClass + Inherits<Node>> OnReady<Gd<T>> {
/// Variant of [`OnReady::new()`], fetching the node located at `path` before `ready()`.
///
/// This is the functional equivalent of the GDScript pattern `@onready var node = $NodePath`.
///
/// # Panics
/// - If `path` does not point to a valid node.
///
/// Note that the panic will only happen if and when the node enters the SceneTree for the first time
/// (i.e.: it receives the `READY` notification).
pub fn node(path: impl Into<NodePath>) -> Self {
let path = path.into();
Self::from_base_fn(|base| base.get_node_as(path))
}
}

impl<T> OnReady<T> {
/// Schedule automatic initialization before `ready()`.
///
Expand All @@ -82,6 +136,14 @@ impl<T> OnReady<T> {
pub fn new<F>(init_fn: F) -> Self
where
F: FnOnce() -> T + 'static,
{
Self::from_base_fn(|_| init_fn())
}

/// Variant of [`OnReady::new()`], allowing access to `Base` when initializing.
pub fn from_base_fn<F>(init_fn: F) -> Self
where
F: FnOnce(&Gd<Node>) -> T + 'static,
{
Self {
state: InitState::AutoPrepared {
Expand Down Expand Up @@ -126,7 +188,7 @@ impl<T> OnReady<T> {
///
/// # Panics
/// If the value is already initialized.
pub(crate) fn init_auto(&mut self) {
pub(crate) fn init_auto(&mut self, base: &Gd<Node>) {
// Two branches needed, because mem::replace() could accidentally overwrite an already initialized value.
match &self.state {
InitState::ManualUninitialized => return, // skipped
Expand All @@ -147,7 +209,7 @@ impl<T> OnReady<T> {
};

self.state = InitState::Initialized {
value: initializer(),
value: initializer(base),
};
}
}
Expand Down Expand Up @@ -214,9 +276,11 @@ impl<T: Var> Var for OnReady<T> {
// ----------------------------------------------------------------------------------------------------------------------------------------------
// Implementation

type InitFn<T> = dyn FnOnce(&Gd<Node>) -> T;

enum InitState<T> {
ManualUninitialized,
AutoPrepared { initializer: Box<dyn FnOnce() -> T> },
AutoPrepared { initializer: Box<InitFn<T>> },
AutoInitializing, // needed because state cannot be empty
Initialized { value: T },
}
4 changes: 2 additions & 2 deletions godot-core/src/private.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ pub struct ClassConfig {
// ----------------------------------------------------------------------------------------------------------------------------------------------
// Capability queries and internal access

pub fn auto_init<T>(l: &mut crate::obj::OnReady<T>) {
l.init_auto();
pub fn auto_init<T>(l: &mut crate::obj::OnReady<T>, base: &crate::obj::Gd<crate::classes::Node>) {
l.init_auto(base);
}

#[cfg(since_api = "4.3")]
Expand Down
65 changes: 54 additions & 11 deletions godot-macros/src/class/derive_godot_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
}
}
} else {
quote! {}
TokenStream::new()
};

let deprecated_base_warning = if fields.has_deprecated_base {
Expand Down Expand Up @@ -226,15 +226,29 @@ fn make_user_class_impl(
is_tool: bool,
all_fields: &[Field],
) -> (TokenStream, bool) {
let onready_field_inits = all_fields
.iter()
.filter(|&field| field.is_onready)
.map(|field| {
let field = &field.name;
let onready_inits = {
let mut onready_fields = all_fields
.iter()
.filter(|&field| field.is_onready)
.map(|field| {
let field = &field.name;
quote! {
::godot::private::auto_init(&mut self.#field, &base);
}
});

if let Some(first) = onready_fields.next() {
quote! {
::godot::private::auto_init(&mut self.#field);
{
let base = <Self as godot::obj::WithBaseField>::to_gd(self).upcast();
#first
#( #onready_fields )*
}
}
});
} else {
TokenStream::new()
}
};

let default_virtual_fn = if all_fields.iter().any(|field| field.is_onready) {
let tool_check = util::make_virtual_tool_check();
Expand Down Expand Up @@ -267,7 +281,7 @@ fn make_user_class_impl(
}

fn __before_ready(&mut self) {
#( #onready_field_inits )*
#onready_inits
}

#default_virtual_fn
Expand Down Expand Up @@ -409,8 +423,37 @@ fn parse_fields(
}

// #[init(default = expr)]
let default = parser.handle_expr("default")?;
field.default = default;
if let Some(default) = parser.handle_expr("default")? {
field.default = Some(default);
}

// #[init(node = "NodePath")]
if let Some(node_path) = parser.handle_expr("node")? {
if !field.is_onready {
return bail!(
parser.span(),
"The key `node` in attribute #[init] requires field of type `OnReady<T>`\n\
Help: The syntax #[init(node = \"NodePath\")] is equivalent to \
#[init(default = OnReady::node(\"NodePath\"))], \
which can only be assigned to fields of type `OnReady<T>`"
);
}

if field.default.is_some() {
return bail!(
parser.span(),
"The key `node` in attribute #[init] is mutually exclusive with the key `default`\n\
Help: The syntax #[init(node = \"NodePath\")] is equivalent to \
#[init(default = OnReady::node(\"NodePath\"))], \
both aren't allowed since they would override each other"
);
}

field.default = Some(quote! {
OnReady::node(#node_path)
});
}

parser.finish()?;
}

Expand Down
Loading
Loading