Skip to content

Commit

Permalink
Support #[export(range = (radians_as_degrees, suffix="XX"))]
Browse files Browse the repository at this point in the history
`radians_as_degrees` was added in Godot [4.2][1] to replace `radians`
with identical functionality and a better name.  `suffix` was added in
[4.0][2] and was seemingly missed when `#[export(range)]` was first
implemented.

This is the first instance where Godot has deprecated an API and gdext
has needed to implement it.  Hopefully this will serve as a useful
template for future work.

This also fixes a previously unknown bug where a `KEY=VALUE` expression
in the third slot of `range = (...)` would yield strange error messages.

[1]: godotengine/godot#82195
[2]: godotengine/godot#50009
  • Loading branch information
fpdotmonkey committed Jul 9, 2024
1 parent 4c8ea83 commit e5ac4fc
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 30 deletions.
7 changes: 7 additions & 0 deletions godot-core/src/deprecated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ pub const fn base_attribute() {}
More information on https://github.com/godot-rust/gdext/pull/702."]
pub const fn feature_custom_godot() {}

#[cfg_attr(
since_api = "4.2",
deprecated = "Use #[export(range = (radians_as_degrees))] and not #[export(range = (radians))]. \n\
More information on https://github.com/godotengine/godot/pull/82195."
)]
pub const fn export_range_radians() {}

