diff --git a/CHANGELOG.md b/CHANGELOG.md index 443291ed5..2b14f8119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to this project will be documented in this file. - `z` property on items - Mouse wheel work on the Flickable and derivatives - One can now omit the type of a two way binding property + - One can declare callback aliases ### Fixed diff --git a/api/sixtyfps-node/native/lib.rs b/api/sixtyfps-node/native/lib.rs index 8232f4b8e..b6bbf6cd7 100644 --- a/api/sixtyfps-node/native/lib.rs +++ b/api/sixtyfps-node/native/lib.rs @@ -224,6 +224,8 @@ fn to_eval_value<'cx>( Type::Enumeration(_) => todo!(), Type::Invalid | Type::Void + | Type::InferredProperty + | Type::InferredCallback | Type::Builtin(_) | Type::Native(_) | Type::Function { .. } diff --git a/docs/langref.md b/docs/langref.md index 255072a84..7b13f46bf 100644 --- a/docs/langref.md +++ b/docs/langref.md @@ -358,6 +358,16 @@ Example := Rectangle { } ``` +### Callback aliases + +It is possible to declare callback aliases in a similar way to two-way bindings: + +```60 +Example := Rectangle { + callback clicked <=> area.clicked; + area := TouchArea {} +} +``` ## Expressions diff --git a/sixtyfps_compiler/generator/cpp.rs b/sixtyfps_compiler/generator/cpp.rs index 3b01c648e..409590878 100644 --- a/sixtyfps_compiler/generator/cpp.rs +++ b/sixtyfps_compiler/generator/cpp.rs @@ -659,6 +659,12 @@ fn generate_component( let mut init = vec!["[[maybe_unused]] auto self = this;".into()]; for (cpp_name, property_decl) in component.root_element.borrow().property_declarations.iter() { + let access = if let Some(alias) = &property_decl.is_alias { + access_named_reference(alias, component, "this") + } else { + format!("this->{}", cpp_name) + }; + let ty = if let Type::Callback { args, return_type } = &property_decl.property_type { let param_types = args.iter().map(|t| get_cpp_type(t, property_decl, diag)).collect::>(); @@ -668,7 +674,7 @@ fn generate_component( if property_decl.expose_in_public_api && is_root { let callback_emitter = vec![format!( "return {}.call({});", - cpp_name, + access, (0..args.len()).map(|i| format!("arg_{}", i)).join(", ") )]; component_struct.members.push(( @@ -696,7 +702,7 @@ fn generate_component( signature: "(Functor && callback_handler) const".into(), statements: Some(vec![format!( "{}.set_handler(std::forward(callback_handler));", - cpp_name + access )]), ..Default::default() }), @@ -707,12 +713,6 @@ fn generate_component( let cpp_type = get_cpp_type(&property_decl.property_type, property_decl, diag); if property_decl.expose_in_public_api && is_root { - let access = if let Some(alias) = &property_decl.is_alias { - access_named_reference(alias, component, "this") - } else { - format!("this->{}", cpp_name) - }; - let prop_getter: Vec = vec![format!("return {}.get();", access)]; component_struct.members.push(( Access::Public, diff --git a/sixtyfps_compiler/generator/rust.rs b/sixtyfps_compiler/generator/rust.rs index 1ad7f2deb..d8465cef8 100644 --- a/sixtyfps_compiler/generator/rust.rs +++ b/sixtyfps_compiler/generator/rust.rs @@ -280,8 +280,13 @@ fn generate_component( let mut property_and_callback_accessors: Vec = vec![]; for (prop_name, property_decl) in component.root_element.borrow().property_declarations.iter() { let prop_ident = format_ident!("{}", prop_name); + let prop = if let Some(alias) = &property_decl.is_alias { + access_named_reference(alias, component, quote!(_self)) + } else { + quote!(#inner_component_id::FIELD_OFFSETS.#prop_ident.apply_pin(_self)) + }; + if let Type::Callback { args, return_type } = &property_decl.property_type { - declared_callbacks.push(prop_ident.clone()); let callback_args = args .iter() .map(|a| get_rust_type(a, &property_decl.type_node(), diag)) @@ -295,16 +300,13 @@ fn generate_component( .map(|i| format_ident!("arg_{}", i)) .collect::>(); let caller_ident = format_ident!("invoke_{}", prop_name); - property_and_callback_accessors.push( - quote!( - #[allow(dead_code)] - pub fn #caller_ident(&self, #(#args_name : #callback_args,)*) -> #return_type { - let _self = vtable::VRc::as_pin_ref(&self.0); - #inner_component_id::FIELD_OFFSETS.#prop_ident.apply_pin(_self).call(&(#(#args_name,)*)) - } - ) - , - ); + property_and_callback_accessors.push(quote!( + #[allow(dead_code)] + pub fn #caller_ident(&self, #(#args_name : #callback_args,)*) -> #return_type { + let _self = vtable::VRc::as_pin_ref(&self.0); + #prop.call(&(#(#args_name,)*)) + } + )); let on_ident = format_ident!("on_{}", prop_name); let args_index = @@ -315,7 +317,7 @@ fn generate_component( pub fn #on_ident(&self, f: impl Fn(#(#callback_args),*) -> #return_type + 'static) { let _self = vtable::VRc::as_pin_ref(&self.0); #[allow(unused)] - #inner_component_id::FIELD_OFFSETS.#prop_ident.apply_pin(_self).set_handler( + #prop.set_handler( // FIXME: why do i need to clone here? move |args| f(#(args.#args_index.clone()),*) ) @@ -324,8 +326,11 @@ fn generate_component( , ); } - declared_callbacks_types.push(callback_args); - declared_callbacks_ret.push(return_type); + if property_decl.is_alias.is_none() { + declared_callbacks.push(prop_ident.clone()); + declared_callbacks_types.push(callback_args); + declared_callbacks_ret.push(return_type); + } } else { let rust_property_type = get_rust_type(&property_decl.property_type, &property_decl.type_node(), diag); @@ -333,12 +338,6 @@ fn generate_component( let getter_ident = format_ident!("get_{}", prop_name); let setter_ident = format_ident!("set_{}", prop_name); - let prop = if let Some(alias) = &property_decl.is_alias { - access_named_reference(alias, component, quote!(_self)) - } else { - quote!(#inner_component_id::FIELD_OFFSETS.#prop_ident.apply_pin(_self)) - }; - property_and_callback_accessors.push(quote!( #[allow(dead_code)] pub fn #getter_ident(&self) -> #rust_property_type { diff --git a/sixtyfps_compiler/object_tree.rs b/sixtyfps_compiler/object_tree.rs index 3790ca4fa..ead8179fd 100644 --- a/sixtyfps_compiler/object_tree.rs +++ b/sixtyfps_compiler/object_tree.rs @@ -638,7 +638,7 @@ impl Element { let unresolved_name = unwrap_or_continue!(identifier_text(&con_node); diag); let PropertyLookupResult { resolved_name, property_type } = r.lookup_property(&unresolved_name); - if let Type::Callback { args, .. } = property_type { + if let Type::Callback { args, .. } = &property_type { let num_arg = con_node.DeclaredIdentifier().count(); if num_arg > args.len() { diag.push_error( @@ -651,23 +651,26 @@ impl Element { &con_node.child_token(SyntaxKind::Identifier).unwrap(), ); } - if r.bindings - .insert( - resolved_name.into_owned(), - BindingExpression::new_uncompiled(con_node.clone().into()), - ) - .is_some() - { - diag.push_error( - "Duplicated callback".into(), - &con_node.child_token(SyntaxKind::Identifier).unwrap(), - ); - } + } else if property_type == Type::InferredCallback { + // argument matching will happen later } else { diag.push_error( format!("'{}' is not a callback in {}", unresolved_name, r.base_type), &con_node.child_token(SyntaxKind::Identifier).unwrap(), ); + continue; + } + if r.bindings + .insert( + resolved_name.into_owned(), + BindingExpression::new_uncompiled(con_node.clone().into()), + ) + .is_some() + { + diag.push_error( + "Duplicated callback".into(), + &con_node.child_token(SyntaxKind::Identifier).unwrap(), + ); } } diff --git a/sixtyfps_compiler/parser/element.rs b/sixtyfps_compiler/parser/element.rs index 07c30e912..a39f4e8ae 100644 --- a/sixtyfps_compiler/parser/element.rs +++ b/sixtyfps_compiler/parser/element.rs @@ -326,7 +326,6 @@ fn parse_callback_declaration(p: &mut impl Parser) { if p.test(SyntaxKind::DoubleArrow) { let mut p = p.start_node(SyntaxKind::TwoWayBinding); - p.consume(); parse_expression(&mut *p); } diff --git a/sixtyfps_compiler/passes/resolving.rs b/sixtyfps_compiler/passes/resolving.rs index 3bdf3f423..e5b24624d 100644 --- a/sixtyfps_compiler/passes/resolving.rs +++ b/sixtyfps_compiler/passes/resolving.rs @@ -241,7 +241,7 @@ impl Expression { Expression::TwoWayBinding(n, None) } Expression::CallbackReference(n) => { - if ctx.property_type != Type::InferredCallback { + if ctx.property_type != Type::InferredCallback && ty != ctx.property_type { ctx.diag.push_error("Cannot bind to a callback".into(), &node); Expression::Invalid } else { diff --git a/sixtyfps_compiler/tests/syntax/lookup/callback_alias3.60 b/sixtyfps_compiler/tests/syntax/lookup/callback_alias3.60 index 06b71b9e3..0444bc13f 100644 --- a/sixtyfps_compiler/tests/syntax/lookup/callback_alias3.60 +++ b/sixtyfps_compiler/tests/syntax/lookup/callback_alias3.60 @@ -8,6 +8,11 @@ Please contact info@sixtyfps.io for more information. LICENSE END */ +Sub := Rectangle { + callback compute(int) -> int; + callback compute_alias <=> compute; +} + Xxx := Rectangle { foo := Rectangle { @@ -26,4 +31,12 @@ Xxx := Rectangle { // ^^error{The expression in a two way binding must be a property reference} callback loop2 <=> loop3; + Sub { + compute_alias(a, b, c) => { + debug(b); // FIXME: one should actually check that the connection has the right amount of arguments +// ^error{Unknown unqualified identifier 'b'} + return "hello"; +// ^error{Cannot convert string to int} + } + } } diff --git a/sixtyfps_runtime/interpreter/dynamic_component.rs b/sixtyfps_runtime/interpreter/dynamic_component.rs index 419cd1b45..aa3ec1a99 100644 --- a/sixtyfps_runtime/interpreter/dynamic_component.rs +++ b/sixtyfps_runtime/interpreter/dynamic_component.rs @@ -439,9 +439,38 @@ impl<'id> ComponentDescription<'id> { if !core::ptr::eq((&self.ct) as *const _, component.get_vtable() as *const _) { return Err(()); } - let x = self.custom_callbacks.get(name).ok_or(())?; - let sig = x.apply(unsafe { &*(component.as_ptr() as *const dynamic_type::Instance) }); - sig.set_handler(handler); + if let Some(alias) = self + .original + .root_element + .borrow() + .property_declarations + .get(name) + .and_then(|d| d.is_alias.as_ref()) + { + generativity::make_guard!(guard); + // Safety: we just verified that the component has the right vtable + let c = unsafe { InstanceRef::from_pin_ref(component, guard) }; + generativity::make_guard!(guard); + let element = alias.element(); + let enclosing_component = eval::enclosing_component_for_element(&element, c, guard); + let component_type = enclosing_component.component_type; + let item_info = &component_type.items[element.borrow().id.as_str()]; + let item = unsafe { item_info.item_from_component(enclosing_component.as_ptr()) }; + + if let Some(callback) = item_info.rtti.callbacks.get(alias.name()) { + callback.set_handler(item, handler) + } else if let Some(callback_offset) = component_type.custom_callbacks.get(alias.name()) + { + let callback = callback_offset.apply(&*enclosing_component.instance); + callback.set_handler(handler) + } else { + return Err(()); + } + } else { + let x = self.custom_callbacks.get(name).ok_or(())?; + let sig = x.apply(unsafe { &*(component.as_ptr() as *const dynamic_type::Instance) }); + sig.set_handler(handler); + } Ok(()) } @@ -458,9 +487,31 @@ impl<'id> ComponentDescription<'id> { if !core::ptr::eq((&self.ct) as *const _, component.get_vtable() as *const _) { return Err(()); } - let x = self.custom_callbacks.get(name).ok_or(())?; - let sig = x.apply(unsafe { &*(component.as_ptr() as *const dynamic_type::Instance) }); - Ok(sig.call(args)) + generativity::make_guard!(guard); + // Safety: we just verified that the component has the right vtable + let c = unsafe { InstanceRef::from_pin_ref(component, guard) }; + if let Some(alias) = self + .original + .root_element + .borrow() + .property_declarations + .get(name) + .and_then(|d| d.is_alias.as_ref()) + { + Ok(eval::invoke_callback( + eval::ComponentInstance::InstanceRef(c), + &alias.element(), + alias.name(), + args, + )) + } else { + Ok(eval::invoke_callback( + eval::ComponentInstance::InstanceRef(c), + &self.original.root_element, + name, + args, + )) + } } } diff --git a/sixtyfps_runtime/interpreter/eval.rs b/sixtyfps_runtime/interpreter/eval.rs index efe886812..670bb4df2 100644 --- a/sixtyfps_runtime/interpreter/eval.rs +++ b/sixtyfps_runtime/interpreter/eval.rs @@ -193,30 +193,8 @@ pub fn eval_expression(e: &Expression, local_context: &mut EvalLocalContext) -> } Expression::FunctionCall { function, arguments, source_location: _ } => match &**function { Expression::CallbackReference(nr) => { - let element = nr.element(); - generativity::make_guard!(guard); - match enclosing_component_instance_for_element(&element, local_context.component_instance, guard) { - ComponentInstance::InstanceRef(enclosing_component) => { - let component_type = enclosing_component.component_type; - let item_info = &component_type.items[element.borrow().id.as_str()]; - let item = unsafe { item_info.item_from_component(enclosing_component.as_ptr()) }; - let args = arguments.iter().map(|e| eval_expression(e, local_context)).collect::>(); - - if let Some(callback) = item_info.rtti.callbacks.get(nr.name()) { - callback.call(item, args.as_slice()) - } else if let Some(callback_offset) = component_type.custom_callbacks.get(nr.name()) - { - let callback = callback_offset.apply(&*enclosing_component.instance); - callback.call(args.as_slice()) - } else { - panic!("unkown callback {}", nr.name()) - } - } - ComponentInstance::GlobalComponent(global) => { - let args = arguments.iter().map(|e| eval_expression(e, local_context)); - global.as_ref().invoke_callback(nr.name(), args.collect::>().as_slice()) - } - } + let args = arguments.iter().map(|e| eval_expression(e, local_context)).collect::>(); + invoke_callback(local_context.component_instance, &nr.element(), nr.name(), &args) } Expression::BuiltinFunctionReference(BuiltinFunction::GetWindowScaleFactor, _) => { match local_context.component_instance { @@ -720,6 +698,35 @@ pub fn store_property( Ok(()) } +pub(crate) fn invoke_callback( + component_instance: ComponentInstance, + element: &ElementRc, + callback_name: &str, + args: &[Value], +) -> Value { + generativity::make_guard!(guard); + match enclosing_component_instance_for_element(element, component_instance, guard) { + ComponentInstance::InstanceRef(enclosing_component) => { + let component_type = enclosing_component.component_type; + let item_info = &component_type.items[element.borrow().id.as_str()]; + let item = unsafe { item_info.item_from_component(enclosing_component.as_ptr()) }; + + if let Some(callback) = item_info.rtti.callbacks.get(callback_name) { + callback.call(item, args) + } else if let Some(callback_offset) = component_type.custom_callbacks.get(callback_name) + { + let callback = callback_offset.apply(&*enclosing_component.instance); + callback.call(args) + } else { + panic!("unkown callback {}", callback_name) + } + } + ComponentInstance::GlobalComponent(global) => { + global.as_ref().invoke_callback(callback_name, args) + } + } +} + fn root_component_instance<'a, 'old_id, 'new_id>( component: InstanceRef<'a, 'old_id>, guard: generativity::Guard<'new_id>, diff --git a/tests/cases/callbacks/callback_alias.60 b/tests/cases/callbacks/callback_alias.60 new file mode 100644 index 000000000..57bd0da3b --- /dev/null +++ b/tests/cases/callbacks/callback_alias.60 @@ -0,0 +1,66 @@ +/* LICENSE BEGIN + This file is part of the SixtyFPS Project -- https://sixtyfps.io + Copyright (c) 2020 Olivier Goffart + Copyright (c) 2020 Simon Hausmann + + SPDX-License-Identifier: GPL-3.0-only + This file is also available under commercial licensing terms. + Please contact info@sixtyfps.io for more information. +LICENSE END */ + +Foo := Rectangle { + callback hallo_alias <=> xxx.hallo; + callback clicked <=> are.clicked; + xxx := Rectangle { + callback hallo(int) -> int; + hallo(a) => { return a + 88; } + } + + are := TouchArea { } +} + +TestCase := Rectangle { + + callback foo1_alias <=> foo1.hallo_alias; + callback foo2_alias <=> foo2.hallo_alias; + + callback foo1_clicked <=> foo1.clicked; + + callback call_foo2(int) -> int; + call_foo2(a) => { return foo2.hallo_alias(a); } + + foo1 := Foo { + hallo_alias(a) => { return a + 22; } + } + + foo2 := Foo { + clicked => { debug(42) } + } + + property test: foo1_alias(100) == 122 && foo2_alias(100) == 188; +} + +/* +```rust +let instance = TestCase::new(); +assert_eq!(instance.invoke_foo1_alias(100), 122); +assert_eq!(instance.invoke_foo2_alias(100), 188); +assert_eq!(instance.invoke_call_foo2(100), 188); +``` + +```cpp +auto handle = TestCase::create(); +const TestCase &instance = *handle; +assert_eq(instance.invoke_foo1_alias(100), 122); +assert_eq(instance.invoke_foo2_alias(100), 188); +assert_eq(instance.invoke_call_foo2(100), 188); +``` + + +```js +var instance = new sixtyfps.TestCase(); +assert.equal(instance.foo1_alias(100), 122); +assert.equal(instance.foo2_alias(100), 188); +assert.equal(instance.call_foo2(100), 188); +``` +*/ \ No newline at end of file