Add support for the initial_focus synthetic property

Setting it will translate to a set_focus_item call in the constructor.

This implements parts of #55
This commit is contained in:
Simon Hausmann 2020-09-25 15:15:17 +02:00
parent 2050f08f1e
commit 9ad8968529
22 changed files with 542 additions and 36 deletions

View file

@ -95,6 +95,11 @@ struct ComponentWindow
&inner, VRef<ComponentVTable> { &Component::component_type, c }); &inner, VRef<ComponentVTable> { &Component::component_type, c });
} }
void set_focus_item(Pin<VRef<ComponentVTable>> c, Pin<VRef<ItemVTable>> item)
{
cbindgen_private::sixtyfps_component_window_set_focus_item(&inner, c, item);
}
private: private:
cbindgen_private::ComponentWindowOpaque inner; cbindgen_private::ComponentWindowOpaque inner;
}; };

View file

@ -184,7 +184,8 @@ fn to_eval_value<'cx>(
| Type::Signal { .. } | Type::Signal { .. }
| Type::Easing | Type::Easing
| Type::Component(_) // The struct case is handled before | Type::Component(_) // The struct case is handled before
| Type::PathElements => cx.throw_error("Cannot convert to a Sixtyfps property value"), | Type::PathElements
| Type::ElementReference => cx.throw_error("Cannot convert to a Sixtyfps property value"),
} }
} }

View file

@ -242,6 +242,7 @@ The `TextInput` is a lower-level item that shows text and allows entering text.
* **`color`** (*color*): The color of the text (default: transparent) * **`color`** (*color*): The color of the text (default: transparent)
* **`horizontal_alignment`**, **`vertical_alignment`** (*FIXME: enum*): How is the text aligned * **`horizontal_alignment`**, **`vertical_alignment`** (*FIXME: enum*): How is the text aligned
within the item within the item
* **`has_focus`** (*bool*): Set to true when item is focused and receives keyboard events.
### Example ### Example

View file

@ -524,3 +524,54 @@ App := Rectangle {
Button {} // from button.60 Button {} // from button.60
} }
``` ```
## Focus Handling
Certain elements such as ```TextInput``` accept not only input from the mouse/finger but
also key events originating from (virtual) keyboards. In order for an item to receive
these events, it must have the focus. This is visible through the `has_focus` property.
Items themselves may acquire the focus as a result of user input such as a mouse click.
In addition, developers can specify which items shall receive the focus when the application
window initially receives the focus. This is specified through the `initial_focus` property,
for which bindings may be declared at the root of components. For example in the following
scene with two ```TextInput``` elements, it is the second one that'll have the initial keyboard
focus when they're shown:
```60
App := Window {
initial_focus: second_input_field;
GridLayout {
Row {
first_input_field := TextInput {}
}
Row {
second_input_field := TextInput {}
}
}
}
```
The initial focus may also be propagated through reusable components:
```60
LabeledInput := GridLayout {
initial_focus: input;
Row {
Text {
text: "Input Label:";
}
input := TextInput {}
}
}
App := Window {
initial_focus: label2;
GridLayout {
label1 := LabeledInput {}
label2 := LabeledInput {}
}
}
```

View file

