Skip to content

Commit

Permalink
add #[property(...)] macro for adding gdscript properties not associa…
Browse files Browse the repository at this point in the history
…ted with a struct field, add tests for #[property(...)] macro, add parse_many to KvParser
  • Loading branch information
you-win committed Sep 7, 2023
1 parent 9e6fe56 commit 2cef439
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 8 deletions.
12 changes: 11 additions & 1 deletion godot-macros/src/class/data_models/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,17 @@ pub struct Field {
}

impl Field {
pub fn new(field: &venial::NamedField) -> Self {
pub fn new(name: Ident, ty: venial::TyExpr) -> Self {
Self {
name,
ty,
default: None,
var: None,
export: None,
}
}

pub fn from_named_field(field: &venial::NamedField) -> Self {
Self {
name: field.name.clone(),
ty: field.ty.clone(),
Expand Down
9 changes: 7 additions & 2 deletions godot-macros/src/class/data_models/field_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ pub enum GetterSetter {
}

impl GetterSetter {
pub(super) fn parse(parser: &mut KvParser, key: &str) -> ParseResult<Self> {
pub(crate) fn parse(parser: &mut KvParser, key: &str) -> ParseResult<Self> {
let getter_setter = match parser.handle_any(key) {
// No `get` argument
None => GetterSetter::Omitted,
Expand Down Expand Up @@ -126,6 +126,10 @@ impl GetterSetter {
pub fn is_omitted(&self) -> bool {
matches!(self, GetterSetter::Omitted)
}

pub fn is_generated(&self) -> bool {
matches!(self, GetterSetter::Generated)
}
}

/// Used to determine whether a [`GetterSetter`] is supposed to be a getter or setter.
Expand Down Expand Up @@ -205,7 +209,8 @@ impl GetterSetterImpl {
}
}

fn from_custom_impl(function_name: &Ident) -> Self {
/// Create a getters/setter impl from a user-defined function.
pub(super) fn from_custom_impl(function_name: &Ident) -> Self {
Self {
function_name: function_name.clone(),
function_impl: TokenStream::new(),
Expand Down
40 changes: 38 additions & 2 deletions godot-macros/src/class/derive_godot_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ pub fn derive_godot_class(decl: Declaration) -> ParseResult<TokenStream> {
.ok_or_else(|| venial::Error::new("Not a valid struct"))?;

let struct_cfg = parse_struct_attributes(class)?;
let fields = parse_fields(class)?;
let mut fields = parse_fields(class)?;
fields.all_fields.extend(struct_cfg.standlone_properties);

let class_name = &class.name;
let class_name_str = class.name.to_string();
Expand Down Expand Up @@ -89,6 +90,7 @@ fn parse_struct_attributes(class: &Struct) -> ParseResult<ClassAttributes> {
let mut base_ty = ident("RefCounted");
let mut has_generated_init = false;
let mut is_tool = false;
let mut standlone_properties = vec![];

// #[class] attribute on struct
if let Some(mut parser) = KvParser::parse(&class.attributes, "class")? {
Expand All @@ -107,10 +109,43 @@ fn parse_struct_attributes(class: &Struct) -> ParseResult<ClassAttributes> {
parser.finish()?;
}

// #[property] attributes on struct
for mut parser in KvParser::parse_many(&class.attributes, "property")? {
let name = parser.handle_expr_required("name")?.to_string();
let ty = parser.handle_ident_required("type")?;

let field_var = FieldVar::new_from_kv(&mut parser)?;
if field_var.getter.is_omitted() && field_var.setter.is_omitted() {
bail!(
parser.span(),
"#[property] must define at least 1 getter or setter"
)?;
}
if field_var.getter.is_generated() || field_var.setter.is_generated() {
bail!(
parser.span(),
"#[property] does not support generated getters and setters"
)?;
}

let mut field = Field::new(
util::ident(name.as_str()),
venial::TyExpr {
tokens: vec![proc_macro2::TokenTree::Ident(ty)],
},
);
field.var = Some(field_var);

standlone_properties.push(field);

parser.finish()?;
}

Ok(ClassAttributes {
base_ty,
has_generated_init,
is_tool,
standlone_properties,
})
}

Expand All @@ -133,7 +168,7 @@ fn parse_fields(class: &Struct) -> ParseResult<Fields> {
// Attributes on struct fields
for (named_field, _punct) in named_fields {
let mut is_base = false;
let mut field = Field::new(&named_field);
let mut field = Field::from_named_field(&named_field);

// #[base]
if let Some(parser) = KvParser::parse(&named_field.attributes, "base")? {
Expand Down Expand Up @@ -190,6 +225,7 @@ struct ClassAttributes {
base_ty: Ident,
has_generated_init: bool,
is_tool: bool,
standlone_properties: Vec<Field>,
}

fn make_godot_init_impl(class_name: &Ident, fields: Fields) -> TokenStream {
Expand Down
32 changes: 31 additions & 1 deletion godot-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,33 @@ use crate::util::ident;
/// impl MyStruct {}
/// ```
///
/// Alternatively, a property can be manually registered with Godot. This can be useful
/// when accessing nested structs or when a custom property name is needed. Automatically
/// generated getters/setters are not supported in this attribute. Custom property hints,
/// hint strings, and usage flags are supported.
/// ```
/// use godot::prelude::*;
///
/// #[derive(GodotClass)]
/// // Registers `my_field` as `my_int` with a getter and setter
/// #[property(name = my_int, type = i64, get = get_my_field, set = set_my_field)]
/// struct MyStruct {
/// my_field: i64,
/// }
///
/// #[godot_api]
/// impl MyStruct {
/// #[func]
/// pub fn get_my_field(&self) -> i64 {
/// self.my_field
/// }
///
/// #[func]
/// pub fn set_my_field(&mut self, value: i64) {
/// self.my_field = value;
/// }
/// }
/// ```
///
/// # Signals
///
Expand All @@ -318,7 +345,10 @@ use crate::util::ident;
/// for more information and further customization.
///
/// This is very similar to [GDScript's `@tool` feature](https://docs.godotengine.org/en/stable/tutorials/plugins/running_code_in_the_editor.html).
#[proc_macro_derive(GodotClass, attributes(class, base, var, export, init, signal))]
#[proc_macro_derive(
GodotClass,
attributes(class, base, var, export, init, signal, property)
)]
pub fn derive_godot_class(input: TokenStream) -> TokenStream {
translate(input, class::derive_godot_class)
}
Expand Down
18 changes: 18 additions & 0 deletions godot-macros/src/util/kv_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,24 @@ impl KvParser {
Ok(found_attr)
}

/// Create many new parsers which check for the presence of many `#[expected]` attributes.
pub fn parse_many(attributes: &[Attribute], expected: &str) -> ParseResult<Vec<Self>> {
let mut found_attrs = vec![];

for attr in attributes.iter() {
let path = &attr.path;
if path_is_single(path, expected) {
let attr_name = expected.to_string();
found_attrs.push(Self {
span: attr.tk_brackets.span,
map: ParserState::parse(attr_name, &attr.value)?,
});
}
}

Ok(found_attrs)
}

pub fn span(&self) -> Span {
self.span
}
Expand Down
11 changes: 11 additions & 0 deletions itest/godot/ManualFfiTests.gd
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,14 @@ func test_func_rename():
assert_eq(func_rename.has_method("renamed_static"), false)
assert_eq(func_rename.has_method("spell_static"), true)
assert_eq(func_rename.spell_static(), "static")

func test_standalone_property():
var standalone_property := StandaloneProperty.new()

assert_eq(standalone_property.my_int, 0)
assert_eq(standalone_property.readonly_int, 0)

standalone_property.my_int = 2

assert_eq(standalone_property.my_int, 2)
assert_eq(standalone_property.readonly_int, 2)
64 changes: 62 additions & 2 deletions itest/rust/src/object_tests/property_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ use godot::{
test::itest,
};

// No tests currently, tests using these classes are in Godot scripts.

#[derive(GodotClass)]
#[class(base=Node)]
struct HasProperty {
Expand Down Expand Up @@ -368,3 +366,65 @@ fn derive_export() {
"A:0,B:1,C:2".to_variant()
);
}

#[derive(GodotClass)]
#[property(name = my_int, type = i32, get = get_integer, set = set_integer)]
#[property(name = readonly_int, type = i32, get = get_integer)]
pub struct StandaloneProperty {
integer: i32,
}

#[godot_api]
impl RefCountedVirtual for StandaloneProperty {
fn init(_base: godot::obj::Base<Self::Base>) -> Self {
Self { integer: 0 }
}
}

#[godot_api]
impl StandaloneProperty {
#[func]
fn get_integer(&self) -> i32 {
self.integer
}

#[func]
fn set_integer(&mut self, integer: i32) {
self.integer = integer;
}
}

#[itest]
fn standalone_property() {
let class: Gd<StandaloneProperty> = Gd::new_default();

let property = class
.get_property_list()
.iter_shared()
.find(|c| c.get_or_nil("name") == "my_int".to_variant())
.unwrap();

assert_eq!(
property.get_or_nil("class_name"),
"StandaloneProperty".to_variant()
);
assert_eq!(
property.get_or_nil("type"),
(VariantType::Int as i32).to_variant()
);

let property = class
.get_property_list()
.iter_shared()
.find(|c| c.get_or_nil("name") == "readonly_int".to_variant())
.unwrap();

assert_eq!(
property.get_or_nil("class_name"),
"StandaloneProperty".to_variant()
);
assert_eq!(
property.get_or_nil("type"),
(VariantType::Int as i32).to_variant()
);
}

0 comments on commit 2cef439

Please sign in to comment.