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
|
||||
* **`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
|
||||
|
||||
|
|
|
@ -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::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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 } => {
|
||||
|
|
|
@ -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")));
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
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