@ -44,6 +44,7 @@ impl Hash for NamedReference {
pub enum BuiltinFunction { pub enum BuiltinFunction {
GetWindowScaleFactor, GetWindowScaleFactor,
Debug, Debug,
SetFocusItem,
} }
impl BuiltinFunction { impl BuiltinFunction {
@ -55,6 +56,10 @@ impl BuiltinFunction {
BuiltinFunction::Debug => { BuiltinFunction::Debug => {
Type::Function { return_type: Box::new(Type::Void), args: vec![Type::String] } Type::Function { return_type: Box::new(Type::Void), args: vec![Type::String] }
} }
BuiltinFunction::SetFocusItem => Type::Function {
return_type: Box::new(Type::Void),
args: vec![Type::ElementReference],
},
} }
} }
} }
@ -183,6 +188,10 @@ 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 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>>),
/// Reference to the index variable of a repeater /// Reference to the index variable of a repeater
/// ///
/// Example: `idx` in `for xxx[idx] in ...`. The element is the reference to the /// Example: `idx` in `for xxx[idx] in ...`. The element is the reference to the
@ -314,6 +323,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::ElementReference(_) => Type::ElementReference,
Expression::RepeaterIndexReference { .. } => Type::Int32, Expression::RepeaterIndexReference { .. } => Type::Int32,
Expression::RepeaterModelReference { element } => { Expression::RepeaterModelReference { element } => {
if let Expression::Cast { from, .. } = element if let Expression::Cast { from, .. } = element
@ -399,6 +409,7 @@ impl Expression {
Expression::PropertyReference { .. } => {} Expression::PropertyReference { .. } => {}
Expression::FunctionParameterReference { .. } => {} Expression::FunctionParameterReference { .. } => {}
Expression::BuiltinFunctionReference { .. } => {} Expression::BuiltinFunctionReference { .. } => {}
Expression::ElementReference(_) => {}
Expression::ObjectAccess { base, .. } => visitor(&**base), Expression::ObjectAccess { base, .. } => visitor(&**base),
Expression::RepeaterIndexReference { .. } => {} Expression::RepeaterIndexReference { .. } => {}
Expression::RepeaterModelReference { .. } => {} Expression::RepeaterModelReference { .. } => {}
@ -461,6 +472,7 @@ impl Expression {
Expression::PropertyReference { .. } => {} Expression::PropertyReference { .. } => {}
Expression::FunctionParameterReference { .. } => {} Expression::FunctionParameterReference { .. } => {}
Expression::BuiltinFunctionReference { .. } => {} Expression::BuiltinFunctionReference { .. } => {}
Expression::ElementReference(_) => {}
Expression::ObjectAccess { base, .. } => visitor(&mut **base), Expression::ObjectAccess { base, .. } => visitor(&mut **base),
Expression::RepeaterIndexReference { .. } => {} Expression::RepeaterIndexReference { .. } => {}
Expression::RepeaterModelReference { .. } => {} Expression::RepeaterModelReference { .. } => {}
@ -522,6 +534,7 @@ impl Expression {
Expression::SignalReference { .. } => false, Expression::SignalReference { .. } => false,
Expression::PropertyReference { .. } => false, Expression::PropertyReference { .. } => false,
Expression::BuiltinFunctionReference { .. } => false, Expression::BuiltinFunctionReference { .. } => false,
Expression::ElementReference(_) => false,
Expression::RepeaterIndexReference { .. } => false, Expression::RepeaterIndexReference { .. } => false,
Expression::RepeaterModelReference { .. } => false, Expression::RepeaterModelReference { .. } => false,
Expression::FunctionParameterReference { .. } => false, Expression::FunctionParameterReference { .. } => false,
@ -678,7 +691,8 @@ impl Expression {
| Type::Native(_) | Type::Native(_)
| Type::Signal { .. } | Type::Signal { .. }
| Type::Function { .. } | Type::Function { .. }
| Type::Void => Expression::Invalid, | Type::Void
| Type::ElementReference => Expression::Invalid,
Type::Float32 => Expression::NumberLiteral(0., Unit::None), Type::Float32 => Expression::NumberLiteral(0., Unit::None),
Type::Int32 => Expression::NumberLiteral(0., Unit::None), Type::Int32 => Expression::NumberLiteral(0., Unit::None),
Type::String => Expression::StringLiteral(String::new()), Type::String => Expression::StringLiteral(String::new()),

View file

@ -775,6 +775,10 @@ fn generate_component(
} }
}); });
for extra_init_code in component.setup_code.borrow().iter() {
init.push(compile_expression(extra_init_code, component));
}
component_struct.members.push(( component_struct.members.push((
Access::Public, Access::Public,
Declaration::Function(Function { Declaration::Function(Function {
@ -1064,7 +1068,11 @@ fn compile_expression(e: &crate::expression_tree::Expression, component: &Rc<Com
"[](auto... args){ (std::cout << ... << args) << std::endl; return nullptr; }" "[](auto... args){ (std::cout << ... << args) << std::endl; return nullptr; }"
.into() .into()
} }
BuiltinFunction::SetFocusItem => {
format!("{}.set_focus_item", window_ref_expression(component))
}
}, },
Expression::ElementReference(_) => todo!("Element references are only supported in the context of built-in function calls at the moment"),
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,
@ -1136,10 +1144,33 @@ fn compile_expression(e: &crate::expression_tree::Expression, component: &Rc<Com
format!("[&]{{ {} }}()", x.join(";")) format!("[&]{{ {} }}()", x.join(";"))
} }
Expression::FunctionCall { function, arguments } => { Expression::FunctionCall { function, arguments } => match &**function {
Expression::BuiltinFunctionReference(BuiltinFunction::SetFocusItem) => {
if arguments.len() != 1 {
panic!("internal error: incorrect argument count to SetFocusItem call");
}
if let Expression::ElementReference(focus_item) = &arguments[0] {
let focus_item = focus_item.upgrade().unwrap();
let focus_item = focus_item.borrow();
let window_ref = window_ref_expression(component);
let component =
format!("{{&{}::component_type, this}}", component_id(component));
let item = format!(
"{{&sixtyfps::private_api::{vt}, &{item}}}",
vt = focus_item.base_type.as_native().vtable_symbol,
item = focus_item.id
);
format!("{}.set_focus_item({}, {});", window_ref, component, item)
} else {
panic!("internal error: argument to SetFocusItem must be an element")
}
}
_ => {
let mut args = arguments.iter().map(|e| compile_expression(e, component)); let mut args = arguments.iter().map(|e| compile_expression(e, component));
format!("{}({})", compile_expression(&function, component), args.join(", ")) format!("{}({})", compile_expression(&function, component), args.join(", "))
} }
},
Expression::SelfAssignment { lhs, rhs, op } => { Expression::SelfAssignment { lhs, rhs, op } => {
let rhs = compile_expression(&*rhs, &component); let rhs = compile_expression(&*rhs, &component);
compile_assignment(lhs, *op, rhs, component) compile_assignment(lhs, *op, rhs, component)

View file

@ -524,6 +524,10 @@ fn generate_component(
}) })
}; };
for extra_init_code in component.setup_code.borrow().iter() {
init.push(compile_expression(extra_init_code, component));
}
Some(quote!( Some(quote!(
#(#resource_symbols)* #(#resource_symbols)*
@ -674,6 +678,7 @@ fn generate_component(
let self_pinned = std::rc::Rc::pin(self_); let self_pinned = std::rc::Rc::pin(self_);
self_pinned.self_weak.set(PinWeak::downgrade(self_pinned.clone())).map_err(|_|()) self_pinned.self_weak.set(PinWeak::downgrade(self_pinned.clone())).map_err(|_|())
.expect("Can only be pinned once"); .expect("Can only be pinned once");
let _self = self_pinned.as_ref();
#(#init)* #(#init)*
self_pinned self_pinned
} }
@ -878,7 +883,9 @@ fn compile_expression(e: &Expression, component: &Rc<Component>) -> TokenStream
quote!(#window_ref.scale_factor) quote!(#window_ref.scale_factor)
} }
BuiltinFunction::Debug => quote!((|x| println!("{:?}", x))), 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::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,
@ -932,9 +939,26 @@ fn compile_expression(e: &Expression, component: &Rc<Component>) -> TokenStream
false, false,
), ),
Expression::FunctionCall { function, arguments } => { Expression::FunctionCall { function, arguments } => {
match &**function {
Expression::BuiltinFunctionReference(BuiltinFunction::SetFocusItem) => {
if arguments.len() != 1 {
panic!("internal error: incorrect argument count to SetFocusItem call");
}
if let Expression::ElementReference(focus_item) = &arguments[0] {
let item = format_ident!("{}", focus_item.upgrade().unwrap().borrow().id);
let window_ref = window_ref_expression(component);
quote!(
#window_ref.set_focus_item(VRef::new_pin(self_pinned.as_ref()), VRef::new_pin(Self::FIELD_OFFSETS.#item.apply_pin(self_pinned.as_ref())));
)
} else {
panic!("internal error: argument to SetFocusItem must be an element")
}
}
_ => {
let f = compile_expression(function, &component); let f = compile_expression(function, &component);
let a = arguments.iter().map(|a| compile_expression(a, &component)); let a = arguments.iter().map(|a| compile_expression(a, &component));
if let Type::Signal { args } = function.ty() { let function_type = function.ty();
if let Type::Signal { args } = function_type {
let cast = args.iter().map(|ty| match ty { let cast = args.iter().map(|ty| match ty {
Type::Bool => quote!(as bool), Type::Bool => quote!(as bool),
Type::Int32 => quote!(as i32), Type::Int32 => quote!(as i32),
@ -946,6 +970,9 @@ fn compile_expression(e: &Expression, component: &Rc<Component>) -> TokenStream
quote! { #f(#(#a.clone()),*)} quote! { #f(#(#a.clone()),*)}
} }
} }
}
}
Expression::SelfAssignment { lhs, rhs, op } => { Expression::SelfAssignment { lhs, rhs, op } => {
let rhs = compile_expression(&*rhs, &component); let rhs = compile_expression(&*rhs, &component);
compile_assignment(lhs, *op, rhs, component) compile_assignment(lhs, *op, rhs, component)

View file

@ -47,6 +47,7 @@ mod passes {
pub mod collect_resources; pub mod collect_resources;
pub mod compile_paths; pub mod compile_paths;
pub mod deduplicate_property_read; pub mod deduplicate_property_read;
pub mod focus_item;
pub mod inlining; pub mod inlining;
pub mod lower_layout; pub mod lower_layout;
pub mod lower_states; pub mod lower_states;
@ -122,6 +123,7 @@ pub fn run_passes(
passes::inlining::inline(doc); passes::inlining::inline(doc);
passes::compile_paths::compile_paths(&doc.root_component, &doc.local_registry, diag); passes::compile_paths::compile_paths(&doc.root_component, &doc.local_registry, diag);
passes::unique_id::assign_unique_id(&doc.root_component); passes::unique_id::assign_unique_id(&doc.root_component);
passes::focus_item::determine_initial_focus_item(&doc.root_component, diag);
passes::materialize_fake_properties::materialize_fake_properties(&doc.root_component); passes::materialize_fake_properties::materialize_fake_properties(&doc.root_component);
passes::collect_resources::collect_resources(&doc.root_component); passes::collect_resources::collect_resources(&doc.root_component);
doc.root_component.embed_file_resources.set(compiler_config.embed_resources); doc.root_component.embed_file_resources.set(compiler_config.embed_resources);

View file

@ -14,7 +14,7 @@ LICENSE END */
use crate::diagnostics::{FileDiagnostics, Spanned, SpannedWithSourceFile}; use crate::diagnostics::{FileDiagnostics, Spanned, SpannedWithSourceFile};
use crate::expression_tree::{Expression, ExpressionSpanned, NamedReference}; use crate::expression_tree::{Expression, ExpressionSpanned, NamedReference};
use crate::parser::{syntax_nodes, SyntaxKind, SyntaxNodeWithSourceFile}; use crate::parser::{syntax_nodes, SyntaxKind, SyntaxNodeWithSourceFile};
use crate::typeregister::{Type, TypeRegister}; use crate::typeregister::{NativeClass, Type, TypeRegister};
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use std::collections::HashMap; use std::collections::HashMap;
use std::rc::{Rc, Weak}; use std::rc::{Rc, Weak};
@ -100,6 +100,9 @@ pub struct Component {
/// When creating this component and inserting "children", append them to the children of /// When creating this component and inserting "children", append them to the children of
/// the element pointer to by this field. /// the element pointer to by this field.
pub child_insertion_point: RefCell<Option<ElementRc>>, pub child_insertion_point: RefCell<Option<ElementRc>>,
/// Code to be inserted into the constructor
pub setup_code: RefCell<Vec<Expression>>,
} }
impl Component { impl Component {
@ -647,6 +650,20 @@ impl Element {
} }
} }
} }
pub fn native_class(&self) -> Option<Rc<NativeClass>> {
let mut base_type = self.base_type.clone();
loop {
match &base_type {
Type::Component(component) => {
base_type = component.root_element.clone().borrow().base_type.clone();
}
Type::Builtin(builtin) => break Some(builtin.native_class.clone()),
Type::Native(native) => break Some(native.clone()),
_ => break None,
}
}
}
} }
fn type_from_node(node: syntax_nodes::Type, diag: &mut FileDiagnostics, tr: &TypeRegister) -> Type { fn type_from_node(node: syntax_nodes::Type, diag: &mut FileDiagnostics, tr: &TypeRegister) -> Type {

View file

@ -0,0 +1,71 @@
/* 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 */
//! This pass follows the initial_focus property on the root element to determine the initial focus item
use std::rc::Rc;
use crate::{
diagnostics::BuildDiagnostics,
expression_tree::{BuiltinFunction, Expression},
object_tree::*,
};
pub fn determine_initial_focus_item(component: &Rc<Component>, diag: &mut BuildDiagnostics) {
let mut focus_item_candidate = component.root_element.clone();
if !focus_item_candidate.borrow().bindings.contains_key("initial_focus") {
return;
}
let focus_item = loop {
let (focus_target, binding_location) = {
let mut focus_item_mut = focus_item_candidate.borrow_mut();
let binding = focus_item_mut.bindings.remove("initial_focus").unwrap();
focus_item_mut.property_declarations.remove("initial_focus");
if let Expression::ElementReference(target) = &binding.expression {
(target.upgrade().unwrap(), binding.clone())
} else {
diag.push_error(
"internal error: initial_focus property is of type ElementReference but received non-element-reference binding".to_owned(),
&binding,
);
break None;
}
};
if focus_target.borrow().bindings.contains_key("initial_focus") {
focus_item_candidate = focus_target;
} else {
if let Some(native_class) = focus_target.borrow().native_class() {
if native_class.lookup_property("has_focus").is_some() {
break Some(focus_target.clone());
} else {
diag.push_error("element is not focusable".to_owned(), &binding_location);
}
} else {
diag.push_error("internal error: item targeted by initial_focus does not have an underlying native class".to_owned(), &binding_location);
}
break None;
}
};
if let Some(focus_item) = focus_item {
let setup_code = Expression::FunctionCall {
function: Box::new(Expression::BuiltinFunctionReference(BuiltinFunction::SetFocusItem)),
arguments: vec![Expression::ElementReference(Rc::downgrade(&focus_item))],
};
component.setup_code.borrow_mut().push(setup_code);
}
// Remove any stray bindings in other inlined elements to avoid materializing them.
recurse_elem(&component.root_element, &(), &mut |element_rc, _| {
element_rc.borrow_mut().bindings.remove("initial_focus");
});
}

View file

@ -9,7 +9,11 @@
LICENSE END */ LICENSE END */
//! Inline each object_tree::Component within the main Component //! Inline each object_tree::Component within the main Component
use crate::{expression_tree::NamedReference, object_tree::*, typeregister::Type}; use crate::{
expression_tree::{Expression, NamedReference},
object_tree::*,
typeregister::Type,
};
use by_address::ByAddress; use by_address::ByAddress;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
@ -113,6 +117,7 @@ fn inline_element(
// Now fixup all binding and reference // Now fixup all binding and reference
for (_, e) in &mapping { for (_, e) in &mapping {
visit_all_named_references(e, |nr| fixup_reference(nr, &mapping)); visit_all_named_references(e, |nr| fixup_reference(nr, &mapping));
visit_element_expressions(e, |expr, _| fixup_element_references(expr, &mapping));
} }
} }
@ -163,6 +168,19 @@ fn fixup_reference(
} }
} }
fn fixup_element_references(
expr: &mut Expression,
mapping: &HashMap<ByAddress<ElementRc>, ElementRc>,
) {
if let Expression::ElementReference(element) = expr {
if let Some(new_element) =
element.upgrade().and_then(|e| mapping.get(&element_key(e.clone())))
{
*element = Rc::downgrade(new_element);
}
}
}
fn duplicate_transition( fn duplicate_transition(
t: &Transition, t: &Transition,
mapping: &mut HashMap<ByAddress<ElementRc>, Rc<RefCell<Element>>>, mapping: &mut HashMap<ByAddress<ElementRc>, Rc<RefCell<Element>>>,

View file

@ -383,6 +383,8 @@ impl Expression {
if let Some(elem) = elem_opt { if let Some(elem) = elem_opt {
let prop_name = if let Some(second) = it.next() { let prop_name = if let Some(second) = it.next() {
second second
} else if matches!(ctx.property_type, Type::ElementReference) {
return Self::ElementReference(Rc::downgrade(&elem));
} else { } else {
ctx.diag.push_error("Cannot take reference of an element".into(), &node); ctx.diag.push_error("Cannot take reference of an element".into(), &node);
return Self::Invalid; return Self::Invalid;

View file

@ -0,0 +1,18 @@
/* 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 */
X := Rectangle {
initial_focus: someRect;
// ^error{element is not focusable}
someRect := Rectangle {}
}

View file

@ -50,6 +50,8 @@ pub enum Type {
Enumeration(Rc<Enumeration>), Enumeration(Rc<Enumeration>),
EnumerationValue(EnumerationValue), EnumerationValue(EnumerationValue),
ElementReference,
} }
impl core::cmp::PartialEq for Type { impl core::cmp::PartialEq for Type {
@ -80,6 +82,7 @@ impl core::cmp::PartialEq for Type {
(Type::PathElements, Type::PathElements) => true, (Type::PathElements, Type::PathElements) => true,
(Type::Easing, Type::Easing) => true, (Type::Easing, Type::Easing) => true,
(Type::Enumeration(lhs), Type::Enumeration(rhs)) => lhs == rhs, (Type::Enumeration(lhs), Type::Enumeration(rhs)) => lhs == rhs,
(Type::ElementReference, Type::ElementReference) => true,
_ => false, _ => false,
} }
} }
@ -141,6 +144,7 @@ impl Display for Type {
Type::EnumerationValue(value) => { Type::EnumerationValue(value) => {
write!(f, "enum {}::{}", value.enumeration.name, value.to_string()) write!(f, "enum {}::{}", value.enumeration.name, value.to_string())
} }
Type::ElementReference => write!(f, "element ref"),
} }
} }
} }
@ -165,6 +169,7 @@ impl Type {
| Self::Model | Self::Model
| Self::Easing | Self::Easing
| Self::Enumeration(_) | Self::Enumeration(_)
| Self::ElementReference
| Self::Object(_) | Self::Object(_)
| Self::Array(_) => true, | Self::Array(_) => true,
Self::Component(c) => c.root_element.borrow().base_type == Type::Void, Self::Component(c) => c.root_element.borrow().base_type == Type::Void,
@ -468,6 +473,7 @@ pub fn reserved_property(name: &str) -> Type {
("row", Type::Int32), ("row", Type::Int32),
("colspan", Type::Int32), ("colspan", Type::Int32),
("rowspan", Type::Int32), ("rowspan", Type::Int32),
("initial_focus", Type::ElementReference),
] ]
.iter() .iter()
{ {

View file

@ -191,7 +191,9 @@ impl ComponentWindow {
self.0.clone().process_key_input(event, component) self.0.clone().process_key_input(event, component)
} }
pub(crate) fn set_focus_item( /// Clears the focus on any previously focused item and makes the provided
/// item the focus item, in order to receive future key events.
pub fn set_focus_item(
&self, &self,
component: core::pin::Pin<crate::component::ComponentRef>, component: core::pin::Pin<crate::component::ComponentRef>,
item: Pin<VRef<crate::items::ItemVTable>>, item: Pin<VRef<crate::items::ItemVTable>>,
@ -601,4 +603,15 @@ pub mod ffi {
let window = &*(handle as *const ComponentWindow); let window = &*(handle as *const ComponentWindow);
window.free_graphics_resources(component) window.free_graphics_resources(component)
} }
/// Sets the focus item.
#[no_mangle]
pub unsafe extern "C" fn sixtyfps_component_window_set_focus_item(
handle: *const ComponentWindowOpaque,
component: Pin<VRef<ComponentVTable>>,
item: Pin<VRef<ItemVTable>>,
) {
let window = &*(handle as *const ComponentWindow);
window.set_focus_item(component, item)
}
} }

View file

@ -840,6 +840,10 @@ pub fn instantiate<'id>(
} }
} }
for extra_init_code in component_type.original.setup_code.borrow().iter() {
eval::eval_expression(extra_init_code, instance_ref, &mut Default::default());
}
component_box component_box
} }

