Skip to content

Commit

Permalink
cxx-qt-gen: use #[qproperty] on the struct instead of field
Browse files Browse the repository at this point in the history
This is a stepping stone to moving qproperty to be defined
on the type in the extern "RustQt" block.

Related to KDAB#559
  • Loading branch information
ahayzen-kdab committed Jun 13, 2023
1 parent 1e1fd8d commit ba49a21
Show file tree
Hide file tree
Showing 26 changed files with 155 additions and 217 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `impl cxx_qt::Threading for qobject::T` now needs to be specified for `qt_thread()` to be available
- `#[cxx_qt::qsignals]` and `#[cxx_qt::inherit]` are now used in an `extern "RustQt"` block as `#[qsignal]` and `#[inherit]`
- `#[qinvokable]` is now defined as a signature in `extern "RustQt"`
- `#[qproperty]` is now defined as an attribute on the qobject rather than the field

### Removed

Expand Down
2 changes: 1 addition & 1 deletion book/src/getting-started/1-qobjects-in-rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Typically this will be instantiated by QML and the lifetime will be directly ass

The generated QObject subclass will then defer to the Rust struct for any behavior, which is then defined in Rust.
For example, using the `#[qinvokable]` attribute, we can define functions that will be exposed to C++, but will execute Rust code.
Also, any fields in the Rust struct marked with `#[qproperty]` will be exposed to Qt as `Q_PROPERTY` fields.
Also, any fields in the Rust struct can be exposed to Qt as `Q_PROPERTY` fields by using the `#[qproperty(T, NAME)]` attribute on the struct.
Therefore allowing you to assign them from QML as well.

But enough theory for now, lets jump in and write [our first CXX-Qt module](./2-our-first-cxx-qt-module.md).
Expand Down
4 changes: 2 additions & 2 deletions book/src/getting-started/2-our-first-cxx-qt-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@ Additionally, we need to either `impl Default` or `#[derive(Default)]` for our s
```

The Rust struct can be defined just like a normal Rust struct and can contain any kind of field, even Rust-only types.
If a field is marked as `#[qproperty]` it will be exposed to the C++ side as a `Q_PROPERTY`.
If a field is tagged as `#[qproperty]` it will be exposed to the C++ side as a `Q_PROPERTY`.

That means the newly created QObject subclass will have two properties as members: `number` and `string`. For names that contain multiple words, like `my_number`, CXX-Qt will automatically rename the field from snake_case to camelCase to fit with C++/QML naming conventions (e.g. `myNumber`).

### Types

Do note though that any fields marked as `#[qproperty]` must be types that CXX can translate to C++ types.
Do note though that any fields tagged as `#[qproperty]` must be types that CXX can translate to C++ types.
In our case that means:
- `number: i32` -> `::std::int32_t number`
- `string: QString` -> `QString string`
Expand Down
6 changes: 3 additions & 3 deletions book/src/qobject/qobject_struct.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ The macro does multiple other things for you though:
- Generate a C++ QObject subclass that wraps the `MyObject` Rust struct.
- Expose the generated QObject subclass to Rust as [`qobject::MyObject`](./generated-qobject.md)
- Generate getters/setters for all fields.
- Generate `Q_PROPERTY`s for all fields that are marked as `#[qproperty]`.
- Generate `Q_PROPERTY`s for all fields that are tagged as `#[qproperty]`.
- Generate signals if paired with a [`#[qsignal]` macro](./signals.md).

## Exposing to QML
Expand Down Expand Up @@ -71,7 +71,7 @@ Fields within the `#[cxx_qt::qobject]` marked struct can be tagged with `#[qprop
{{#include ../../../examples/qml_features/rust/src/properties.rs:book_properties_struct}}
```

Any type that CXX supports may be marked as a `#[qproperty]`.
Any type that CXX supports may be tagged as a `#[qproperty]`.
See the [Types page](../concepts/types.md) for a list of supported types.

For every `#[qproperty]`, CXX-Qt will generate setters and getters, as well as a "changed" signal.
Expand All @@ -90,7 +90,7 @@ where `<Property>` is the name of the property.

These setters and getters assure that the changed signal is emitted every time the property is edited.

