diff --git a/docs/builtin_elements.md b/docs/builtin_elements.md index e80329a59..343bdd8c8 100644 --- a/docs/builtin_elements.md +++ b/docs/builtin_elements.md @@ -244,6 +244,9 @@ The `TextInput` is a lower-level item that shows text and allows entering text. within the item * **`has_focus`** (*bool*): Set to true when item is focused and receives keyboard events. +### Methods + +* **`focus()`** Call this function to focus the text input and make it receive future keyboard events. ### Example diff --git a/docs/langref.md b/docs/langref.md index 33708594f..0cac5aca5 100644 --- a/docs/langref.md +++ b/docs/langref.md @@ -574,4 +574,22 @@ App := Window { label2 := LabeledInput {} } } +``` + +It's also possible to manually activate the focus on elements such as ```TextInput```: + +```60 +import { Button } from "sixtyfps_widgets.60"; + +App := Window { + GridLayout { + Button { + text: "press me"; + clicked => { input.focus(); } + } + input := TextInput { + text: "I am a text input field"; + } + } +} ``` \ No newline at end of file diff --git a/sixtyfps_compiler/expression_tree.rs b/sixtyfps_compiler/expression_tree.rs index 77b9c322f..971665e38 100644 --- a/sixtyfps_compiler/expression_tree.rs +++ b/sixtyfps_compiler/expression_tree.rs @@ -10,8 +10,7 @@ LICENSE END */ use crate::diagnostics::{BuildDiagnostics, Spanned, SpannedWithSourceFile}; use crate::object_tree::*; use crate::parser::SyntaxNodeWithSourceFile; -use crate::typeregister::BuiltinElement; -use crate::typeregister::{EnumerationValue, Type}; +use crate::typeregister::{BuiltinElement, EnumerationValue, Type}; use core::cell::RefCell; use std::collections::HashMap; use std::hash::Hash; @@ -188,6 +187,14 @@ pub enum Expression { /// Reference to a function built into the run-time, implemented natively BuiltinFunctionReference(BuiltinFunction), + /// A MemberFunction expression exists only for a short time, for example for `item.focus()` to be translated to + /// a regular FunctionCall expression where the base becomes the first argument. + MemberFunction { + base: Box, + base_node: SyntaxNodeWithSourceFile, + member: Box, + }, + /// A reference to a specific element. This isn't possible to create in .60 syntax itself, but intermediate passes may generate this /// type of expression. ElementReference(Weak>), @@ -323,6 +330,7 @@ impl Expression { element.upgrade().unwrap().borrow().lookup_property(name) } Expression::BuiltinFunctionReference(funcref) => funcref.ty(), + Expression::MemberFunction { member, .. } => member.ty(), Expression::ElementReference(_) => Type::ElementReference, Expression::RepeaterIndexReference { .. } => Type::Int32, Expression::RepeaterModelReference { element } => { @@ -409,6 +417,10 @@ impl Expression { Expression::PropertyReference { .. } => {} Expression::FunctionParameterReference { .. } => {} Expression::BuiltinFunctionReference { .. } => {} + Expression::MemberFunction { base, member, .. } => { + visitor(&**base); + visitor(&**member); + } Expression::ElementReference(_) => {} Expression::ObjectAccess { base, .. } => visitor(&**base), Expression::RepeaterIndexReference { .. } => {} @@ -472,6 +484,10 @@ impl Expression { Expression::PropertyReference { .. } => {} Expression::FunctionParameterReference { .. } => {} Expression::BuiltinFunctionReference { .. } => {} + Expression::MemberFunction { base, member, .. } => { + visitor(&mut **base); + visitor(&mut **member); + } Expression::ElementReference(_) => {} Expression::ObjectAccess { base, .. } => visitor(&mut **base), Expression::RepeaterIndexReference { .. } => {} @@ -534,6 +550,7 @@ impl Expression { Expression::SignalReference { .. } => false, Expression::PropertyReference { .. } => false, Expression::BuiltinFunctionReference { .. } => false, + Expression::MemberFunction { .. } => false, Expression::ElementReference(_) => false, Expression::RepeaterIndexReference { .. } => false, Expression::RepeaterModelReference { .. } => false, diff --git a/sixtyfps_compiler/generator/cpp.rs b/sixtyfps_compiler/generator/cpp.rs index 5d5dd0770..3493d8884 100644 --- a/sixtyfps_compiler/generator/cpp.rs +++ b/sixtyfps_compiler/generator/cpp.rs @@ -1073,6 +1073,7 @@ fn compile_expression(e: &crate::expression_tree::Expression, component: &Rc todo!("Element references are only supported in the context of built-in function calls at the moment"), + Expression::MemberFunction { .. } => panic!("member function expressions must not appear in the code generator anymore"), Expression::RepeaterIndexReference { element } => { let access = access_member( &element.upgrade().unwrap().borrow().base_type.as_component().root_element, diff --git a/sixtyfps_compiler/generator/rust.rs b/sixtyfps_compiler/generator/rust.rs index 35b5d584e..25e57e200 100644 --- a/sixtyfps_compiler/generator/rust.rs +++ b/sixtyfps_compiler/generator/rust.rs @@ -885,7 +885,8 @@ fn compile_expression(e: &Expression, component: &Rc) -> TokenStream BuiltinFunction::Debug => quote!((|x| println!("{:?}", x))), BuiltinFunction::SetFocusItem => panic!("internal error: SetFocusItem is handled directly in CallFunction") }, - Expression::ElementReference(_) => todo!("Element references are only supported in the context of built-in function calls at the moment"), + Expression::ElementReference(_) => todo!("Element references are only supported in the context of built-in function calls at the moment"), + Expression::MemberFunction{ .. } => panic!("member function expressions must not appear in the code generator anymore"), Expression::RepeaterIndexReference { element } => { let access = access_member( &element.upgrade().unwrap().borrow().base_type.as_component().root_element, diff --git a/sixtyfps_compiler/passes/resolving.rs b/sixtyfps_compiler/passes/resolving.rs index 9a5ce7b0e..f6d4713d9 100644 --- a/sixtyfps_compiler/passes/resolving.rs +++ b/sixtyfps_compiler/passes/resolving.rs @@ -405,6 +405,14 @@ impl Expression { element: Rc::downgrade(&elem), name: prop_name.to_string(), }); + } else if matches!(p, Type::Function{..}) { + let member = + elem.borrow().base_type.lookup_member_function(prop_name.text().as_str()); + return Self::MemberFunction { + base: Box::new(Expression::ElementReference(Rc::downgrade(&elem))), + base_node: node, + member: Box::new(member), + }; } else { ctx.diag.push_error(format!("Cannot access property '{}'", prop_name), &prop_name); return Self::Invalid; @@ -497,9 +505,19 @@ impl Expression { ctx: &mut LookupCtx, ) -> Expression { let mut sub_expr = - node.Expression().map(|n| (Self::from_expression_node(n.clone(), ctx), n)); - let function = Box::new(sub_expr.next().map_or(Expression::Invalid, |e| e.0)); - let arguments = sub_expr.collect::>(); + node.Expression().map(|n| (Self::from_expression_node(n.clone(), ctx), n.0)); + + let mut arguments = Vec::new(); + + let function = sub_expr.next().map_or(Expression::Invalid, |e| e.0); + let function = if let Expression::MemberFunction { base, base_node, member } = function { + arguments.push((*base, base_node)); + member + } else { + Box::new(function) + }; + + arguments.extend(sub_expr); let arguments = match function.ty() { Type::Function { args, .. } | Type::Signal { args } => { diff --git a/sixtyfps_compiler/typeregister.rs b/sixtyfps_compiler/typeregister.rs index ae4c4bde5..ee9b7298d 100644 --- a/sixtyfps_compiler/typeregister.rs +++ b/sixtyfps_compiler/typeregister.rs @@ -10,7 +10,7 @@ LICENSE END */ use std::collections::{BTreeMap, HashMap, HashSet}; use std::{cell::RefCell, fmt::Display, rc::Rc}; -use crate::expression_tree::{Expression, Unit}; +use crate::expression_tree::{BuiltinFunction, Expression, Unit}; use crate::object_tree::Component; #[derive(Debug, Clone)] @@ -232,6 +232,13 @@ impl Type { tr.lookup_element(name) } + pub fn lookup_member_function(&self, name: &str) -> Expression { + match self { + Type::Builtin(builtin) => builtin.member_functions.get(name).unwrap().clone(), + _ => Expression::Invalid, + } + } + /// Assume this is a builtin type, panic if it isn't pub fn as_builtin(&self) -> &BuiltinElement { match self { @@ -435,6 +442,7 @@ pub struct BuiltinElement { pub disallow_global_types_as_child_elements: bool, /// Non-item type do not have reserved properties (x/width/rowspan/...) added to them (eg: PropertyAnimation) pub is_non_item_type: bool, + pub member_functions: HashMap, } impl BuiltinElement { @@ -525,19 +533,32 @@ impl TypeRegister { let text_vertical_alignment = declare_enum("TextVerticalAlignment", &["align_top", "align_center", "align_bottom"]); + let native_class_with_member_functions = + |tr: &mut TypeRegister, + name: &str, + properties: &[(&str, Type)], + default_bindings: &[(&str, Expression)], + member_functions: &[(&str, Type, Expression)]| { + let native = Rc::new(NativeClass::new_with_properties( + name, + properties.iter().map(|(n, t)| (n.to_string(), t.clone())), + )); + let mut builtin = BuiltinElement::new(native); + for (prop, expr) in default_bindings { + builtin.default_bindings.insert(prop.to_string(), expr.clone()); + } + for (name, funtype, fun) in member_functions { + builtin.properties.insert(name.to_string(), funtype.clone()); + builtin.member_functions.insert(name.to_string(), fun.clone()); + } + tr.types.insert(name.to_string(), Type::Builtin(Rc::new(builtin))); + }; + let native_class = |tr: &mut TypeRegister, name: &str, properties: &[(&str, Type)], default_bindings: &[(&str, Expression)]| { - let native = Rc::new(NativeClass::new_with_properties( - name, - properties.iter().map(|(n, t)| (n.to_string(), t.clone())), - )); - let mut builtin = BuiltinElement::new(native); - for (prop, expr) in default_bindings { - builtin.default_bindings.insert(prop.to_string(), expr.clone()); - } - tr.types.insert(name.to_string(), Type::Builtin(Rc::new(builtin))); + native_class_with_member_functions(tr, name, properties, default_bindings, &[]) }; let mut rectangle = NativeClass::new("Rectangle"); @@ -636,7 +657,7 @@ impl TypeRegister { native_class(&mut r, "Window", &[("width", Type::Length), ("height", Type::Length)], &[]); - native_class( + native_class_with_member_functions( &mut r, "TextInput", &[ @@ -682,6 +703,11 @@ impl TypeRegister { ), ("text_cursor_width", Expression::NumberLiteral(2., Unit::Lx)), ], + &[( + "focus", + Type::Function { return_type: Box::new(Type::Void), args: vec![] }, + Expression::BuiltinFunctionReference(BuiltinFunction::SetFocusItem), + )], ); let mut grid_layout = BuiltinElement::new(Rc::new(NativeClass::new("GridLayout"))); diff --git a/sixtyfps_runtime/interpreter/eval.rs b/sixtyfps_runtime/interpreter/eval.rs index b08163034..900d097a5 100644 --- a/sixtyfps_runtime/interpreter/eval.rs +++ b/sixtyfps_runtime/interpreter/eval.rs @@ -199,6 +199,7 @@ pub fn eval_expression( "naked builtin function reference not allowed, should be handled by function call" ), Expression::ElementReference(_) => todo!("Element references are only supported in the context of built-in function calls at the moment"), + Expression::MemberFunction { .. } => panic!("member function expressions must not appear in the code generator anymore"), Expression::PropertyReference(NamedReference { element, name }) => { load_property(component, &element.upgrade().unwrap(), name.as_ref()).unwrap() } diff --git a/tests/cases/focus/focus_change_through_signal.60 b/tests/cases/focus/focus_change_through_signal.60 new file mode 100644 index 000000000..ce94a7e84 --- /dev/null +++ b/tests/cases/focus/focus_change_through_signal.60 @@ -0,0 +1,80 @@ +/* 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 */ +TestCase := Rectangle { + width: 400px; + height: 400px; + + signal focus_input1(); + focus_input1 => { input1.focus(); } + + signal focus_input2(); + focus_input2 => { input2.focus(); } + + input1 := TextInput { + width: parent.width; + height: 200px; + } + + input2 := TextInput { + y: 200px; + width: parent.width; + height: 200px; + } + + property input1_focused: input1.has_focus; + property input1_text: input1.text; + property input2_focused: input2.has_focus; + property input2_text: input2.text; +} + +/* +```rust +let instance = TestCase::new(); +let instance = instance.as_ref(); +assert!(!instance.get_input1_focused()); +assert!(!instance.get_input2_focused()); + +instance.emit_focus_input1(); +assert!(instance.get_input1_focused()); +assert!(!instance.get_input2_focused()); + +instance.emit_focus_input2(); +assert!(!instance.get_input1_focused()); +assert!(instance.get_input2_focused()); +``` + +```cpp +TestCase instance; +assert(!instance.get_input1_focused()); +assert(!instance.get_input2_focused()); + +instance.emit_focus_input1(); +assert(instance.get_input1_focused()); +assert(!instance.get_input2_focused()); + +instance.emit_focus_input2(); +assert(!instance.get_input1_focused()); +assert(instance.get_input2_focused()); +``` + +```js +var instance = new sixtyfps.TestCase(); +assert(!instance.input1_focused); +assert(!instance.input2_focused); + +instance.focus_input1(); +assert(instance.input1_focused); +assert(!instance.input2_focused); + +instance.focus_input2(); +assert(!instance.input1_focused); +assert(instance.input2_focused); +``` +*/