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 #559
  • Loading branch information
ahayzen-kdab committed Jul 17, 2023
1 parent d897640 commit ff46e81
Show file tree
Hide file tree
Showing 29 changed files with 148 additions and 215 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `#[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"`
- `rust_mut` is now safe to call
- `#[qproperty]` is now defined as an attribute on the qobject rather than the field

### Fixed

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
3 changes: 0 additions & 3 deletions crates/cxx-qt-gen/src/generator/cpp/property/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,10 @@ mod tests {
ParsedQProperty {
ident: format_ident!("trivial_property"),
ty: parse_quote! { i32 },
vis: syn::Visibility::Inherited,
},
ParsedQProperty {
ident: format_ident!("opaque_property"),
ty: parse_quote! { UniquePtr<QColor> },
vis: syn::Visibility::Inherited,
},
];
let qobject_idents = create_qobjectname();
Expand Down Expand Up @@ -251,7 +249,6 @@ mod tests {
let properties = vec![ParsedQProperty {
ident: format_ident!("mapped_property"),
ty: parse_quote! { A1 },
vis: syn::Visibility::Inherited,
}];
let qobject_idents = create_qobjectname();

Expand Down
1 change: 0 additions & 1 deletion crates/cxx-qt-gen/src/generator/naming/property.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ pub mod tests {
let property = ParsedQProperty {
ident: format_ident!("my_property"),
ty,
vis: syn::Visibility::Inherited,
};
QPropertyName::from(&property)
}
Expand Down
3 changes: 0 additions & 3 deletions crates/cxx-qt-gen/src/generator/rust/property/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,14 @@ mod tests {
ParsedQProperty {
ident: format_ident!("trivial_property"),
ty: parse_quote! { i32 },
vis: syn::Visibility::Inherited,
},
ParsedQProperty {
ident: format_ident!("opaque_property"),
ty: parse_quote! { UniquePtr<QColor> },
vis: parse_quote! { pub },
},
ParsedQProperty {
ident: format_ident!("unsafe_property"),
ty: parse_quote! { *mut T },
vis: syn::Visibility::Inherited,
},
];
let qobject_idents = create_qobjectname();
Expand Down
78 changes: 75 additions & 3 deletions crates/cxx-qt-gen/src/parser/property.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,86 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0

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

/// Describes a single Q_PROPERTY for a struct
pub struct ParsedQProperty {
/// The [syn::Ident] of the property
pub ident: Ident,
/// The [syn::Type] of the property
pub ty: Type,
/// 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 })
})
}
}

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

use quote::format_ident;
use syn::{parse_quote, 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());
}
}
43 changes: 14 additions & 29 deletions crates/cxx-qt-gen/src/parser/qobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ use crate::{
},
syntax::{
attribute::{attribute_find_path, attribute_tokens_to_map, AttributeDefault},
fields::fields_to_named_fields_mut,
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 @@ -97,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 @@ -261,21 +260,11 @@ impl ParsedQObject {
}
}

/// Extract all the properties from [syn::Fields] from a [syn::ItemStruct]
fn parse_struct_fields(fields: &mut Fields) -> Result<Vec<ParsedQProperty>> {
fn parse_struct_attributes(attrs: &mut Vec<Attribute>) -> Result<Vec<ParsedQProperty>> {
let mut properties = 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(),
});
}

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

Ok(properties)
Expand All @@ -287,7 +276,7 @@ pub mod tests {
use super::*;

use crate::parser::tests::f64_type;
use syn::{parse_quote, ItemImpl, Visibility};
use syn::{parse_quote, ItemImpl};

pub fn create_parsed_qobject() -> ParsedQObject {
let qobject_struct: ItemStruct = parse_quote! {
Expand Down Expand Up @@ -324,11 +313,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 @@ -398,11 +386,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 @@ -413,20 +400,18 @@ 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_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
Loading

0 comments on commit ff46e81

Please sign in to comment.