Any field that's not marked as `#[qproperty]` won't be accessible from C++, but it will be accessible from Rust.
Any field that's not tagged as `#[qproperty]` won't be accessible from C++, but it will be accessible from Rust.
See the [Private fields section](#private-methods-and-fields)

## Default
Expand Down
81 changes: 80 additions & 1 deletion crates/cxx-qt-gen/src/parser/property.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0

use syn::{Ident, Type, Visibility};
use syn::{parse::ParseStream, parse_quote, Attribute, Ident, Result, Token, Type, Visibility};

/// Describes a single field for a struct
pub struct ParsedRustField {
Expand All @@ -24,3 +24,82 @@ pub struct ParsedQProperty {
/// The [syn::Visibility] of the property
pub vis: Visibility,
}

impl ParsedQProperty {
pub fn parse(attr: Attribute) -> Result<Self> {
attr.parse_args_with(|input: ParseStream| -> Result<Self> {
let ty = input.parse()?;
let _comma = input.parse::<Token![,]>()?;
let ident = input.parse()?;

// TODO: later we'll need to parse setters and getters here
// which are key-value, hence this not being parsed as a list

Ok(Self {
ident,
ty,
// For now always assume pub visibility
vis: parse_quote! { pub },
})
})
}
}

#[cfg(test)]
mod tests {
use super::*;

use quote::format_ident;
use syn::ItemStruct;

#[test]
fn test_parse_property() {
let mut input: ItemStruct = parse_quote! {
#[qproperty(T, name)]
struct MyStruct;
};
let property = ParsedQProperty::parse(input.attrs.remove(0)).unwrap();
assert_eq!(property.ident, format_ident!("name"));
assert_eq!(property.ty, parse_quote! { T });
}

#[test]
fn test_parse_property_arg_extra() {
let mut input: ItemStruct = parse_quote! {
#[qproperty(T, name, A = B)]
struct MyStruct;
};
let property = ParsedQProperty::parse(input.attrs.remove(0));
assert!(property.is_err());
}

#[test]
fn test_parse_property_arg_wrong() {
let mut input: ItemStruct = parse_quote! {
#[qproperty(A = B, name)]
struct MyStruct;
};
let property = ParsedQProperty::parse(input.attrs.remove(0));
assert!(property.is_err());
}

#[test]
fn test_parse_property_no_name() {
let mut input: ItemStruct = parse_quote! {
#[qproperty(T)]
struct MyStruct;
};
let property = ParsedQProperty::parse(input.attrs.remove(0));
assert!(property.is_err());
}

#[test]
fn test_parse_property_no_type() {
let mut input: ItemStruct = parse_quote! {
#[qproperty(T)]
struct MyStruct;
};
let property = ParsedQProperty::parse(input.attrs.remove(0));
assert!(property.is_err());
}
}
56 changes: 16 additions & 40 deletions crates/cxx-qt-gen/src/parser/qobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::syntax::{
attribute::{attribute_find_path, attribute_tokens_to_map, AttributeDefault},
fields::fields_to_named_fields_mut,
};
use crate::syntax::attribute::{attribute_find_path, attribute_tokens_to_map, AttributeDefault};
use crate::{
parser::{
inherit::ParsedInheritedMethod,
Expand All @@ -17,8 +14,8 @@ use crate::{
syntax::path::path_compare_str,
};
use syn::{
spanned::Spanned, Error, Fields, Ident, ImplItem, Item, ItemImpl, ItemStruct, LitStr, Result,
Visibility,
spanned::Spanned, Attribute, Error, Ident, ImplItem, Item, ItemImpl, ItemStruct, LitStr,
Result, Visibility,
};

/// Metadata for registering QML element
Expand Down Expand Up @@ -99,7 +96,7 @@ impl ParsedQObject {

// Parse any properties in the struct
// and remove the #[qproperty] attribute
let (properties, _) = Self::parse_struct_fields(&mut qobject_struct.fields)?;
let properties = Self::parse_struct_attributes(&mut qobject_struct.attrs)?;

// Ensure that the QObject is marked as pub otherwise the error is non obvious
// https://github.com/KDAB/cxx-qt/issues/457
Expand Down Expand Up @@ -264,33 +261,14 @@ impl ParsedQObject {
}
}

/// Extract all the properties from [syn::Fields] from a [syn::ItemStruct]
fn parse_struct_fields(
fields: &mut Fields,
) -> Result<(Vec<ParsedQProperty>, Vec<ParsedRustField>)> {
fn parse_struct_attributes(attrs: &mut Vec<Attribute>) -> Result<Vec<ParsedQProperty>> {
let mut properties = vec![];
let mut rust_fields = vec![];
for field in fields_to_named_fields_mut(fields)? {
// Try to find any properties defined within the struct
if let Some(index) = attribute_find_path(&field.attrs, &["qproperty"]) {
// Remove the #[qproperty] attribute
field.attrs.remove(index);

properties.push(ParsedQProperty {
ident: field.ident.clone().unwrap(),
ty: field.ty.clone(),
vis: field.vis.clone(),
});
} else {
rust_fields.push(ParsedRustField {
ident: field.ident.clone().unwrap(),
ty: field.ty.clone(),
vis: field.vis.clone(),
})
}

while let Some(index) = attribute_find_path(attrs, &["qproperty"]) {
properties.push(ParsedQProperty::parse(attrs.remove(index))?);
}

Ok((properties, rust_fields))
Ok(properties)
}
}

Expand Down Expand Up @@ -336,11 +314,10 @@ pub mod tests {
fn test_from_struct_properties_and_fields() {
let qobject_struct: ItemStruct = parse_quote! {
#[cxx_qt::qobject]
#[qproperty(i32, int_property)]
#[qproperty(i32, public_property)]
pub struct MyObject {
#[qproperty]
int_property: i32,

#[qproperty]
pub public_property: i32,

field: i32,
Expand Down Expand Up @@ -412,11 +389,10 @@ pub mod tests {
fn test_parse_struct_fields_valid() {
let item: ItemStruct = parse_quote! {
#[cxx_qt::qobject]
#[qproperty(f64, f64_property)]
#[qproperty(f64, public_property)]
pub struct T {
#[qproperty]
f64_property: f64,

#[qproperty]
pub public_property: f64,

field: f64,
Expand All @@ -427,20 +403,20 @@ pub mod tests {

assert_eq!(properties[0].ident, "f64_property");
assert_eq!(properties[0].ty, f64_type());
assert!(matches!(properties[0].vis, Visibility::Inherited));
assert!(matches!(properties[0].vis, Visibility::Public(_)));

assert_eq!(properties[1].ident, "public_property");
assert_eq!(properties[1].ty, f64_type());
assert!(matches!(properties[1].vis, Visibility::Public(_)));
}

#[test]
fn test_parse_struct_fields_invalid() {
fn test_parse_struct_fields() {
let item: ItemStruct = parse_quote! {
#[cxx_qt::qobject]
pub struct T(f64);
};
assert!(ParsedQObject::from_struct(&item, 0).is_err());
assert!(ParsedQObject::from_struct(&item, 0).is_ok());
}

#[test]
Expand Down
117 changes: 0 additions & 117 deletions crates/cxx-qt-gen/src/syntax/fields.rs

This file was deleted.

Loading

0 comments on commit ba49a21

Please sign in to comment.