diff --git a/api/rs/slint/Cargo.toml b/api/rs/slint/Cargo.toml index a3f59209b..a1197807b 100644 --- a/api/rs/slint/Cargo.toml +++ b/api/rs/slint/Cargo.toml @@ -138,6 +138,8 @@ log = { version = "0.4.17", optional = true } [dev-dependencies] slint-build = { path = "../build" } i-slint-backend-testing = { version = "=1.0.3", path = "../../../internal/backends/testing" } +serde_json = "1.0.96" +serde = { version = "1.0.163", features = ["derive"] } [package.metadata.docs.rs] rustdoc-args = [ diff --git a/api/rs/slint/lib.rs b/api/rs/slint/lib.rs index cbeb82252..a75236bba 100644 --- a/api/rs/slint/lib.rs +++ b/api/rs/slint/lib.rs @@ -208,6 +208,26 @@ struct MyStruct { } ``` +The `.slint` file allows you to utilize Rust attributes and features for defining structures using the `@rust-attr()` directive. +This enables you to customize the generated code by applying additional traits, derivations, or annotations. +Consider the following structure defined in the `.slint` file with Rust attributes: +```slint,ignore +@rust-attr(derive(serde::Serialize, serde::Deserialize)) +struct MyStruct { + foo : i32, +} +``` + +Based on this structure, the following Rust code would be generated: + +```rust +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Default, Clone, Debug, PartialEq)] +struct MyStruct { + foo : i32, +} +``` + ## Exported Global singletons */ diff --git a/api/rs/slint/tests/simple_macro.rs b/api/rs/slint/tests/simple_macro.rs index 07676a775..7fa3b5357 100644 --- a/api/rs/slint/tests/simple_macro.rs +++ b/api/rs/slint/tests/simple_macro.rs @@ -15,3 +15,19 @@ fn empty_stuff() { slint!(export struct Hei { abcd: bool }); slint!(export global G { }); } + +#[test] +fn test_serialize_deserialize_struct() { + i_slint_backend_testing::init(); + slint! { + @rust-attr(derive(serde::Serialize, serde::Deserialize)) + export struct TestStruct { + foo: int, + } + export component Test { } + } + let data = TestStruct { foo: 1 }; + let serialized = serde_json::to_string(&data).unwrap(); + let deserialized: TestStruct = serde_json::from_str(&serialized).unwrap(); + assert_eq!(data, deserialized); +} diff --git a/internal/compiler/expression_tree.rs b/internal/compiler/expression_tree.rs index 633878f1f..bb020e37f 100644 --- a/internal/compiler/expression_tree.rs +++ b/internal/compiler/expression_tree.rs @@ -173,6 +173,7 @@ impl BuiltinFunction { .collect(), name: Some("Size".to_string()), node: None, + rust_attributes: None, }), args: vec![Type::Image], }, @@ -1027,7 +1028,7 @@ impl Expression { }, ( Type::Struct { fields: ref left, .. }, - Type::Struct { fields: right, name, node: n }, + Type::Struct { fields: right, name, node: n, rust_attributes }, ) if left != right => { if let Expression::Struct { mut values, .. } = self { let mut new_values = HashMap::new(); @@ -1051,6 +1052,7 @@ impl Expression { fields: left.clone(), name: name.clone(), node: n.clone(), + rust_attributes: rust_attributes.clone(), }, }), name: key.clone(), diff --git a/internal/compiler/generator/cpp.rs b/internal/compiler/generator/cpp.rs index e6d5500b9..befbdc296 100644 --- a/internal/compiler/generator/cpp.rs +++ b/internal/compiler/generator/cpp.rs @@ -579,7 +579,7 @@ pub fn generate(doc: &Document) -> impl std::fmt::Display { } for ty in doc.root_component.used_types.borrow().structs.iter() { - if let Type::Struct { fields, name: Some(name), node: Some(node) } = ty { + if let Type::Struct { fields, name: Some(name), node: Some(node), .. } = ty { generate_struct(&mut file, name, fields, node); } } diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index 054f35030..13f3d74e9 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -25,6 +25,7 @@ use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; use std::collections::BTreeMap; use std::num::NonZeroUsize; +use std::str::FromStr; type EvaluationContext<'a> = llr_EvaluationContext<'a, TokenStream>; type ParentCtx<'a> = llr_ParentCtx<'a, TokenStream>; @@ -139,8 +140,8 @@ pub fn generate(doc: &Document) -> TokenStream { .structs .iter() .filter_map(|ty| { - if let Type::Struct { fields, name: Some(name), node: Some(_) } = ty { - Some((ident(name), generate_struct(name, fields))) + if let Type::Struct { fields, name: Some(name), node: Some(_), rust_attributes } = ty { + Some((ident(name), generate_struct(name, fields, rust_attributes))) } else { None } @@ -422,12 +423,28 @@ fn generate_public_component(llr: &llr::PublicComponent) -> TokenStream { ) } -fn generate_struct(name: &str, fields: &BTreeMap) -> TokenStream { +fn generate_struct( + name: &str, + fields: &BTreeMap, + rust_attributes: &Option>, +) -> TokenStream { let component_id = struct_name_to_tokens(name); let (declared_property_vars, declared_property_types): (Vec<_>, Vec<_>) = fields.iter().map(|(name, ty)| (ident(name), rust_primitive_type(ty).unwrap())).unzip(); + let attributes = if let Some(feature) = rust_attributes { + let attr = + feature.iter().map(|f| match TokenStream::from_str(format!(r#"#[{}]"#, f).as_str()) { + Ok(eval) => eval, + Err(_) => quote! {}, + }); + quote! { #(#attr)* } + } else { + quote! {} + }; + quote! { + #attributes #[derive(Default, PartialEq, Debug, Clone)] pub struct #component_id { #(pub #declared_property_vars : #declared_property_types),* diff --git a/internal/compiler/langtype.rs b/internal/compiler/langtype.rs index e68aab3ec..3e834c7ff 100644 --- a/internal/compiler/langtype.rs +++ b/internal/compiler/langtype.rs @@ -60,6 +60,8 @@ pub enum Type { name: Option, /// When declared in .slint, this is the node of the declaration. node: Option, + /// deriven + rust_attributes: Option>, }, Enumeration(Rc), @@ -104,8 +106,8 @@ impl core::cmp::PartialEq for Type { Type::Easing => matches!(other, Type::Easing), Type::Brush => matches!(other, Type::Brush), Type::Array(a) => matches!(other, Type::Array(b) if a == b), - Type::Struct { fields, name, node: _ } => { - matches!(other, Type::Struct{fields: f, name: n, node: _} if fields == f && name == n) + Type::Struct { fields, name, node: _, rust_attributes: _ } => { + matches!(other, Type::Struct{fields:f,name:n,node:_, rust_attributes: _ } if fields == f && name == n) } Type::Enumeration(lhs) => matches!(other, Type::Enumeration(rhs) if lhs == rhs), Type::UnitProduct(a) => matches!(other, Type::UnitProduct(b) if a == b), diff --git a/internal/compiler/layout.rs b/internal/compiler/layout.rs index 1805adb1a..eb673be48 100644 --- a/internal/compiler/layout.rs +++ b/internal/compiler/layout.rs @@ -460,6 +460,7 @@ pub fn layout_info_type() -> Type { .collect(), name: Some("LayoutInfo".into()), node: None, + rust_attributes: None, } } diff --git a/internal/compiler/llr/lower_expression.rs b/internal/compiler/llr/lower_expression.rs index 1a2557f1a..eb6636132 100644 --- a/internal/compiler/llr/lower_expression.rs +++ b/internal/compiler/llr/lower_expression.rs @@ -398,6 +398,7 @@ pub fn lower_animation(a: &PropertyAnimation, ctx: &ExpressionContext<'_>) -> An fields: animation_fields().collect(), name: Some("PropertyAnimation".into()), node: None, + rust_attributes: None, } } @@ -435,6 +436,7 @@ pub fn lower_animation(a: &PropertyAnimation, ctx: &ExpressionContext<'_>) -> An .collect(), name: None, node: None, + rust_attributes: None, }, values: IntoIterator::into_iter([ ("0".to_string(), get_anim), @@ -661,6 +663,7 @@ fn box_layout_data( .collect(), name: Some("BoxLayoutCellData".into()), node: None, + rust_attributes: None, }; if repeater_count == 0 { @@ -747,6 +750,7 @@ pub(super) fn grid_layout_cell_data_ty() -> Type { .collect(), name: Some("GridLayoutCellData".into()), node: None, + rust_attributes: None, } } @@ -843,6 +847,7 @@ fn compile_path(path: &crate::expression_tree::Path, ctx: &ExpressionContext) -> fields: Default::default(), name: Some("PathElement".to_owned()), node: None, + rust_attributes: None, }, values: elements, as_model: false, @@ -866,6 +871,7 @@ fn compile_path(path: &crate::expression_tree::Path, ctx: &ExpressionContext) -> .collect(), name: element.element_type.native_class.cpp_type.clone(), node: None, + rust_attributes: None, }; llr_Expression::Struct { @@ -917,6 +923,7 @@ fn compile_path(path: &crate::expression_tree::Path, ctx: &ExpressionContext) -> .collect(), name: None, node: None, + rust_attributes: None, }, values: IntoIterator::into_iter([ ( @@ -960,5 +967,8 @@ fn make_struct( values.insert(name.to_string(), expr); } - llr_Expression::Struct { ty: Type::Struct { fields, name: Some(name), node: None }, values } + llr_Expression::Struct { + ty: Type::Struct { fields, name: Some(name), node: None, rust_attributes: None }, + values, + } } diff --git a/internal/compiler/load_builtins.rs b/internal/compiler/load_builtins.rs index a49265e66..a7dd1125a 100644 --- a/internal/compiler/load_builtins.rs +++ b/internal/compiler/load_builtins.rs @@ -36,7 +36,7 @@ pub fn load_builtins(register: &mut TypeRegister) { // parse structs for s in doc.StructDeclaration().chain(doc.ExportsList().flat_map(|e| e.StructDeclaration())) { let external_name = identifier_text(&s.DeclaredIdentifier()).unwrap(); - let mut ty = object_tree::type_struct_from_node(s.ObjectType(), &mut diag, register); + let mut ty = object_tree::type_struct_from_node(s.ObjectType(), &mut diag, register, None); if let Type::Struct { name, .. } = &mut ty { *name = Some( parse_annotation("name", &s.ObjectType()) diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index 9f42a420f..8324ea051 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -7,7 +7,7 @@ // cSpell: ignore qualname -use itertools::Either; +use itertools::{Either, Itertools}; use crate::diagnostics::{BuildDiagnostics, SourceLocation, Spanned}; use crate::expression_tree::{self, BindingExpression, Expression, Unit}; @@ -77,7 +77,21 @@ impl Document { |n: syntax_nodes::StructDeclaration, diag: &mut BuildDiagnostics, local_registry: &mut TypeRegister| { - let mut ty = type_struct_from_node(n.ObjectType(), diag, local_registry); + let rust_attributes: Vec = n + .children() + .filter(|child| child.kind() == SyntaxKind::AtRustAttr) + .map(|child| { + let mut text = child.text().to_string(); + text.pop(); + text + }) + .collect_vec(); + let mut ty = type_struct_from_node( + n.ObjectType(), + diag, + local_registry, + Some(rust_attributes), + ); if let Type::Struct { name, .. } = &mut ty { *name = parser::identifier_text(&n.DeclaredIdentifier()); } else { @@ -1630,7 +1644,7 @@ pub fn type_from_node( } prop_type } else if let Some(object_node) = node.ObjectType() { - type_struct_from_node(object_node, diag, tr) + type_struct_from_node(object_node, diag, tr, None) } else if let Some(array_node) = node.ArrayType() { Type::Array(Box::new(type_from_node(array_node.Type(), diag, tr))) } else { @@ -1644,6 +1658,7 @@ pub fn type_struct_from_node( object_node: syntax_nodes::ObjectType, diag: &mut BuildDiagnostics, tr: &TypeRegister, + rust_attributes: Option>, ) -> Type { let fields = object_node .ObjectTypeMember() @@ -1654,7 +1669,7 @@ pub fn type_struct_from_node( ) }) .collect(); - Type::Struct { fields, name: None, node: Some(object_node) } + Type::Struct { fields, name: None, node: Some(object_node), rust_attributes } } fn animation_element_from_node( diff --git a/internal/compiler/parser.rs b/internal/compiler/parser.rs index b4a6b2323..56ab9e4d0 100644 --- a/internal/compiler/parser.rs +++ b/internal/compiler/parser.rs @@ -422,8 +422,9 @@ declare_syntax! { /// `[ type ]` ArrayType -> [ Type ], /// `struct Foo := { ... } - StructDeclaration -> [DeclaredIdentifier, ObjectType], - + StructDeclaration -> [DeclaredIdentifier, ObjectType, ?AtRustAttr], + /// `@rust-attr(...)` + AtRustAttr -> [], } } diff --git a/internal/compiler/parser/document.rs b/internal/compiler/parser/document.rs index 42541ef42..5f6cc3108 100644 --- a/internal/compiler/parser/document.rs +++ b/internal/compiler/parser/document.rs @@ -4,6 +4,7 @@ use super::element::{parse_element, parse_element_content}; use super::prelude::*; use super::r#type::parse_struct_declaration; +use crate::parser::r#type::parse_rustattr; #[cfg_attr(test, parser_test)] /// ```test,Document @@ -45,6 +46,35 @@ pub fn parse_document(p: &mut impl Parser) -> bool { break; } } + "@" if p.nth(1).as_str() == "rust-attr" => { + let mut is_export = false; + let mut i = 0; + loop { + let value = p.nth(i); + if value.as_str() == ")" && p.nth(i + 1).as_str() == "export" { + is_export = true; + break; + } else if (value.as_str() == ")" + && p.nth(i + 1).as_str() != "struct" + && p.nth(i + 1).as_str() != "export" + && p.nth(i + 1).as_str() != ")") + || (value.as_str() == ")" && p.nth(i + 1).as_str() == "struct") + { + break; + } + i += 1; + } + if is_export { + let mut p = p.start_node(SyntaxKind::ExportsList); + if !parse_rustattr(&mut *p) { + break; + } + } else { + if !parse_rustattr(&mut *p) { + break; + } + } + } _ => { if !parse_component(&mut *p) { break; diff --git a/internal/compiler/parser/type.rs b/internal/compiler/parser/type.rs index 90a35405f..82b937168 100644 --- a/internal/compiler/parser/type.rs +++ b/internal/compiler/parser/type.rs @@ -85,3 +85,57 @@ pub fn parse_struct_declaration(p: &mut impl Parser) -> bool { parse_type_object(&mut *p); true } + +pub fn parse_rustattr(p: &mut impl Parser) -> bool { + let checkpoint = p.checkpoint(); + debug_assert_eq!(p.peek().as_str(), "@"); + p.consume(); // "@" + if p.peek().as_str() != "rust-attr" { + p.expect(SyntaxKind::AtRustAttr); + } + p.consume(); // "rust-attr" + p.expect(SyntaxKind::LParent); + parse_parentheses(&mut *p); + if p.peek().as_str() == "export" { + p.consume(); + } + let mut p = p.start_node_at(checkpoint, SyntaxKind::StructDeclaration); + p.consume(); // "struct" + { + let mut p = p.start_node(SyntaxKind::DeclaredIdentifier); + p.expect(SyntaxKind::Identifier); + } + + if p.peek().kind() == SyntaxKind::ColonEqual { + p.warning("':=' to declare a struct is deprecated. Remove the ':='"); + p.consume(); + } + + parse_type_object(&mut *p); + true +} + +fn parse_parentheses(p: &mut impl Parser) -> bool { + let mut p = p.start_node(SyntaxKind::AtRustAttr); + let mut opened = 0; + let mut closed = 0; + while closed <= opened { + if p.peek().kind() == SyntaxKind::LParent { + opened += 1; + } + if p.peek().kind() == SyntaxKind::RParent { + closed += 1; + } + if closed == opened && opened != 0 && closed != 0 && p.peek().kind() != SyntaxKind::RParent + { + p.error("Parse error: `)` or `,`"); + return false; + } + p.consume(); + } + if p.peek().as_str() != "struct" && p.peek().as_str() != "export" { + p.error("Parse error: expected `struct` or `export`"); + return false; + } + true +} diff --git a/internal/compiler/passes/compile_paths.rs b/internal/compiler/passes/compile_paths.rs index a0b43f6e3..72b665503 100644 --- a/internal/compiler/passes/compile_paths.rs +++ b/internal/compiler/passes/compile_paths.rs @@ -139,6 +139,7 @@ fn compile_path_from_string_literal( .collect(), name: Some("Point".into()), node: None, + rust_attributes: None, }; let mut points = Vec::new(); diff --git a/internal/compiler/passes/resolving.rs b/internal/compiler/passes/resolving.rs index f2501561d..5a2b6f10a 100644 --- a/internal/compiler/passes/resolving.rs +++ b/internal/compiler/passes/resolving.rs @@ -969,6 +969,7 @@ impl Expression { fields: values.iter().map(|(k, v)| (k.clone(), v.ty())).collect(), name: None, node: None, + rust_attributes: None, }; Expression::Struct { ty, values } } @@ -1030,8 +1031,14 @@ impl Expression { fields: mut result_fields, name: result_name, node: result_node, + rust_attributes, + }, + Type::Struct { + fields: elem_fields, + name: elem_name, + node: elem_node, + rust_attributes: deriven, }, - Type::Struct { fields: elem_fields, name: elem_name, node: elem_node }, ) => { for (elem_name, elem_ty) in elem_fields.into_iter() { match result_fields.entry(elem_name) { @@ -1052,6 +1059,7 @@ impl Expression { name: result_name.or(elem_name), fields: result_fields, node: result_node.or(elem_node), + rust_attributes: rust_attributes.or(deriven), } } (Type::Color, Type::Brush) | (Type::Brush, Type::Color) => Type::Brush, diff --git a/internal/interpreter/highlight.rs b/internal/interpreter/highlight.rs index c10de428d..e86ff5d7a 100644 --- a/internal/interpreter/highlight.rs +++ b/internal/interpreter/highlight.rs @@ -355,6 +355,7 @@ fn add_highlight_items(doc: &Document) { .collect(), name: None, node: None, + rust_attributes: None, } .into(), ),