Add support for calling focus() on TextInput elements

This allows activating text inputs in signal handlers connected for
example to buttons.

This implements parts of #55
This commit is contained in:
Simon Hausmann 2020-09-30 14:35:02 +02:00
parent 6d9af3449a
commit 7e0e7b43f0
9 changed files with 182 additions and 17 deletions

View file

@ -244,6 +244,9 @@ The `TextInput` is a lower-level item that shows text and allows entering text.
within the item within the item
* **`has_focus`** (*bool*): Set to true when item is focused and receives keyboard events. * **`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 ### Example

View file

@ -575,3 +575,21 @@ App := Window {
} }
} }
``` ```
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";
}
}
}
```

View file

@ -10,8 +10,7 @@ LICENSE END */
use crate::diagnostics::{BuildDiagnostics, Spanned, SpannedWithSourceFile}; use crate::diagnostics::{BuildDiagnostics, Spanned, SpannedWithSourceFile};
use crate::object_tree::*; use crate::object_tree::*;
use crate::parser::SyntaxNodeWithSourceFile; use crate::parser::SyntaxNodeWithSourceFile;
use crate::typeregister::BuiltinElement; use crate::typeregister::{BuiltinElement, EnumerationValue, Type};
use crate::typeregister::{EnumerationValue, Type};
use core::cell::RefCell; use core::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::hash::Hash; use std::hash::Hash;
@ -188,6 +187,14 @@ pub enum Expression {
/// Reference to a function built into the run-time, implemented natively /// Reference to a function built into the run-time, implemented natively
BuiltinFunctionReference(BuiltinFunction), 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<Expression>,
base_node: SyntaxNodeWithSourceFile,
member: Box<Expression>,
},
/// A reference to a specific element. This isn't possible to create in .60 syntax itself, but intermediate passes may generate this /// 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. /// type of expression.
ElementReference(Weak<RefCell<Element>>), ElementReference(Weak<RefCell<Element>>),
@ -323,6 +330,7 @@ impl Expression {
element.upgrade().unwrap().borrow().lookup_property(name) element.upgrade().unwrap().borrow().lookup_property(name)
} }
Expression::BuiltinFunctionReference(funcref) => funcref.ty(), Expression::BuiltinFunctionReference(funcref) => funcref.ty(),
Expression::MemberFunction { member, .. } => member.ty(),
Expression::ElementReference(_) => Type::ElementReference, Expression::ElementReference(_) => Type::ElementReference,
Expression::RepeaterIndexReference { .. } => Type::Int32, Expression::RepeaterIndexReference { .. } => Type::Int32,
Expression::RepeaterModelReference { element } => { Expression::RepeaterModelReference { element } => {
@ -409,6 +417,10 @@ impl Expression {
Expression::PropertyReference { .. } => {} Expression::PropertyReference { .. } => {}
Expression::FunctionParameterReference { .. } => {} Expression::FunctionParameterReference { .. } => {}
Expression::BuiltinFunctionReference { .. } => {} Expression::BuiltinFunctionReference { .. } => {}
Expression::MemberFunction { base, member, .. } => {
visitor(&**base);
visitor(&**member);
}
Expression::ElementReference(_) => {} Expression::ElementReference(_) => {}
Expression::ObjectAccess { base, .. } => visitor(&**base), Expression::ObjectAccess { base, .. } => visitor(&**base),
Expression::RepeaterIndexReference { .. } => {} Expression::RepeaterIndexReference { .. } => {}
@ -472,6 +484,10 @@ impl Expression {
Expression::PropertyReference { .. } => {} Expression::PropertyReference { .. } => {}
Expression::FunctionParameterReference { .. } => {} Expression::FunctionParameterReference { .. } => {}
Expression::BuiltinFunctionReference { .. } => {} Expression::BuiltinFunctionReference { .. } => {}
Expression::MemberFunction { base, member, .. } => {
visitor(&mut **base);
visitor(&mut **member);
}
Expression::ElementReference(_) => {} Expression::ElementReference(_) => {}
Expression::ObjectAccess { base, .. } => visitor(&mut **base), Expression::ObjectAccess { base, .. } => visitor(&mut **base),
Expression::RepeaterIndexReference { .. } => {} Expression::RepeaterIndexReference { .. } => {}
@ -534,6 +550,7 @@ impl Expression {
Expression::SignalReference { .. } => false, Expression::SignalReference { .. } => false,
Expression::PropertyReference { .. } => false, Expression::PropertyReference { .. } => false,
Expression::BuiltinFunctionReference { .. } => false, Expression::BuiltinFunctionReference { .. } => false,
Expression::MemberFunction { .. } => false,
Expression::ElementReference(_) => false, Expression::ElementReference(_) => false,
Expression::RepeaterIndexReference { .. } => false, Expression::RepeaterIndexReference { .. } => false,
Expression::RepeaterModelReference { .. } => false, Expression::RepeaterModelReference { .. } => false,

View file

@ -1073,6 +1073,7 @@ fn compile_expression(e: &crate::expression_tree::Expression, component: &Rc<Com
} }
}, },
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 } => { Expression::RepeaterIndexReference { element } => {
let access = access_member( let access = access_member(
&element.upgrade().unwrap().borrow().base_type.as_component().root_element, &element.upgrade().unwrap().borrow().base_type.as_component().root_element,

View file

@ -886,6 +886,7 @@ fn compile_expression(e: &Expression, component: &Rc<Component>) -> TokenStream
BuiltinFunction::SetFocusItem => panic!("internal error: SetFocusItem is handled directly in CallFunction") 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 } => { Expression::RepeaterIndexReference { element } => {
let access = access_member( let access = access_member(
&element.upgrade().unwrap().borrow().base_type.as_component().root_element, &element.upgrade().unwrap().borrow().base_type.as_component().root_element,

View file

@ -405,6 +405,14 @@ impl Expression {
element: Rc::downgrade(&elem), element: Rc::downgrade(&elem),
name: prop_name.to_string(), 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 { } else {
ctx.diag.push_error(format!("Cannot access property '{}'", prop_name), &prop_name); ctx.diag.push_error(format!("Cannot access property '{}'", prop_name), &prop_name);
return Self::Invalid; return Self::Invalid;
@ -497,9 +505,19 @@ impl Expression {
ctx: &mut LookupCtx, ctx: &mut LookupCtx,
) -> Expression { ) -> Expression {
let mut sub_expr = let mut sub_expr =
node.Expression().map(|n| (Self::from_expression_node(n.clone(), ctx), n)); node.Expression().map(|n| (Self::from_expression_node(n.clone(), ctx), n.0));
let function = Box::new(sub_expr.next().map_or(Expression::Invalid, |e| e.0));
let arguments = sub_expr.collect::<Vec<_>>(); 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() { let arguments = match function.ty() {
Type::Function { args, .. } | Type::Signal { args } => { Type::Function { args, .. } | Type::Signal { args } => {

View file

@ -10,7 +10,7 @@ LICENSE END */
use std::collections::{BTreeMap, HashMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use std::{cell::RefCell, fmt::Display, rc::Rc}; 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; use crate::object_tree::Component;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -232,6 +232,13 @@ impl Type {
tr.lookup_element(name) 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 /// Assume this is a builtin type, panic if it isn't
pub fn as_builtin(&self) -> &BuiltinElement { pub fn as_builtin(&self) -> &BuiltinElement {
match self { match self {
@ -435,6 +442,7 @@ pub struct BuiltinElement {
pub disallow_global_types_as_child_elements: bool, pub disallow_global_types_as_child_elements: bool,
/// Non-item type do not have reserved properties (x/width/rowspan/...) added to them (eg: PropertyAnimation) /// Non-item type do not have reserved properties (x/width/rowspan/...) added to them (eg: PropertyAnimation)
pub is_non_item_type: bool, pub is_non_item_type: bool,
pub member_functions: HashMap<String, Expression>,
} }
impl BuiltinElement { impl BuiltinElement {
@ -525,19 +533,32 @@ impl TypeRegister {
let text_vertical_alignment = let text_vertical_alignment =
declare_enum("TextVerticalAlignment", &["align_top", "align_center", "align_bottom"]); 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, let native_class = |tr: &mut TypeRegister,
name: &str, name: &str,
properties: &[(&str, Type)], properties: &[(&str, Type)],
default_bindings: &[(&str, Expression)]| { default_bindings: &[(&str, Expression)]| {
let native = Rc::new(NativeClass::new_with_properties( native_class_with_member_functions(tr, name, properties, default_bindings, &[])
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)));
}; };
let mut rectangle = NativeClass::new("Rectangle"); 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(&mut r, "Window", &[("width", Type::Length), ("height", Type::Length)], &[]);
native_class( native_class_with_member_functions(
&mut r, &mut r,
"TextInput", "TextInput",
&[ &[
@ -682,6 +703,11 @@ impl TypeRegister {
), ),
("text_cursor_width", Expression::NumberLiteral(2., Unit::Lx)), ("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"))); let mut grid_layout = BuiltinElement::new(Rc::new(NativeClass::new("GridLayout")));

View file

@ -199,6 +199,7 @@ pub fn eval_expression(
"naked builtin function reference not allowed, should be handled by function call" "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::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 }) => { Expression::PropertyReference(NamedReference { element, name }) => {
load_property(component, &element.upgrade().unwrap(), name.as_ref()).unwrap() load_property(component, &element.upgrade().unwrap(), name.as_ref()).unwrap()
} }

View file

@ -0,0 +1,80 @@
/* LICENSE BEGIN
This file is part of the SixtyFPS Project -- https://sixtyfps.io
Copyright (c) 2020 Olivier Goffart <olivier.goffart@sixtyfps.io>
Copyright (c) 2020 Simon Hausmann <simon.hausmann@sixtyfps.io>
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<bool> input1_focused: input1.has_focus;
property<string> input1_text: input1.text;
property<bool> input2_focused: input2.has_focus;
property<string> 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);
```
*/