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 macro for manually registering properties #400

Closed
Closed
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
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
80 changes: 77 additions & 3 deletions godot-macros/src/class/derive_godot_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

use proc_macro2::{Ident, Punct, TokenStream};
use proc_macro2::{Delimiter, Group, Ident, Punct, TokenStream, TokenTree};
use quote::{format_ident, quote};
use venial::{Declaration, NamedField, Struct, StructFields};

Expand All @@ -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 @@ -104,13 +106,84 @@ fn parse_struct_attributes(class: &Struct) -> ParseResult<ClassAttributes> {
is_tool = true;
}

if let Some(mut list_parser) = parser.handle_array("properties")? {
while let Some(inner_list_parser) = list_parser.try_next_list(Delimiter::Parenthesis)? {
let mut parser =
KvParser::parse_from_list_parser(inner_list_parser, Delimiter::Parenthesis)?;

let name = parser.handle_expr_required("name")?.to_string();
let ty = parser.handle_expr_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(),
"#[properties] item must define at least 1 getter or setter"
)?;
}
if field_var.getter.is_generated() || field_var.setter.is_generated() {
bail!(
parser.span(),
"#[properties] item does not support generated getters and setters"
)?;
}

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

standlone_properties.push(field);

parser.finish()?;
}

list_parser.finish()?;
}

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_expr_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(
ident(name.as_str()),
venial::TyExpr {
tokens: vec![TokenTree::Group(Group::new(Delimiter::None, 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 +206,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 +263,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
48 changes: 46 additions & 2 deletions godot-macros/src/util/kv_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

use crate::ParseResult;
use proc_macro2::{Delimiter, Ident, Spacing, Span, TokenStream, TokenTree};
use proc_macro2::{Delimiter, Ident, Punct, Spacing, Span, TokenStream, TokenTree};
use quote::ToTokens;
use std::collections::HashMap;
use venial::Attribute;
Expand Down Expand Up @@ -59,6 +59,46 @@ 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 parse_from_list_parser(parser: ListParser, delimiter: Delimiter) -> ParseResult<Self> {
let span = parser.span_close;
let tokens = parser
.lists
.into_iter()
.flat_map(|v| {
let mut tokens = v.into_tokens();
tokens.push(TokenTree::Punct(Punct::new(',', Spacing::Alone)));

tokens
})
.collect::<Vec<TokenTree>>();

Ok(Self {
span,
map: ParserState::parse(
"nested list".to_string(),
&venial::AttributeValue::Group(venial::GroupSpan { delimiter, span }, tokens),
)?,
})
}

pub fn span(&self) -> Span {
self.span
}
Expand Down Expand Up @@ -315,7 +355,11 @@ impl<'a> ParserState<'a> {
} else {
"".to_owned()
};
return bail!(cur, "expected identifier{parens_hint}");
let attr_name = self.attr_name;
let tokens = self.tokens;
let prev = self.prev;
let cur = self.cur;
return bail!(cur, "expected identifier{parens_hint} attr {attr_name} tokens {tokens:?} prev {prev:?} cur {cur:?}");
}
}
}
Expand Down
16 changes: 14 additions & 2 deletions godot-macros/src/util/list_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ use crate::ParseResult;
/// Parses a list of tokens as an ordered list of values. Unlike [`KvParser`] which treats the tokens as a
/// set of values.
pub struct ListParser {
lists: VecDeque<KvValue>,
pub(super) lists: VecDeque<KvValue>,
/// The last span of the list, usually a closing parenthesis.
span_close: Span,
pub(super) span_close: Span,
}

impl ListParser {
Expand Down Expand Up @@ -194,6 +194,18 @@ impl ListParser {
}
}

/// Takes the next list element of the list.
///
/// # Example
/// `((name = foo), (name = boo))` will yield `(name = foo)`
pub(crate) fn try_next_list(&mut self, delimiter: Delimiter) -> ParseResult<Option<Self>> {
let Some(kv) = self.pop_next() else {
return Ok(None);
};

Ok(Some(ListParser::new_from_tree(kv.single()?, delimiter)?))
}

/// Ensure all values have been consumed.
pub fn finish(&mut self) -> ParseResult<()> {
if let Some(kv) = self.pop_next() {
Expand Down
22 changes: 22 additions & 0 deletions itest/godot/ManualFfiTests.gd
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,25 @@ 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)
assert_eq(standalone_property.int_array, [0])

standalone_property.my_int = 2

assert_eq(standalone_property.my_int, 2)
assert_eq(standalone_property.readonly_int, 2)
assert_eq(standalone_property.int_array, [2])

standalone_property.int_array = [10, 11]

assert_eq(standalone_property.my_int, 10)
assert_eq(standalone_property.readonly_int, 10)
assert_eq(standalone_property.int_array, [10])

assert_eq(standalone_property.first_int, 10)
assert_eq(standalone_property.second_int, 10)
Loading