mirror of
https://github.com/slint-ui/slint.git
synced 2025-10-01 14:21:16 +00:00
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:
parent
6d9af3449a
commit
7e0e7b43f0
9 changed files with 182 additions and 17 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 } => {
|
||||||
|
|
|
@ -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")));
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
80
tests/cases/focus/focus_change_through_signal.60
Normal file
80
tests/cases/focus/focus_change_through_signal.60
Normal 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);
|
||||||
|
```
|
||||||
|
*/
|
Loading…
Add table
Add a link
Reference in a new issue