-
Notifications
You must be signed in to change notification settings - Fork 211
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
Enum property with custom int discriminants #775
base: master
Are you sure you want to change the base?
Conversation
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.
Thank you for your contribution! I added a few comments.
Once your version is final, you can run cargo fmt
to auto-format everything. 🧹
let keys: Vec<String> = self.values.keys().cloned().collect(); | ||
keys.join(",").into() |
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.
It looks like you're not using the values (i.e. associated numbers), are you?
Great how you reduced the code to be much more idiomatic 🙂
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.
It looks like you're not using the values (i.e. associated numbers), are you?
I didn't aware of that before, so I read the code about export a property in gdnative, and found this line at godot-headers
, it seems that I can't export information more than enum names? If so, maybe I should find a workaround to export enum name and its corresponding value?
edit: I don't know why I can't see the preview...code is here
typedef enum {
GODOT_PROPERTY_HINT_NONE, ///< no hint provided.
GODOT_PROPERTY_HINT_RANGE, ///< hint_text = "min,max,step,slider; //slider is optional"
GODOT_PROPERTY_HINT_EXP_RANGE, ///< hint_text = "min,max,step", exponential edit
GODOT_PROPERTY_HINT_ENUM, ///< hint_text= "val1,val2,val3,etc" <--- this line
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'll reply to the whole issue, as this affects how we continue.
impl From<HashMap<String, i32>> for EnumHint { | ||
#[inline] | ||
fn from(values: HashMap<String, i32>) -> Self { | ||
EnumHint { values } | ||
} | ||
} |
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 would rather add a method in EnumHint
, e.g. the suggested with_numbers()
taking tuples. It allows to create EnumHint
from a literal syntax and is consistent with the existing new()
constructor -- there's no From
implementation for Vec<String>
either.
Construction could look as follows (this can even be added to documentation):
EnumHint::with_numbers(vec![
("one".into(), 1),
("two".into(), 2),
("seven".into(), 7),
])
@@ -116,30 +119,31 @@ where | |||
/// ``` | |||
#[derive(Clone, Eq, PartialEq, Debug, Default)] | |||
pub struct EnumHint { | |||
values: Vec<String>, | |||
values: HashMap<String, i32>, |
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.
My edit on the post came a bit late, maybe you missed it:
Unless we need properties of a map somewhere, I would keep things simple and stay with
Vec
.
It's probably better for such little-used things, as Vec
is much cheaper (performance- and memory-wise).
Your discovery means that Godot doesn't use numbers in the hint at all. Which probably makes sense, since it uses enum names (strings) to abstract from these very numbers. But it also means we cannot implement this PR as it is now -- there's no point having private numeric values inside We should probably take a step back and ask what problem we exactly want to solve. In the issue, we had this GDScript code: enum Dir { Top = -1, Bottom = 1 }
export(Dir) var dir = Dir.Bottom but this does three things:
In other words, using the enum definition and export syntax, the previous code is equivalent to this: const Dir: Dictionary = {
"Top": -1,
"Bottom": 1,
}
export(int, "Top", "Bottom") var dir = Dir["Bottom"] By the way, you can verify this easily: print("Selected value: ", dir) # 1
print("Enum: ", Dir) # {Bottom:1, Top:-1}
print("Is Dict: ", typeof(Dir) == TYPE_DICTIONARY) # true In other words, the desired functionality is already today possible with godot-rust, by exporting an int and using a list of strings as enum hint. What might add value however is if this could be done in a more convenient way -- maybe by allowing to use a Rust |
Wow, that's more complicated than my excepted before. I need more time to learn the usage of hint and export to complete this. |
Sure, don't feel forced to address this now, I think we both underestimated it 😉 Maybe I can copy the important findings to the actual issue. So maybe before doing more implementation work, let's figure out if there's something to be done at all, and if so, what. |
7b8deb9
to
ef9fb61
Compare
@Bromeon BTW, derive |
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.
Reminder that as of #964, there is now a more comprehensive solution for ToVariant
and FromVariant
of numeric enums, integrated into the ordinary derive macros, with #[variant(enum = "repr")]
. Please see if you can fit your work into the existing framework, so that we don't have to maintain two separate code paths for the same thing, one being a subset of another.
self.values | ||
.iter() | ||
.map(|(key, val)| match val { | ||
Some(val) => format!("{}:{}", key, val), | ||
None => key.clone(), | ||
}) | ||
.collect::<Vec<_>>() | ||
.join(",") | ||
.into() |
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.
The original code is iterative for a reason. While format!
, collect
and join
are indeed convenient and pretty, they perform many allocations under the hood. It's often said that premature optimization is the root of all evil, but that does not mean we need to needlessly slow things down just because.
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.
Done
gdnative-derive/src/export_enum.rs
Outdated
use super::*; | ||
|
||
#[test] | ||
fn deny_non_unit_variant() -> syn::Result<()> { |
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.
Tests for proc-macros are better modeled as UI tests, as they not only allow us to check if the behavior is correct (with a real compiler, too), but also whether the error messages we show to users are up to par.
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.
Done
gdnative-derive/src/lib.rs
Outdated
#[proc_macro_derive(ExportEnum)] | ||
pub fn derive_export_enum(input: TokenStream) -> TokenStream { |
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'm not sure if this is the best way we can present this in the API. On one hand, naming the macro Export
would be too general when it only really works on primitive enums at the present time. On the other hand, users are very unlikely to think of typing derive(ExportEnum)
by themselves when the compiler is telling them that they're missing Export
.
I don't have a concrete suggestion, but personally I'm leaning toward using Export
as the name, and relying on error messages to point users to documentation when they try to use it on something else like a struct (in which case they probably want a custom resource instead).
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.
Agree, I've rename it to Export
.
currently the implementaion is dirty. I will refactor it after verifying it can work.
the repr part should be added to change the serialization behavior.
Sorry for the very late reply. I got some time these days and fixed it. If it's OK to merge, I'll reorg it into a few commits.
The FYI, The demo project is also updated: https://github.com/Bogay/export-enum-demo |
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 for all the effort! The general design looks good to me, although there are still a number of minor concerns on the implementation details. See below.
I understand that it has been a long journey by now, so if you don't see yourself working any further on this PR, feel free to let me know if you'd like me to carry out any of the changes on your behalf.
bors try
tryBuild succeeded! The publicly hosted instance of bors-ng is deprecated and will go away soon. If you want to self-host your own instance, instructions are here. If you want to switch to GitHub's built-in merge queue, visit their help page. |
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.
A few further things. Now all that's left should be the attribute trigger discussed in the open thread above.
bors try
tryBuild succeeded! The publicly hosted instance of bors-ng is deprecated and will go away soon. If you want to self-host your own instance, instructions are here. If you want to switch to GitHub's built-in merge queue, visit their help page. |
1f9c20c
to
f46c163
Compare
f46c163
to
e055ea2
Compare
76b7773
to
7159dd9
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.
This PR should be almost done. If the code is OK, I would update the docs.
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.
Looks good to me now. The added UI tests are nice too! Please do whatever else you meant to do and squash the remaining commits. It should be good to merge after that.
Thanks for following through this entire time!
bors try
tryBuild succeeded! The publicly hosted instance of bors-ng is deprecated and will go away soon. If you want to self-host your own instance, instructions are here. If you want to switch to GitHub's built-in merge queue, visit their help page. |
@chitoyuu Sorry, I found that if |
Interesting. I think there are two options here:
I think option 1 with the soft warning would be the most friendly to users and developers, even though it doesn't adhere strictly to the correctness-by-construction principle. I think this is a case where the input is "correct enough" to be acceptable, and thus not worth the increased complexity and duplicated code paths to reject at compile time. Which one of the options do you think is best, or maybe something else? |
Currently I prefer to warn the user when explicit mappings are given to gdnative/gdnative-core/src/export/property/hint.rs Lines 363 to 367 in 6dc9721
Maybe something like this? But that looks like a hack... let hint_string = match self {
SH::Enum(e) | SH::File(e) | SH::GlobalFile(e) => {
if e.has_value() {
godot_warn!("StringHint should not be used with EnumHint with mappings");
}
e.to_godot_hint_string()
}
SH::Placeholder { placeholder } => placeholder.into(),
_ => GodotString::new(),
}; Other options (strip mappings, ignore property, or separate types) all seem to require some additional API changes and more complexity. If we allow breaking changes, I would prefer to introduce a separate type for enum hint with mapping. However, I think it is better to maintain backwards compatibility in this case. |
I suppose
That shouldn't be a more breaking a change than what's already in tree since all the public enums now have enum IntHint {
Enum(EnumHint), // old/current type without value
EnumBikeshed(EnumBikeshedHint), // new type with EnumHintEntry
// ...
}
enum StringHint {
// Keep as is
}
I agree. |
Do you have a suggestion? I'm sorry that I don't know how to move this check to somewhere like
Do you mean something like this? let hint_string = match self {
SH::Enum(e) | SH::File(e) | SH::GlobalFile(e) => {
if e.has_value() {
godot_warn!("StringHint should not be used with EnumHint with mappings");
}
// strip all values stored inside `EnumHint`
e.strip();
e.to_godot_hint_string()
}
SH::Placeholder { placeholder } => placeholder.into(),
_ => GodotString::new(),
}; I am not sure if it would be better to implement some general purpose APIs in this case. What do you think? let hint_string = match self {
SH::Enum(e) | SH::File(e) | SH::GlobalFile(e) => {
if e.iter().any(|ent| ent.value.is_some()) {
godot_warn!("StringHint should not be used with EnumHint with mappings");
}
// strip all values stored inside `EnumHint`
for ent in e.iter_mut() {
ent.value = None;
}
e.to_godot_hint_string()
}
SH::Placeholder { placeholder } => placeholder.into(),
_ => GodotString::new(),
}; |
I guess we can also add a
I think the second example is better. I don't sense any use cases for the functions outside this one case. If they ever become necessary we can always extract the code into functions later. |
Fix #764