#[macro_export]
macro_rules! emit_deprecated_warning {
($warning_fn:ident) => {
Expand Down
1 change: 1 addition & 0 deletions godot-core/src/meta/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ impl PropertyInfo {
/// false,
/// false,
/// false,
/// Some("mm".to_string()),
/// ));
/// ```
pub fn with_hint_info(self, hint_info: PropertyHintInfo) -> Self {
Expand Down
45 changes: 36 additions & 9 deletions godot-core/src/registry/property.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,24 @@ pub mod export_info_functions {
}

// We want this to match the options available on `@export_range(..)`
/// Mark an exported numerical value to use the editor's range UI.
///
/// You'll never call this function itself, but will instead use the macro `#[export(range=(...))]`, as below. The syntax is
/// very similar to Godot's [`@export_range`](https://docs.godotengine.org/en/stable/classes/class_%40gdscript.html#class-gdscript-annotation-export-range).
/// `min`, `max`, and `step` are `f32` positional arguments, with `step` being optional and defaulting to `1.0`. The rest of
/// the arguments can be written in any order. The symbols of type `bool` just need to have those symbols written, and those of type `Option<T>` will be written as `{KEY}={VALUE}`, e.g. `suffix="px"`.
///
/// ```
/// # use godot::prelude::*;
/// #[derive(GodotClass)]
/// #[class(init, base=Node)]
/// struct MyClassWithRangedValues {
/// #[export(range=(0.0, 400.0, 1.0, or_greater, suffix="px"))]
/// icon_width: i32,
/// #[export(range=(-180.0, 180.0, degrees))]
/// angle: f32,
/// }
/// ```
#[allow(clippy::too_many_arguments)]
pub fn export_range(
min: f64,
Expand All @@ -176,23 +194,32 @@ pub mod export_info_functions {
or_greater: bool,
or_less: bool,
exp: bool,
radians: bool,
radians_as_degrees: bool,
degrees: bool,
hide_slider: bool,
suffix: Option<String>,
) -> PropertyHintInfo {
let hint_beginning = if let Some(step) = step {
format!("{min},{max},{step}")
} else {
format!("{min},{max}")
};
let rest =
comma_separate_boolean_idents!(or_greater, or_less, exp, radians, degrees, hide_slider);

let hint_string = if rest.is_empty() {
hint_beginning
} else {
format!("{hint_beginning},{rest}")
};
let rest = comma_separate_boolean_idents!(
or_greater,
or_less,
exp,
radians_as_degrees,
degrees,
hide_slider
);

let mut hint_string = hint_beginning;
if !rest.is_empty() {
hint_string.push_str(&format!(",{rest}"));
}
if let Some(suffix) = suffix {
hint_string.push_str(&format!(",suffix:{suffix}"));
}

PropertyHintInfo {
hint: PropertyHint::RANGE,
Expand Down
73 changes: 55 additions & 18 deletions godot-macros/src/class/data_models/field_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

use proc_macro2::{Ident, TokenStream};
use quote::quote;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};

use crate::util::{KvParser, ListParser};
use crate::ParseResult;
Expand Down Expand Up @@ -35,9 +35,11 @@ pub enum FieldExport {
or_greater: bool,
or_less: bool,
exp: bool,
radians_as_degrees: bool,
radians: bool,
degrees: bool,
hide_slider: bool,
suffix: Option<TokenStream>,
},

/// ### GDScript annotations
Expand Down Expand Up @@ -255,20 +257,21 @@ impl FieldExport {
}

fn new_range_list(mut parser: ListParser) -> ParseResult<FieldExport> {
const ALLOWED_OPTIONS: [&str; 6] = [
const FLAG_OPTIONS: [&str; 7] = [
"or_greater",
"or_less",
"exp",
"radians",
"radians_as_degrees",
"radians", // godot deprecated this key for 4.2 in favor of radians_as_degrees
"degrees",
"hide_slider",
];
const KV_OPTIONS: [&str; 1] = ["suffix"];

let min = parser.next_expr()?;
let max = parser.next_expr()?;
// If there is a next element and it is not an identifier,
// we take its tokens directly.
let step = if parser.peek().is_some_and(|kv| kv.as_ident().is_err()) {
// If there is a next element and it is a literal, we take its tokens directly.
let step = if parser.peek().is_some_and(|kv| kv.as_literal().is_ok()) {
let value = parser
.next_expr()
.expect("already guaranteed there was a TokenTree to parse");
Expand All @@ -277,10 +280,21 @@ impl FieldExport {
quote! { None }
};

let mut options = HashSet::new();
let mut flags = HashSet::<String>::new();
let mut kvs = HashMap::<String, TokenStream>::new();

while let Some(option) = parser.next_allowed_ident(&ALLOWED_OPTIONS[..])? {
options.insert(option.to_string());
loop {
let key_maybe_value =
parser.next_allowed_key_optional_value(&FLAG_OPTIONS, &KV_OPTIONS)?;
match key_maybe_value {
Some((option, None)) => {
flags.insert(option.to_string());
}
Some((option, Some(value))) => {
kvs.insert(option.to_string(), value.expr()?);
}
None => break,
}
}

parser.finish()?;
Expand All @@ -289,12 +303,14 @@ impl FieldExport {
min,
max,
step,
or_greater: options.contains("or_greater"),
or_less: options.contains("or_less"),
exp: options.contains("exp"),
radians: options.contains("radians"),
degrees: options.contains("degrees"),
hide_slider: options.contains("hide_slider"),
or_greater: flags.contains("or_greater"),
or_less: flags.contains("or_less"),
exp: flags.contains("exp"),
radians_as_degrees: flags.contains("radians_as_degrees"),
radians: flags.contains("radians"),
degrees: flags.contains("degrees"),
hide_slider: flags.contains("hide_slider"),
suffix: kvs.get("suffix").cloned(),
})
}

Expand Down Expand Up @@ -370,12 +386,33 @@ impl FieldExport {
or_greater,
or_less,
exp,
radians_as_degrees,
radians,
degrees,
hide_slider,
} => quote_export_func! {
export_range(#min, #max, #step, #or_greater, #or_less, #exp, #radians, #degrees, #hide_slider)
},
suffix,
} => {
let suffix = if suffix.is_some() {
quote! { Some(#suffix.to_string()) }
} else {
quote! { None }
};
let export_func = quote_export_func! {
export_range(#min, #max, #step, #or_greater, #or_less, #exp, #radians_as_degrees || #radians, #degrees, #hide_slider, #suffix)
}?;
let deprecation_warning = if *radians {
// for some reason, rustfmt insists on it looking like this. Probably a bug.
quote! {
#export_func;
::godot::__deprecated::emit_deprecated_warning!(export_range_radians);
}
} else {
quote! { #export_func }
};
Some(quote! {
#deprecation_warning
})
}

FieldExport::Enum { variants } => {
let variants = variants.iter().map(ValueWithKey::to_tuple_expression);
Expand Down
13 changes: 12 additions & 1 deletion godot-macros/src/util/kv_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

use crate::ParseResult;
use proc_macro2::{Delimiter, Ident, Spacing, Span, TokenStream, TokenTree};
use proc_macro2::{Delimiter, Ident, Literal, Spacing, Span, TokenStream, TokenTree};
use quote::ToTokens;
use std::collections::HashMap;

Expand Down Expand Up @@ -290,6 +290,17 @@ impl KvValue {
other => bail!(other, "expected identifier"),
}
}

pub fn as_literal(&self) -> ParseResult<Literal> {
if self.tokens.len() > 1 {
return bail!(&self.tokens[1], "expected a single literal");
}

match &self.tokens[0] {
TokenTree::Literal(literal) => Ok(literal.clone()),
other => bail!(other, "expected literal"),
}
}
}

struct ParserState<'a> {
Expand Down
30 changes: 30 additions & 0 deletions godot-macros/src/util/list_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,36 @@ impl ListParser {
}
}

/// Like `next_key_optional_value`, but checks if input flags and keys are in the allowed sets and `Err`s if not.
///
/// If an allowed flag appears as a key or an allowed key as a flag, that will also `Err` with a helpful message.
pub(crate) fn next_allowed_key_optional_value(
&mut self,
allowed_flag_keys: &[&str],
allowed_kv_keys: &[&str],
) -> ParseResult<Option<(Ident, Option<KvValue>)>> {
let allowed_keys = || {
let allowed_flag_keys = allowed_flag_keys.join(",");
let allowed_kv_keys = allowed_kv_keys.join(",");
[allowed_flag_keys, allowed_kv_keys].join(",")
};
match self.next_key_optional_value()? {
Some((key, None)) if !allowed_flag_keys.contains(&key.to_string().as_str()) => {
if allowed_kv_keys.contains(&key.to_string().as_str()) {
return bail!(key, "`{key}` requires a value `{key} = VALUE`");
}
bail!(key, "expected one of \"{}\"", allowed_keys())
}
Some((key, Some(_))) if !allowed_kv_keys.contains(&key.to_string().as_str()) => {
if allowed_flag_keys.contains(&key.to_string().as_str()) {
return bail!(key, "key `{key}` mustn't have a value");
}
bail!(key, "expected one of \"{}\"", allowed_keys())
}
key_maybe_value => Ok(key_maybe_value),
}
}

/// Ensure all values have been consumed.
pub fn finish(&mut self) -> ParseResult<()> {
if let Some(kv) = self.pop_next() {
Expand Down
5 changes: 3 additions & 2 deletions itest/rust/src/object_tests/property_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,11 @@ struct CheckAllExports {
#[export]
normal: GString,

#[export(range = (0.0, 10.0, or_greater, or_less, exp, radians, hide_slider))]
// `suffix = "px"` should be in the third slot to test that key-value pairs in that position no longer error.
#[export(range = (0.0, 10.0, suffix = "px", or_greater, or_less, exp, degrees, hide_slider))]
range_exported: f64,

#[export(range = (0.0, 10.0, 0.2, or_greater, or_less, exp, radians, hide_slider))]
#[export(range = (0.0, 10.0, 0.2, or_greater, or_less, exp, radians_as_degrees, hide_slider))]
range_exported_with_step: f64,

#[export(enum = (A = 10, B, C, D = 20))]
Expand Down

0 comments on commit e5ac4fc

Please sign in to comment.