View file

@ -198,6 +198,7 @@ pub fn eval_expression(
Expression::BuiltinFunctionReference(_) => panic!( Expression::BuiltinFunctionReference(_) => panic!(
"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::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()
} }
@ -241,9 +242,9 @@ pub fn eval_expression(
} }
v v
} }
Expression::FunctionCall { function, arguments } => { Expression::FunctionCall { function, arguments } => match &**function {
Expression::SignalReference(NamedReference { element, name }) => {
let a = arguments.iter().map(|e| eval_expression(e, component, local_context)); let a = arguments.iter().map(|e| eval_expression(e, component, local_context));
if let Expression::SignalReference(NamedReference { element, name }) = &**function {
let element = element.upgrade().unwrap(); let element = element.upgrade().unwrap();
generativity::make_guard!(guard); generativity::make_guard!(guard);
let enclosing_component = let enclosing_component =
@ -266,20 +267,45 @@ pub fn eval_expression(
} }
Value::Void Value::Void
} else if let Expression::BuiltinFunctionReference(funcref) = &**function { }
match funcref { Expression::BuiltinFunctionReference(BuiltinFunction::GetWindowScaleFactor) => {
BuiltinFunction::GetWindowScaleFactor => {
Value::Number(window_ref(component).unwrap().scale_factor() as _) Value::Number(window_ref(component).unwrap().scale_factor() as _)
} }
BuiltinFunction::Debug => { Expression::BuiltinFunctionReference(BuiltinFunction::Debug) => {
let a = arguments.iter().map(|e| eval_expression(e, component, local_context));
println!("{:?}", a); println!("{:?}", a);
Value::Void Value::Void
} }
Expression::BuiltinFunctionReference(BuiltinFunction::SetFocusItem) => {
if arguments.len() != 1 {
panic!("internal error: incorrect argument count to SetFocusItem")
} }
if let Expression::ElementReference(focus_item) = &arguments[0] {
generativity::make_guard!(guard);
let component_ref: Pin<vtable::VRef<corelib::component::ComponentVTable>> = unsafe {
Pin::new_unchecked(vtable::VRef::from_raw(
core::ptr::NonNull::from(&component.component_type.ct).cast(),
core::ptr::NonNull::from(&*component.as_ptr()),
))
};
let focus_item = focus_item.upgrade().unwrap();
let enclosing_component =
enclosing_component_for_element(&focus_item, component, guard);
let component_type = enclosing_component.component_type;
let item_info = &component_type.items[focus_item.borrow().id.as_str()];
let item =
unsafe { item_info.item_from_component(enclosing_component.as_ptr()) };
window_ref(component).unwrap().set_focus_item(component_ref, item);
Value::Void
} else { } else {
panic!("call of something not a signal") panic!("internal error: argument to SetFocusItem must be an element")
} }
} }
_ => panic!("call of something not a signal"),
}
Expression::SelfAssignment { lhs, rhs, op } => { Expression::SelfAssignment { lhs, rhs, op } => {
let rhs = eval_expression(&**rhs, component, local_context); let rhs = eval_expression(&**rhs, component, local_context);
eval_assignement(lhs, *op, rhs, component, local_context); eval_assignement(lhs, *op, rhs, component, local_context);

View file

@ -0,0 +1,90 @@
/* 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;
initial_focus: input1;
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());
sixtyfps::testing::send_keyboard_string_sequence(instance, "Only for field 1");
assert_eq!(instance.get_input1_text(), "Only for field 1");
assert_eq!(instance.get_input2_text(), "");
sixtyfps::testing::send_mouse_click(instance, 150., 100.);
assert!(instance.get_input1_focused());
assert!(!instance.get_input2_focused());
sixtyfps::testing::send_mouse_click(instance, 150., 300.);
assert!(!instance.get_input1_focused());
assert!(instance.get_input2_focused());
```
```cpp
TestCase instance;
assert(instance.get_input1_focused());
assert(!instance.get_input2_focused());
sixtyfps::testing::send_keyboard_string_sequence(instance, "Only for field 1");
assert_eq(instance.get_input1_text(), "Only for field 1");
assert_eq(instance.get_input2_text(), "");
sixtyfps::testing::send_mouse_click(instance, 150., 100.);
assert(instance.get_input1_focused());
assert(!instance.get_input2_focused());
sixtyfps::testing::send_mouse_click(instance, 150., 300.);
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.send_keyboard_string_sequence("Only for field 1");
assert.equal(instance.input1_text, "Only for field 1");
assert.equal(instance.input2_text, "");
instance.send_mouse_click(150., 100.);
assert(instance.input1_focused);
assert(!instance.input2_focused);
instance.send_mouse_click(150., 300.);
assert(!instance.input1_focused);
assert(instance.input2_focused);
```
*/

View file

@ -0,0 +1,53 @@
/* 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 */
TextComponent := Rectangle {
property<bool> has_focus: my_text_input.has_focus;
initial_focus: my_text_input;
my_text_input := TextInput { }
}
TestCase := Rectangle {
width: 400px;
height: 400px;
initial_focus: input2;
input1 := TextComponent {
}
input2 := TextComponent {
}
property<bool> input1_focused: input1.has_focus;
property<bool> input2_focused: input2.has_focus;
}
/*
```rust
let instance = TestCase::new();
let instance = instance.as_ref();
assert!(!instance.get_input1_focused());
assert!(instance.get_input2_focused());
```
```cpp
TestCase instance;
assert(!instance.get_input1_focused());
assert(instance.get_input2_focused());
```
```js
var instance = new sixtyfps.TestCase();
assert(!instance.input1_focused);
assert(instance.input2_focused);
```
*/

View file

@ -0,0 +1,55 @@
/* 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 */
TextComponent := GridLayout {
property<bool> has_focus: my_text_input.has_focus;
initial_focus: my_text_input;
Row {
my_text_input := TextInput { }
}
}
TestCase := Rectangle {
width: 400px;
height: 400px;
initial_focus: input2;
input1 := TextComponent {
}
input2 := TextComponent {
}
property<bool> input1_focused: input1.has_focus;
property<bool> input2_focused: input2.has_focus;
}
/*
```rust
let instance = TestCase::new();
let instance = instance.as_ref();
assert!(!instance.get_input1_focused());
assert!(instance.get_input2_focused());
```
```cpp
TestCase instance;
assert(!instance.get_input1_focused());
assert(instance.get_input2_focused());
```
```js
var instance = new sixtyfps.TestCase();
assert(!instance.input1_focused);
assert(instance.input2_focused);
```
*/

View file

@ -143,6 +143,7 @@ fn gen_corelib(include_dir: &Path) -> anyhow::Result<()> {
"sixtyfps_component_window_get_scale_factor", "sixtyfps_component_window_get_scale_factor",
"sixtyfps_component_window_set_scale_factor", "sixtyfps_component_window_set_scale_factor",
"sixtyfps_component_window_free_graphics_resources", "sixtyfps_component_window_free_graphics_resources",
"sixtyfps_component_window_set_focus_item",
"sixtyfps_new_path_elements", "sixtyfps_new_path_elements",
"sixtyfps_new_path_events", "sixtyfps_new_path_events",
] ]