diff --git a/api/sixtyfps-cpp/include/sixtyfps_testing.h b/api/sixtyfps-cpp/include/sixtyfps_testing.h index ce82d9655..a01ad1530 100644 --- a/api/sixtyfps-cpp/include/sixtyfps_testing.h +++ b/api/sixtyfps-cpp/include/sixtyfps_testing.h @@ -25,10 +25,11 @@ inline void send_mouse_click(const ComponentHandle *component, float } template -inline void send_keyboard_string_sequence(const Component &component, - const sixtyfps::SharedString &str) +inline void send_keyboard_string_sequence(const Component *component, + const sixtyfps::SharedString &str, + KeyboardModifiers modifiers = {}) { - cbindgen_private::send_keyboard_string_sequence(&str, {}, &component.window); + cbindgen_private::send_keyboard_string_sequence(&str, modifiers, &component->window); } #define assert_eq(A, B) \ diff --git a/sixtyfps_runtime/corelib/input.rs b/sixtyfps_runtime/corelib/input.rs index 83eea8848..56b179250 100644 --- a/sixtyfps_runtime/corelib/input.rs +++ b/sixtyfps_runtime/corelib/input.rs @@ -194,6 +194,7 @@ pub struct KeyEvent { /// Represents how an item's key_event handler dealt with a key event. /// An accepted event results in no further event propagation. #[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum KeyEventResult { /// The event was handled. EventAccepted, @@ -203,7 +204,7 @@ pub enum KeyEventResult { /// This event is sent to a component and items when they receive or loose /// the keyboard focus. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] #[repr(C)] pub enum FocusEvent { /// This event is sent when an item receives the focus. diff --git a/sixtyfps_runtime/corelib/items.rs b/sixtyfps_runtime/corelib/items.rs index b802671fa..6e6d4d73e 100644 --- a/sixtyfps_runtime/corelib/items.rs +++ b/sixtyfps_runtime/corelib/items.rs @@ -128,6 +128,10 @@ impl ItemRc { pub fn downgrade(&self) -> ItemWeak { ItemWeak { component: VRc::downgrade(&self.component), index: self.index } } + pub fn parent_item(&self) -> ItemWeak { + let comp_ref_pin = vtable::VRc::borrow_pin(&self.component); + comp_ref_pin.as_ref().parent_item(self.index) + } } /// A Weak reference to an item that can be constructed from an ItemRc. diff --git a/sixtyfps_runtime/corelib/items/text.rs b/sixtyfps_runtime/corelib/items/text.rs index 60fe14885..42eb584c8 100644 --- a/sixtyfps_runtime/corelib/items/text.rs +++ b/sixtyfps_runtime/corelib/items/text.rs @@ -270,7 +270,7 @@ impl Item for TextInput { } match event.event_type { - KeyEventType::KeyPressed => { + KeyEventType::KeyPressed => { if let Some(keycode) = InternalKeyCode::try_decode_from_string(&event.text) { if let Ok(text_cursor_movement) = TextCursorDirection::try_from(keycode.clone()) { @@ -292,12 +292,11 @@ impl Item for TextInput { return KeyEventResult::EventAccepted; } } - KeyEventResult::EventIgnored - } - KeyEventType::KeyReleased + // Only insert/interpreter non-control character strings - if !event.text.is_empty() && event.text.as_str().chars().all(|ch| !ch.is_control()) => - { + if event.text.is_empty() || event.text.as_str().chars().any(|ch| ch.is_control()) { + return KeyEventResult::EventIgnored; + } if event.modifiers.control { if event.text == "c" { self.copy(); @@ -306,6 +305,7 @@ impl Item for TextInput { self.paste(); return KeyEventResult::EventAccepted; } + return KeyEventResult::EventIgnored; } self.delete_selection(); diff --git a/sixtyfps_runtime/corelib/window.rs b/sixtyfps_runtime/corelib/window.rs index 1a800976c..4871e7f77 100644 --- a/sixtyfps_runtime/corelib/window.rs +++ b/sixtyfps_runtime/corelib/window.rs @@ -135,9 +135,15 @@ impl Window { /// * `event`: The key event received by the windowing system. /// * `component`: The SixtyFPS compiled component that provides the tree of items. pub fn process_key_input(self: Rc, event: &KeyEvent) { - if let Some(focus_item) = self.focus_item.borrow().upgrade() { + let mut item = self.focus_item.borrow().clone(); + while let Some(focus_item) = item.upgrade() { let window = &ComponentWindow::new(self.clone()); - focus_item.borrow().as_ref().key_event(event, &window); + if focus_item.borrow().as_ref().key_event(event, &window) + == crate::input::KeyEventResult::EventAccepted + { + return; + } + item = focus_item.parent_item(); } } diff --git a/tests/cases/focus/event_propagation.60 b/tests/cases/focus/event_propagation.60 new file mode 100644 index 000000000..7b3d16700 --- /dev/null +++ b/tests/cases/focus/event_propagation.60 @@ -0,0 +1,106 @@ +/* LICENSE BEGIN + This file is part of the SixtyFPS Project -- https://sixtyfps.io + Copyright (c) 2020 Olivier Goffart + Copyright (c) 2020 Simon Hausmann + + 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: 400phx; + height: 400phx; + forward-focus: input2; + + input1 := TextInput { + width: parent.width; + height: 200phx; + Rectangle { + FocusScope { + width: 75%; + key-pressed(event) => { + recieved += event.text + } + + input2 := TextInput { + width: 75%; + height: 100%; + } + } + } + } + + property input1_focused: input1.has_focus; + property input1_text: input1.text; + property input2_focused: input2.has_focus; + property input2_text: input2.text; + property recieved; +} + +/* +```rust +let ctrl_modifier = sixtyfps::re_exports::KeyboardModifiers { + control: true, + ..Default::default() +}; + +let instance = TestCase::new(); + +assert!(!instance.get_input1_focused()); +assert!(instance.get_input2_focused()); + +sixtyfps::testing::send_keyboard_string_sequence(&instance, "Hello"); +assert_eq!(instance.get_input2_text(), "Hello"); +assert_eq!(instance.get_input1_text(), ""); +assert_eq!(instance.get_recieved(), ""); + +sixtyfps::testing::set_current_keyboard_modifiers(&instance, ctrl_modifier); +sixtyfps::testing::send_keyboard_string_sequence(&instance, "ß"); +assert_eq!(instance.get_input2_text(), "Hello"); +assert_eq!(instance.get_input1_text(), ""); +assert_eq!(instance.get_recieved(), "ß"); +``` + +```cpp +sixtyfps::cbindgen_private::KeyboardModifiers ctrl_modifier{}; +ctrl_modifier.control = true; + +auto handle = TestCase::create(); +const TestCase &instance = *handle; + +assert(!instance.get_input1_focused()); +assert(instance.get_input2_focused()); + +sixtyfps::testing::send_keyboard_string_sequence(&instance, "Hello"); +assert_eq(instance.get_input2_text(), "Hello"); +assert_eq(instance.get_input1_text(), ""); +assert_eq(instance.get_recieved(), ""); + +sixtyfps::testing::send_keyboard_string_sequence(&instance, "ß", ctrl_modifier); +assert_eq(instance.get_input2_text(), "Hello"); +assert_eq(instance.get_input1_text(), ""); +assert_eq(instance.get_recieved(), "ß"); +``` + +```j*s +var instance = new sixtyfps.TestCase(); +assert(!instance.input1_focused); +assert(!instance.input2_focused); + +instance.send_mouse_click(150., 100.); +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., 300.); +assert(!instance.input1_focused); +assert(instance.input2_focused); + +instance.send_keyboard_string_sequence("Only for field 2"); +assert.equal(instance.input1_text, "Only for field 1"); +assert.equal(instance.input2_text, "Only for field 2"); +``` +*/ diff --git a/tests/cases/focus/focus_change.60 b/tests/cases/focus/focus_change.60 index 786fca8e1..aac306106 100644 --- a/tests/cases/focus/focus_change.60 +++ b/tests/cases/focus/focus_change.60 @@ -61,7 +61,7 @@ sixtyfps::testing::send_mouse_click(&handle, 150., 100.); assert(instance.get_input1_focused()); assert(!instance.get_input2_focused()); -sixtyfps::testing::send_keyboard_string_sequence(instance, "Only for field 1"); +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(), ""); @@ -69,7 +69,7 @@ sixtyfps::testing::send_mouse_click(&handle, 150., 300.); assert(!instance.get_input1_focused()); assert(instance.get_input2_focused()); -sixtyfps::testing::send_keyboard_string_sequence(instance, "Only for field 2"); +sixtyfps::testing::send_keyboard_string_sequence(&instance, "Only for field 2"); assert_eq(instance.get_input1_text(), "Only for field 1"); assert_eq(instance.get_input2_text(), "Only for field 2"); ``` diff --git a/tests/cases/focus/initial_focus.60 b/tests/cases/focus/initial_focus.60 index 3177fc22a..ea38ef7e8 100644 --- a/tests/cases/focus/initial_focus.60 +++ b/tests/cases/focus/initial_focus.60 @@ -54,7 +54,7 @@ const TestCase &instance = *handle; assert(instance.get_input1_focused()); assert(!instance.get_input2_focused()); -sixtyfps::testing::send_keyboard_string_sequence(instance, "Only for field 1"); +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(), "");