-
-
Notifications
You must be signed in to change notification settings - Fork 218
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 OnEditor<T>
, remove impl<T> Export for Gd<T>
and DynGd<T, D>
#1051
base: master
Are you sure you want to change the base?
Conversation
After some tweaking I decided to support (some) built-ins as well. Alright, long story short, this PR removes implementation of the Additional logic is being run before I could not find proper blanket implementation that would satisfy all the GodotTypes, so we are stuck with three different impls – blanket implementations for It ain't that bad tho. For now I also found & removed the bug that allowed to use Node-based classes as an |
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1051 |
OnEditor<T>
, remove impl<T> Export for Gd<T>
OnEditor<T>
, remove impl<T> Export for Gd<T>
OnEditor<T>
, remove impl<T> Export for Gd<T>
and DynGd<T, D>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot! 👍
I could not find proper blanket implementation that would satisfy all the GodotTypes, so we are stuck with three different impls – blanket implementations for
Gd
andDynGd
and concrete implementations for built-ins. I don't like it and explored few alternatives (I had some success with approach similar to disjointed impls) and all of them were even worse :I. DoubledVar
implementations are bad and I would gladly accept any suggestion for improvement 🫡 .
What were the concrete problems you faced, e.g. for implementing
impl<T> Var for OnEditor<T> { ... }
As mentioned in one of the comments, I don't think OnEditor
should support general late-init functionality. Spontaneously speaking, I'd keep the overlap in functionality with OnReady
to an absolute minimum.
Regarding integration with ready()
, can we not just panic if it's not yet initialized at that point? Or what was the reason for allowing manual initialization inside ready()
, when OnReady
can already do that?
impl<T: Export> Export for Array<T> | ||
where | ||
T: ArrayElement + Export, | ||
T: ArrayElement, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this change? Bounds shouldn't be declared in mixed places.
impl<T: GodotClass> Export for Array<Gd<T>> | ||
where | ||
T: Bounds<Exportable = bounds::Yes>, | ||
Gd<T>: ArrayElement, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this bound necessary?
That trait is implemented for all Gd<T>
:
gdext/godot-core/src/obj/gd.rs
Line 795 in ac8b1da
impl<T: GodotClass> ArrayElement for Gd<T> { |
godot-core/src/obj/dyn_gd.rs
Outdated
where | ||
T: GodotClass + Bounds<Exportable = bounds::Yes>, | ||
D: ?Sized + 'static, | ||
{ | ||
fn export_hint() -> PropertyHintInfo { | ||
PropertyHintInfo { | ||
hint_string: get_dyn_property_hint_string::<D>(), | ||
..<Gd<T> as Export>::export_hint() | ||
..Gd::<T>::export_hint() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please keep the explicit trait qualification, it makes code a bit more explicit (and in rare cases, may disambiguate).
|
||
#[doc(hidden)] | ||
fn as_node_class() -> Option<ClassName> { | ||
Gd::<T>::as_node_class() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also here:
Gd::<T>::as_node_class() | |
<Gd<T> as Export>::as_node_class() |
There are a few more places; but you can also wait in case we find a way to consolidate the trait impls across OnEditor
.
godot-core/src/obj/dyn_gd.rs
Outdated
#[doc(hidden)] | ||
#[allow(clippy::derivable_impls)] | ||
impl<T, D> Default for OnEditor<DynGd<T, D>> | ||
where | ||
T: GodotClass, | ||
D: ?Sized + 'static, | ||
{ | ||
fn default() -> Self { | ||
OnEditor::null() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding #[doc(hidden)]
:
Default
is part of the public API -- we should not implement it if we don't want to expose it.
godot-core/src/obj/oneditor.rs
Outdated
/// #[godot_api] | ||
/// impl INode for MyClass { | ||
/// fn ready(&mut self) { | ||
/// // Field `required_with_default` can be either default value - specified in `#[init]` or value set via the Godot Editor. | ||
/// assert_eq!(self.required_with_default.get_class(), GString::from("Node")); | ||
/// // Will always be valid and must be set via editor – an additional check is being run before ready to make sure that given value can't be null. | ||
/// let some_variant = self.editor_field.get_meta("SomeName"); | ||
/// } | ||
/// } | ||
/// ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check the generated PR output per bot link -- the code needs horizontal scrolling, and some statements are not aligned 🙂
https://godot-rust.github.io/docs/gdext/pr-1051/godot/obj/struct.OnEditor.html
godot-core/src/obj/oneditor.rs
Outdated
enum OnEditorState<T> { | ||
// Represents uninitialized, null value. | ||
Null, | ||
// Represents initialized, invalid value. | ||
Uninitialized(T), | ||
Initialized(T), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
enum OnEditorState<T> { | |
// Represents uninitialized, null value. | |
Null, | |
// Represents initialized, invalid value. | |
Uninitialized(T), | |
Initialized(T), | |
} | |
enum OnEditorState<T> { | |
/// Uninitialized null value. | |
Null, | |
/// Uninitialized state, but with a value marked as invalid. | |
Uninitialized(T), | |
/// Initialized with a value. | |
Initialized(T), | |
} |
godot-core/src/registry/property.rs
Outdated
impl_property_by_godot_convert!(Rect2, oneditor); | ||
impl_property_by_godot_convert!(Rect2i, oneditor); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also here, we should see if this is really necessary, or if a blanket impl is possible.
These individual trait impl
are likely not great for compile times, and they also clutter the OnEditor
doc page massively:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let oneditor_panic_inits = { | ||
// Despite its name OnEditor shouldn't panic in the editor for tool classes. | ||
let editor_check = quote! { ::godot::classes::Engine::singleton().is_editor_hint() }; | ||
|
||
// Inform the user which fields haven't been set, instead of panicking on the very first one. Useful for debugging. | ||
let on_editor_fields_checks = all_fields | ||
.iter() | ||
.filter(|&field| field.is_oneditor) | ||
.map(|field| { | ||
let field = &field.name; | ||
let warning_message = | ||
format! { "godot-rust: OnEditor field {field} hasn't been initialized."}; | ||
quote! { | ||
if this.#field.is_invalid() { | ||
::godot::global::godot_warn!(#warning_message); | ||
is_oneditor_properly_initialized = false; | ||
} | ||
} | ||
}) | ||
.collect::<Vec<_>>(); | ||
|
||
if !on_editor_fields_checks.is_empty() { | ||
run_before_ready = true; | ||
quote! { | ||
fn __are_oneditor_fields_initalized(this: &#class_name) -> bool { | ||
if #editor_check { | ||
return true; | ||
} | ||
|
||
let mut is_oneditor_properly_initialized: bool = true; | ||
#( #on_editor_fields_checks )* | ||
|
||
is_oneditor_properly_initialized | ||
} | ||
|
||
if !__are_oneditor_fields_initalized(&self) { | ||
panic!("godot-rust: OnEditor fields must be properly initialized before ready.") | ||
} | ||
} | ||
} else { | ||
TokenStream::new() | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should maybe be its own function...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved both oneditor_panic_inits
and onready_inits
to their own functions. Both return TokenStreams
and before_ready is being generated only if any of these TokenStreams isn't empty
I fixed it in meanwhile 🫡. Long story short, before figuring out the correct bounds for all the types, I've been entangled in conflicting implementations of traits (Gd implements GodotType etc). The correct approach turned out to be fairly simple – I just had to add one extra marker trait into the mix AND tell compiler that Negative trait bounds and specialization would come in handy here – albeit they are still work in progress.
EDIT: The marker Trait is called |
8bc1c2c
to
4540e39
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for the update! Added some comments.
I'm not really sure about the name "uninit" in OnEditor::unit()
+ #[init(uninit = ...)]
. First, people probably know this from mem::MaybeUninit::uninit()
, where it has a totally different meaning (actually uninitialized memory).
Second, it doesn't express that the value is not only "uninitialized", but also a "invalid marker" for value that's used to distinguish valid/invalid state. In programming, sentinel value probably comes close to describe it.
I don't really have a great alternative though, maybe we can brainstorm a bit?
- Single word:
unset
,invalid
, ... - Marker:
invalid_marker
,uninit_marker
- Sentinel:
uninit_sentinel
, ... - Descriptive:
needs_init
- Exclusion:
except
,exclude
,excluded
,exempt
,unless
...
(Perfect would be a single word expressing both "needs initialization" and "marks an invalid value", but that's very specific...)
godot-core/src/obj/dyn_gd.rs
Outdated
@@ -495,18 +495,69 @@ where | |||
} | |||
} | |||
|
|||
impl<T, D> Export for DynGd<T, D> | |||
#[allow(clippy::derivable_impls)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please quickly add a comment why this #[allow()]
is necessary, and why we don't derive Default
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed, Clippy no longer complain about it after adding blanket implementation for OnEditor.
godot-core/src/obj/gd.rs
Outdated
} | ||
} | ||
|
||
#[allow(clippy::derivable_impls)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also here
godot-core/src/obj/oneditor.rs
Outdated
fn deref(&self) -> &Self::Target { | ||
match &self.inner { | ||
OnEditorState::Null | OnEditorState::Uninitialized(_) => { | ||
panic!("godot-rust: OnEditor field hasn't been initialized.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for godot-rust:
prefix in panics, the handler already prints this when necessary.
Also in other places.
fn as_node_class() -> Option<ClassName> { | ||
PropertyHintInfo::object_as_node_class::<T>() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be #[doc(hidden)]
everywhere. Unfortunately rustdoc doesn't seem to be smart enough to pick up the attribute from the trait and apply it to all impls.
godot-core/src/meta/property_info.rs
Outdated
#[doc(hidden)] | ||
pub fn object_as_node_class<T: GodotClass + Bounds<Exportable = bounds::Yes>>( | ||
) -> Option<ClassName> { | ||
T::inherits::<classes::Node>().then(|| T::class_name()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the bounds are nested, or get long enough to require line break, please use a where
clause.
godot-core/src/registry/property.rs
Outdated
/// Marker trait to identify non-nullable `GodotType`s that can be directly used with `#[export]` and `#[var]`. | ||
pub trait DirectExport {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great idea to address the "too many impls" problem, thanks! 👍
I'm not sure if the term "direct" is very precise. What about BuiltinExport
-- in practice, all types for which the trait is implemented are builtins, no?
An alternative would also be to check this via Bounds
associated type -- like the Exportable
type here, it could be something like BuiltinExportable
or so.
gdext/godot-core/src/obj/bounds.rs
Lines 100 to 104 in 027a969
/// True if *either* `T: Inherits<Node>` *or* `T: Inherits<Resource>` is fulfilled. | |
/// | |
/// Enables `#[export]` for those classes. | |
#[doc(hidden)] | |
type Exportable: Exportable; |
These are all just ideas, I'm fine with getting the basics sorted in this PR and then refine later 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried it via Bounds and it seems to work – therefore DirectExport
is now a part of the GodotType as an associated type BuiltinExportable: Exportable
;
The question is if we want to move declaration of Exportable
and bounds::Yes/No
, for now on I left it in obj/bounds.rs
🤷..
Earlier on I couldn't get it working 🤔 (probably because I haven't specified both OnEditor<T>: GodotConvert<Via = T>
and T: GodotConvert<Via = T>
in required bounds).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I meant to use an associated type inside Bounds
, not GodotType
. But I missed that Bounds
doesn't support non-class types 🤦♂️ But I don't think GodotType
is a good idea, this is a very core type that almost everything must implement, so adding more requirements there has far-reaching consequences.
Probably the original proposal of yours was indeed better. There are some marker traits in godot::meta
already; we could possibly add BuiltinExportable
there?
Also, where does Variant
stand in this? Like object, it has a null state (Variant::nil()
), should OnEditor
expect that it's non-nil as well?
if let Some(first) = onready_fields.next() { | ||
quote! { | ||
{ | ||
let base = <Self as godot::obj::WithBaseField>::to_gd(self).upcast(); | ||
#first | ||
#( #onready_fields )* | ||
} | ||
} | ||
} else { | ||
TokenStream::new() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is only the first field read here?
Or what's the point of mapping/filtering the whole slice and then discard all but one element?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I blindly copied&pasted this code to new function 😅 – frankly I had little issues as well, since everything else is using collect and Vec 🤔. It just checks if iterator is empty by fetching the very first element.
I rewrote it to use collect and Vec instead.
if #editor_check { | ||
return true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add comments about these semantics -- they are not obvious to someone reading the code in the future.
let mut is_oneditor_properly_initialized: bool = true; | ||
#( #on_editor_fields_checks )* | ||
|
||
is_oneditor_properly_initialized |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also here -- "properly" sounds vague and sloppy; if there isn't a more precise name, at least a comment should be used to clarify.
|
||
#[derive(GodotClass)] | ||
#[class(init)] | ||
struct RefcDynGdExporter { | ||
struct RefcHasDynGdVar { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
struct RefcHasDynGdVar { | |
struct RefcDynGdVarDeclarer { |
maybe? 😅
6b5fdad
to
66646bb
Compare
I cleaned PR, here is short, and I hope, non-chaotic, coherent description. What problem this PR is aiming to solve?
Modus operandi of this PR has been documented in newly created gdext/godot-core/src/registry/property.rs Lines 85 to 99 in 608c3f6
How does this PR solves aforementioned issues?
Docs has been written to make purpose and use of these features clear. What are extra features implemented in this PR?
What are some open issues?
What still needs to be done?
|
48162fa
to
608c3f6
Compare
…nGd` and `Gd` - Add `OnEditor<T>` - a wrapper that allows to export non-nullable types which must be nullable in the Editor. - Remove Export and Var implementations for `DynGd<T, D>` and `Gd<T>` - it should be provided by algebraic types instead (OnEditor and Option). - Add marker trait `BuiltinExport` which informs if given type can be safely and conveniently used in a `#[export]` directly. - Create implementations for `OnEditor<Gd<T>>` and `OnEditor<DynGd<D, T>>`. - Create proper blanket implementation for Godot Types other than Gd<T> - Inform user about available use cases for `OnEditor<T>` in associated docs - Move Gd<T>::export_info to PropertyHintInfo::export_gd. - Add before_ready check for OnEditor fields to panic and warn user in case if values haven't been set. - FIX: `Option<Gd<T>>` and `OnEditor<Gd<T>>` can no longer be used as an Export for Resource-based classes for `T` Inheriting Node.
608c3f6
to
fd5bdf2
Compare
Closes #892
OnEditor<T>
.impl<T> Export for Gd<T>
andDynGd<T, D>
- it should be provided by algebraic types instead.Option<Gd<T: Inherits<Node>>>
andOnEditor<Gd<T: Inherits<Node>>>
can no longer be used as an Export for Resource-based classes.Draft, since I have to give it a proper test run in real-world environment.