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
* **`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

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::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<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
/// type of expression.
ElementReference(Weak<RefCell<Element>>),
@ -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,

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::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,

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")
},
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,

View file

@ -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::<Vec<_>>();
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 } => {

View file

@ -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<String, Expression>,
}
impl BuiltinElement {
@ -525,10 +533,12 @@ impl TypeRegister {
let text_vertical_alignment =
declare_enum("TextVerticalAlignment", &["align_top", "align_center", "align_bottom"]);
let native_class = |tr: &mut TypeRegister,
let native_class_with_member_functions =
|tr: &mut TypeRegister,
name: &str,
properties: &[(&str, Type)],
default_bindings: &[(&str, Expression)]| {
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())),
@ -537,9 +547,20 @@ impl TypeRegister {
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)]| {
native_class_with_member_functions(tr, name, properties, default_bindings, &[])
};
let mut rectangle = NativeClass::new("Rectangle");
rectangle.properties.insert("color".to_owned(), Type::Color);
rectangle.properties.insert("x".to_owned(), Type::Length);
@ -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")));

View file

@ -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()
}

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);
```
*/