From d325ca78a3bd68df5de20e37f8731328089a471a Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Thu, 28 Jan 2021 17:19:02 -0800 Subject: [PATCH] Hardware keyboard: Web, embedder, and dart:ui (#23466) --- ci/licenses_golden/licenses_flutter | 8 + lib/ui/BUILD.gn | 4 + lib/ui/dart_ui.gni | 1 + lib/ui/hooks.dart | 6 + lib/ui/key.dart | 109 ++ lib/ui/platform_dispatcher.dart | 65 +- lib/ui/ui.dart | 2 +- lib/ui/window.dart | 9 + lib/ui/window/key_data.cc | 18 + lib/ui/window/key_data.h | 48 + lib/ui/window/key_data_packet.cc | 26 + lib/ui/window/key_data_packet.h | 46 + lib/ui/window/platform_configuration.cc | 32 + lib/ui/window/platform_configuration.h | 43 +- lib/ui/window/window.cc | 18 + lib/ui/window/window.h | 12 + lib/web_ui/lib/src/engine.dart | 2 + lib/web_ui/lib/src/engine/dom_renderer.dart | 1 + lib/web_ui/lib/src/engine/key_map.dart | 706 +++++++++++++ .../lib/src/engine/keyboard_binding.dart | 481 +++++++++ .../lib/src/engine/platform_dispatcher.dart | 31 +- lib/web_ui/lib/src/ui/key.dart | 108 ++ .../lib/src/ui/platform_dispatcher.dart | 6 +- lib/web_ui/lib/src/ui/window.dart | 5 + lib/web_ui/lib/ui.dart | 1 + lib/web_ui/test/keyboard_converter_test.dart | 969 ++++++++++++++++++ runtime/runtime_controller.cc | 14 + runtime/runtime_controller.h | 14 + shell/common/engine.cc | 8 + shell/common/engine.h | 15 + shell/common/platform_view.cc | 6 + shell/common/platform_view.h | 28 + shell/common/shell.cc | 17 + shell/common/shell.h | 5 + shell/common/shell_unittests.cc | 4 + .../Source/FlutterEnginePlatformViewTest.mm | 2 + .../Source/FlutterPlatformViewsTest.mm | 2 + .../Source/accessibility_bridge_test.mm | 2 + shell/platform/embedder/embedder.cc | 53 + shell/platform/embedder/embedder.h | 92 ++ shell/platform/embedder/embedder_engine.cc | 16 + shell/platform/embedder/embedder_engine.h | 16 + shell/platform/embedder/fixtures/main.dart | 41 + .../embedder/tests/embedder_unittests.cc | 203 ++++ .../fuchsia/flutter/platform_view_unittest.cc | 4 + .../dart/window_hooks_integration_test.dart | 1 + 46 files changed, 3295 insertions(+), 5 deletions(-) create mode 100644 lib/ui/key.dart create mode 100644 lib/ui/window/key_data.cc create mode 100644 lib/ui/window/key_data.h create mode 100644 lib/ui/window/key_data_packet.cc create mode 100644 lib/ui/window/key_data_packet.h create mode 100644 lib/web_ui/lib/src/engine/key_map.dart create mode 100644 lib/web_ui/lib/src/engine/keyboard_binding.dart create mode 100644 lib/web_ui/lib/src/ui/key.dart create mode 100644 lib/web_ui/test/keyboard_converter_test.dart diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 36f6129fc..12d0d31cb 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -319,6 +319,7 @@ FILE: ../../../flutter/lib/ui/isolate_name_server/isolate_name_server.cc FILE: ../../../flutter/lib/ui/isolate_name_server/isolate_name_server.h FILE: ../../../flutter/lib/ui/isolate_name_server/isolate_name_server_natives.cc FILE: ../../../flutter/lib/ui/isolate_name_server/isolate_name_server_natives.h +FILE: ../../../flutter/lib/ui/key.dart FILE: ../../../flutter/lib/ui/lerp.dart FILE: ../../../flutter/lib/ui/natives.dart FILE: ../../../flutter/lib/ui/painting.dart @@ -406,6 +407,10 @@ FILE: ../../../flutter/lib/ui/ui_dart_state.h FILE: ../../../flutter/lib/ui/volatile_path_tracker.cc FILE: ../../../flutter/lib/ui/volatile_path_tracker.h FILE: ../../../flutter/lib/ui/window.dart +FILE: ../../../flutter/lib/ui/window/key_data.cc +FILE: ../../../flutter/lib/ui/window/key_data.h +FILE: ../../../flutter/lib/ui/window/key_data_packet.cc +FILE: ../../../flutter/lib/ui/window/key_data_packet.h FILE: ../../../flutter/lib/ui/window/platform_configuration.cc FILE: ../../../flutter/lib/ui/window/platform_configuration.h FILE: ../../../flutter/lib/ui/window/platform_configuration_unittests.cc @@ -500,7 +505,9 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/surface.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/surface_stats.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/transform.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html_image_codec.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/key_map.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/keyboard.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse_cursor.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/navigation/history.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart @@ -560,6 +567,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/ui/compositing.dart FILE: ../../../flutter/lib/web_ui/lib/src/ui/geometry.dart FILE: ../../../flutter/lib/web_ui/lib/src/ui/hash_codes.dart FILE: ../../../flutter/lib/web_ui/lib/src/ui/initialization.dart +FILE: ../../../flutter/lib/web_ui/lib/src/ui/key.dart FILE: ../../../flutter/lib/web_ui/lib/src/ui/lerp.dart FILE: ../../../flutter/lib/web_ui/lib/src/ui/natives.dart FILE: ../../../flutter/lib/web_ui/lib/src/ui/painting.dart diff --git a/lib/ui/BUILD.gn b/lib/ui/BUILD.gn index 2f9efe9e9..1d2ab90df 100644 --- a/lib/ui/BUILD.gn +++ b/lib/ui/BUILD.gn @@ -93,6 +93,10 @@ source_set("ui") { "ui_dart_state.h", "volatile_path_tracker.cc", "volatile_path_tracker.h", + "window/key_data.cc", + "window/key_data.h", + "window/key_data_packet.cc", + "window/key_data_packet.h", "window/platform_configuration.cc", "window/platform_configuration.h", "window/platform_message.cc", diff --git a/lib/ui/dart_ui.gni b/lib/ui/dart_ui.gni index 11fa74da5..96a7b06fb 100644 --- a/lib/ui/dart_ui.gni +++ b/lib/ui/dart_ui.gni @@ -10,6 +10,7 @@ dart_ui_files = [ "//flutter/lib/ui/hash_codes.dart", "//flutter/lib/ui/hooks.dart", "//flutter/lib/ui/isolate_name_server.dart", + "//flutter/lib/ui/key.dart", "//flutter/lib/ui/lerp.dart", "//flutter/lib/ui/natives.dart", "//flutter/lib/ui/painting.dart", diff --git a/lib/ui/hooks.dart b/lib/ui/hooks.dart index 273f44eda..f3e872588 100644 --- a/lib/ui/hooks.dart +++ b/lib/ui/hooks.dart @@ -96,6 +96,12 @@ void _dispatchPointerDataPacket(ByteData packet) { PlatformDispatcher.instance._dispatchPointerDataPacket(packet); } +@pragma('vm:entry-point') +// ignore: unused_element +void _dispatchKeyData(ByteData packet, int responseId) { + PlatformDispatcher.instance._dispatchKeyData(packet, responseId); +} + @pragma('vm:entry-point') // ignore: unused_element void _dispatchSemanticsAction(int id, int action, ByteData? args) { diff --git a/lib/ui/key.dart b/lib/ui/key.dart new file mode 100644 index 000000000..e6a349d1a --- /dev/null +++ b/lib/ui/key.dart @@ -0,0 +1,109 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.12 + +part of dart.ui; + +/// The type of a key event. +// Must match the KeyEventType enum in ui/window/key_data.h. +enum KeyEventType { + /// The key is pressed. + down, + + /// The key is released. + up, + + /// The key is held, causing a repeated key input. + repeat, +} + +/// Information about a key event. +class KeyData { + /// Creates an object that represents a key event. + const KeyData({ + required this.timeStamp, + required this.type, + required this.physical, + required this.logical, + required this.character, + required this.synthesized, + }); + + /// Time of event dispatch, relative to an arbitrary timeline. + /// + /// For synthesized events, the [timeStamp] might not be the actual time that + /// the key press or release happens. + final Duration timeStamp; + + /// The type of the event. + final KeyEventType type; + + /// The key code for the physical key that has changed. + final int physical; + + /// The key code for the logical key that has changed. + final int logical; + + /// Character input from the event. + /// + /// Ignored for up events. + final String? character; + + /// If [synthesized] is true, this event does not correspond to a native event. + /// + /// Although most of Flutter's keyboard events are transformed from native + /// events, some events are not based on native events, and are synthesized + /// only to conform Flutter's key event model (as documented in + /// the `HardwareKeyboard` class in the framework). + /// + /// For example, some key downs or ups might be lost when the window loses + /// focus. Some platforms provides ways to query whether a key is being held. + /// If the embedder detects an inconsistancy between its internal record and + /// the state returned by the system, the embedder will synthesize a + /// corresponding event to synchronize the state without breaking the event + /// model. + /// + /// As another example, macOS treats CapsLock in a special way by sending + /// down and up events at the down of alterate presses to indicate the + /// direction in which the lock is toggled instead of that the physical key is + /// going. A macOS embedder should normalize the behavior by converting a + /// native down event into a down event followed immediately by a synthesized + /// up event, and the native up event also into a down event followed + /// immediately by a synthesized up event. + /// + /// Synthesized events do not have a trustworthy [timeStamp], and should not be + /// processed as if the key actually went down or up at the time of the + /// callback. + /// + /// [KeyRepeatEvent] is never synthesized. + final bool synthesized; + + @override + String toString() => 'KeyData(type: ${_typeToString(type)}, physical: 0x${physical.toRadixString(16)}, ' + 'logical: 0x${logical.toRadixString(16)}, character: $character)'; + + /// Returns a complete textual description of the information in this object. + String toStringFull() { + return '$runtimeType(' + 'type: ${_typeToString(type)}, ' + 'timeStamp: $timeStamp, ' + 'physical: 0x${physical.toRadixString(16)}, ' + 'logical: 0x${logical.toRadixString(16)}, ' + 'character: $character, ' + 'synthesized: $synthesized' + ')'; + } + + static String _typeToString(KeyEventType type) { + switch (type) { + case KeyEventType.up: + return 'up'; + case KeyEventType.down: + return 'down'; + case KeyEventType.repeat: + return 'repeat'; + } + } +} diff --git a/lib/ui/platform_dispatcher.dart b/lib/ui/platform_dispatcher.dart index dc7223b79..612de4643 100644 --- a/lib/ui/platform_dispatcher.dart +++ b/lib/ui/platform_dispatcher.dart @@ -28,6 +28,12 @@ typedef TimingsCallback = void Function(List timings); /// Signature for [PlatformDispatcher.onPointerDataPacket]. typedef PointerDataPacketCallback = void Function(PointerDataPacket packet); +// Signature for the response to KeyDataCallback. +typedef _KeyDataResponseCallback = void Function(int responseId, bool handled); + +/// Signature for [PlatformDispatcher.onKeyData]. +typedef KeyDataCallback = bool Function(KeyData data); + /// Signature for [PlatformDispatcher.onSemanticsAction]. typedef SemanticsActionCallback = void Function(int id, SemanticsAction action, ByteData? args); @@ -332,6 +338,63 @@ class PlatformDispatcher { return PointerDataPacket(data: data); } + /// Called by [_dispatchKeyData]. + void _respondToKeyData(int responseId, bool handled) + native 'PlatformConfiguration_respondToKeyData'; + + /// A callback that is invoked when key data is available. + /// + /// The framework invokes this callback in the same zone in which the callback + /// was set. + KeyDataCallback? get onKeyData => _onKeyData; + KeyDataCallback? _onKeyData; + Zone _onKeyDataZone = Zone.root; + set onKeyData(KeyDataCallback? callback) { + _onKeyData = callback; + _onKeyDataZone = Zone.current; + } + + // Called from the engine, via hooks.dart + void _dispatchKeyData(ByteData packet, int responseId) { + _invoke2( + (KeyData data, _KeyDataResponseCallback callback) { + callback(responseId, onKeyData == null ? false : onKeyData!(data)); + }, + _onKeyDataZone, + _unpackKeyData(packet), + _respondToKeyData, + ); + } + + // If this value changes, update the encoding code in the following files: + // + // * key_data.h + // * key.dart (ui) + // * key.dart (web_ui) + // * HardwareKeyboard.java + static const int _kKeyDataFieldCount = 5; + + // The packet structure is described in `key_data_packet.h`. + static KeyData _unpackKeyData(ByteData packet) { + const int kStride = Int64List.bytesPerElement; + + int offset = 0; + final int charDataSize = packet.getUint64(kStride * offset++, _kFakeHostEndian); + final String? character = charDataSize == 0 ? null : utf8.decoder.convert( + packet.buffer.asUint8List(kStride * (offset + _kKeyDataFieldCount), charDataSize)); + + final KeyData keyData = KeyData( + timeStamp: Duration(microseconds: packet.getUint64(kStride * offset++, _kFakeHostEndian)), + type: KeyEventType.values[packet.getInt64(kStride * offset++, _kFakeHostEndian)], + physical: packet.getUint64(kStride * offset++, _kFakeHostEndian), + logical: packet.getUint64(kStride * offset++, _kFakeHostEndian), + character: character, + synthesized: packet.getUint64(kStride * offset++, _kFakeHostEndian) != 0, + ); + + return keyData; + } + /// A callback that is invoked to report the [FrameTiming] of recently /// rasterized frames. /// @@ -1547,4 +1610,4 @@ class Locale { out.write('$separator$countryCode'); return out.toString(); } -} \ No newline at end of file +} diff --git a/lib/ui/ui.dart b/lib/ui/ui.dart index ea966c88c..23d85835c 100644 --- a/lib/ui/ui.dart +++ b/lib/ui/ui.dart @@ -12,7 +12,6 @@ // @dart = 2.12 library dart.ui; -import 'dart:_internal' hide Symbol; // ignore: unused_import import 'dart:async'; import 'dart:collection' as collection; import 'dart:convert'; @@ -30,6 +29,7 @@ part 'geometry.dart'; part 'hash_codes.dart'; part 'hooks.dart'; part 'isolate_name_server.dart'; +part 'key.dart'; part 'lerp.dart'; part 'natives.dart'; part 'painting.dart'; diff --git a/lib/ui/window.dart b/lib/ui/window.dart index ac984a2a6..da65c1d62 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -556,6 +556,15 @@ class SingletonFlutterWindow extends FlutterWindow { platformDispatcher.onPointerDataPacket = callback; } + /// A callback that is invoked when key data is available. + /// + /// The framework invokes this callback in the same zone in which the + /// callback was set. + KeyDataCallback? get onKeyData => platformDispatcher.onKeyData; + set onKeyData(KeyDataCallback? callback) { + platformDispatcher.onKeyData = callback; + } + /// The route or path that the embedder requested when the application was /// launched. /// diff --git a/lib/ui/window/key_data.cc b/lib/ui/window/key_data.cc new file mode 100644 index 000000000..2dacd8d7b --- /dev/null +++ b/lib/ui/window/key_data.cc @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/lib/ui/window/key_data.h" + +#include + +namespace flutter { + +static_assert(sizeof(KeyData) == kBytesPerKeyField * kKeyDataFieldCount, + "KeyData has the wrong size"); + +void KeyData::Clear() { + memset(this, 0, sizeof(KeyData)); +} + +} // namespace flutter diff --git a/lib/ui/window/key_data.h b/lib/ui/window/key_data.h new file mode 100644 index 000000000..5c84bf0cb --- /dev/null +++ b/lib/ui/window/key_data.h @@ -0,0 +1,48 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_LIB_UI_WINDOW_KEY_DATA_H_ +#define FLUTTER_LIB_UI_WINDOW_KEY_DATA_H_ + +#include + +namespace flutter { + +// If this value changes, update the key data unpacking code in hooks.dart. +static constexpr int kKeyDataFieldCount = 5; +static constexpr int kBytesPerKeyField = sizeof(int64_t); + +// The change of the key event, used by KeyData. +// +// Must match the KeyEventType enum in ui/key.dart. +enum class KeyEventType : int64_t { + kDown = 0, + kUp, + kRepeat, +}; + +// The fixed-length sections of a KeyDataPacket. +// +// KeyData does not contain `character`, for variable-length data are stored in +// a different way in KeyDataPacket. +// +// This structure is unpacked by hooks.dart. +struct alignas(8) KeyData { + // Timestamp in microseconds from an arbitrary and consistant start point + uint64_t timestamp; + KeyEventType type; + uint64_t physical; + uint64_t logical; + // True if the event does not correspond to a native event. + // + // The value is 1 for true, and 0 for false. + uint64_t synthesized; + + // Sets all contents of `Keydata` to 0. + void Clear(); +}; + +} // namespace flutter + +#endif // FLUTTER_LIB_UI_WINDOW_POINTER_DATA_H_ diff --git a/lib/ui/window/key_data_packet.cc b/lib/ui/window/key_data_packet.cc new file mode 100644 index 000000000..fe24af210 --- /dev/null +++ b/lib/ui/window/key_data_packet.cc @@ -0,0 +1,26 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/lib/ui/window/key_data_packet.h" + +#include + +#include "flutter/fml/logging.h" + +namespace flutter { + +KeyDataPacket::KeyDataPacket(const KeyData& event, const char* character) { + size_t char_size = character == nullptr ? 0 : strlen(character); + uint64_t char_size_64 = char_size; + data_.resize(sizeof(uint64_t) + sizeof(KeyData) + char_size); + memcpy(CharacterSizeStart(), &char_size_64, sizeof(char_size)); + memcpy(KeyDataStart(), &event, sizeof(KeyData)); + if (character != nullptr) { + memcpy(CharacterStart(), character, char_size); + } +} + +KeyDataPacket::~KeyDataPacket() = default; + +} // namespace flutter diff --git a/lib/ui/window/key_data_packet.h b/lib/ui/window/key_data_packet.h new file mode 100644 index 000000000..e8efdf173 --- /dev/null +++ b/lib/ui/window/key_data_packet.h @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_LIB_UI_WINDOW_KEY_DATA_MESSAGE_H_ +#define FLUTTER_LIB_UI_WINDOW_KEY_DATA_MESSAGE_H_ + +#include +#include + +#include "flutter/fml/macros.h" +#include "flutter/lib/ui/window/key_data.h" + +namespace flutter { + +// A byte stream representing a key event, to be sent to the framework. +class KeyDataPacket { + public: + // Build the key data packet by providing information. + // + // The `character` is a nullable C-string that ends with a '\0'. + KeyDataPacket(const KeyData& event, const char* character); + ~KeyDataPacket(); + + // Prevent copying. + KeyDataPacket(KeyDataPacket const&) = delete; + KeyDataPacket& operator=(KeyDataPacket const&) = delete; + + const std::vector& data() const { return data_; } + + private: + // Packet structure: + // | CharDataSize | (1 field) + // | Key Data | (kKeyDataFieldCount fields) + // | CharData | (CharDataSize bits) + + uint8_t* CharacterSizeStart() { return data_.data(); } + uint8_t* KeyDataStart() { return CharacterSizeStart() + sizeof(uint64_t); } + uint8_t* CharacterStart() { return KeyDataStart() + sizeof(KeyData); } + + std::vector data_; +}; + +} // namespace flutter + +#endif // FLUTTER_LIB_UI_WINDOW_POINTER_DATA_MESSAGE_H_ diff --git a/lib/ui/window/platform_configuration.cc b/lib/ui/window/platform_configuration.cc index ccf9cc877..44f389a88 100644 --- a/lib/ui/window/platform_configuration.cc +++ b/lib/ui/window/platform_configuration.cc @@ -181,6 +181,15 @@ void GetPersistentIsolateData(Dart_NativeArguments args) { persistent_isolate_data->GetSize())); } +void RespondToKeyData(Dart_Handle window, int response_id, bool handled) { + UIDartState::Current()->platform_configuration()->CompleteKeyDataResponse( + response_id, handled); +} + +void _RespondToKeyData(Dart_NativeArguments args) { + tonic::DartCallStatic(&RespondToKeyData, args); +} + Dart_Handle ToByteData(const std::vector& buffer) { return tonic::DartByteData::Create(buffer.data(), buffer.size()); } @@ -349,6 +358,13 @@ void PlatformConfiguration::DispatchSemanticsAction(int32_t id, args_handle})); } +uint64_t PlatformConfiguration::RegisterKeyDataResponse( + KeyDataResponse callback) { + uint64_t response_id = next_key_response_id_++; + pending_key_responses_[response_id] = std::move(callback); + return response_id; +} + void PlatformConfiguration::BeginFrame(fml::TimePoint frameTime) { std::shared_ptr dart_state = begin_frame_.dart_state().lock(); @@ -424,6 +440,21 @@ void PlatformConfiguration::CompletePlatformMessageResponse( response->Complete(std::make_unique(std::move(data))); } +void PlatformConfiguration::CompleteKeyDataResponse(uint64_t response_id, + bool handled) { + if (response_id == 0) { + return; + } + auto it = pending_key_responses_.find(response_id); + FML_DCHECK(it != pending_key_responses_.end()); + if (it == pending_key_responses_.end()) { + return; + } + KeyDataResponse callback = std::move(it->second); + pending_key_responses_.erase(it); + callback(handled); +} + Dart_Handle ComputePlatformResolvedLocale(Dart_Handle supportedLocalesHandle) { std::vector supportedLocales = tonic::DartConverter>::FromDart( @@ -454,6 +485,7 @@ void PlatformConfiguration::RegisterNatives( true}, {"PlatformConfiguration_respondToPlatformMessage", _RespondToPlatformMessage, 3, true}, + {"PlatformConfiguration_respondToKeyData", _RespondToKeyData, 3, true}, {"PlatformConfiguration_render", Render, 3, true}, {"PlatformConfiguration_updateSemantics", UpdateSemantics, 2, true}, {"PlatformConfiguration_setIsolateDebugName", SetIsolateDebugName, 2, diff --git a/lib/ui/window/platform_configuration.h b/lib/ui/window/platform_configuration.h index f61d47494..e6dacbb53 100644 --- a/lib/ui/window/platform_configuration.h +++ b/lib/ui/window/platform_configuration.h @@ -5,6 +5,7 @@ #ifndef FLUTTER_LIB_UI_WINDOW_PLATFORM_CONFIGURATION_H_ #define FLUTTER_LIB_UI_WINDOW_PLATFORM_CONFIGURATION_H_ +#include #include #include #include @@ -22,6 +23,8 @@ class FontCollection; class PlatformMessage; class Scene; +typedef std::function KeyDataResponse; + //-------------------------------------------------------------------------- /// @brief An enum for defining the different kinds of accessibility features /// that can be enabled by the platform. @@ -323,6 +326,24 @@ class PlatformConfiguration final { SemanticsAction action, std::vector args); + //---------------------------------------------------------------------------- + /// @brief Registers a callback to be invoked when the framework has + /// decided whether to handle an event. This callback originates + /// in the platform view and has been forwarded through the engine + /// to here. + /// + /// This method will move and store the `callback`, associate it + /// with a self-incrementing identifier, the response ID, then + /// return the ID, which is typically used by + /// Window::DispatchKeyDataPacket. + /// + /// @param[in] callback The callback to be registered. + /// + /// @return The response ID to be associated with the callback. Using this + /// ID in CompleteKeyDataResponse will invoke the callback. + /// + uint64_t RegisterKeyDataResponse(KeyDataResponse callback); + //---------------------------------------------------------------------------- /// @brief Notifies the framework that it is time to begin working on a /// new frame previously scheduled via a call to @@ -411,6 +432,21 @@ class PlatformConfiguration final { /// void CompletePlatformMessageEmptyResponse(int response_id); + //---------------------------------------------------------------------------- + /// @brief Responds to a previously registered key data message from the + /// framework to the engine. + /// + /// For each response_id, this method should be called exactly + /// once. Responding to a response_id that has not been registered + /// or has been invoked will lead to a fatal error. + /// + /// @param[in] response_id The unique id that identifies the original platform + /// message to respond to, created by + /// RegisterKeyDataResponse. + /// @param[in] handled Whether the key data is handled. + /// + void CompleteKeyDataResponse(uint64_t response_id, bool handled); + private: PlatformConfigurationClient* client_; tonic::DartPersistentValue update_locales_; @@ -419,6 +455,7 @@ class PlatformConfiguration final { tonic::DartPersistentValue update_semantics_enabled_; tonic::DartPersistentValue update_accessibility_features_; tonic::DartPersistentValue dispatch_platform_message_; + tonic::DartPersistentValue dispatch_key_message_; tonic::DartPersistentValue dispatch_semantics_action_; tonic::DartPersistentValue begin_frame_; tonic::DartPersistentValue draw_frame_; @@ -426,10 +463,14 @@ class PlatformConfiguration final { std::unordered_map> windows_; - // We use id 0 to mean that no response is expected. + // ID starts at 1 because an ID of 0 indicates that no response is expected. int next_response_id_ = 1; std::unordered_map> pending_responses_; + + // ID starts at 1 because an ID of 0 indicates that no response is expected. + uint64_t next_key_response_id_ = 1; + std::unordered_map pending_key_responses_; }; } // namespace flutter diff --git a/lib/ui/window/window.cc b/lib/ui/window/window.cc index 082df1b82..994eca2ab 100644 --- a/lib/ui/window/window.cc +++ b/lib/ui/window/window.cc @@ -36,6 +36,24 @@ void Window::DispatchPointerDataPacket(const PointerDataPacket& packet) { library_.value(), "_dispatchPointerDataPacket", {data_handle})); } +void Window::DispatchKeyDataPacket(const KeyDataPacket& packet, + uint64_t response_id) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) + return; + tonic::DartState::Scope scope(dart_state); + + const std::vector& buffer = packet.data(); + Dart_Handle data_handle = + tonic::DartByteData::Create(buffer.data(), buffer.size()); + if (Dart_IsError(data_handle)) { + return; + } + tonic::LogIfError( + tonic::DartInvokeField(library_.value(), "_dispatchKeyData", + {data_handle, tonic::ToDart(response_id)})); +} + void Window::UpdateWindowMetrics(const ViewportMetrics& metrics) { viewport_metrics_ = metrics; diff --git a/lib/ui/window/window.h b/lib/ui/window/window.h index b6fa2555b..9a6847d3c 100644 --- a/lib/ui/window/window.h +++ b/lib/ui/window/window.h @@ -5,10 +5,12 @@ #ifndef FLUTTER_LIB_UI_WINDOW_WINDOW_H_ #define FLUTTER_LIB_UI_WINDOW_WINDOW_H_ +#include #include #include #include +#include "flutter/lib/ui/window/key_data_packet.h" #include "flutter/lib/ui/window/platform_message.h" #include "flutter/lib/ui/window/pointer_data_packet.h" #include "flutter/lib/ui/window/viewport_metrics.h" @@ -26,7 +28,17 @@ class Window final { const ViewportMetrics& viewport_metrics() const { return viewport_metrics_; } + // Dispatch a packet to the framework that indicates one or a few pointer + // events. void DispatchPointerDataPacket(const PointerDataPacket& packet); + // Dispatch a packet to the framework that indicates a key event. + // + // The `response_id` is used to label the response of whether the key event + // is handled by the framework, typically the return value of + // PlatformConfiguration::RegisterKeyDataResponse. + // It should be used later in + // PlatformConfiguration::CompleteKeyDataResponse. + void DispatchKeyDataPacket(const KeyDataPacket& packet, uint64_t response_id); void UpdateWindowMetrics(const ViewportMetrics& metrics); private: diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 726e3b31d..926394bd5 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -98,7 +98,9 @@ part 'engine/html/surface.dart'; part 'engine/html/surface_stats.dart'; part 'engine/html/transform.dart'; part 'engine/html_image_codec.dart'; +part 'engine/keyboard_binding.dart'; part 'engine/keyboard.dart'; +part 'engine/key_map.dart'; part 'engine/mouse_cursor.dart'; part 'engine/onscreen_logging.dart'; part 'engine/picture.dart'; diff --git a/lib/web_ui/lib/src/engine/dom_renderer.dart b/lib/web_ui/lib/src/engine/dom_renderer.dart index c3b90a881..d87c8d685 100644 --- a/lib/web_ui/lib/src/engine/dom_renderer.dart +++ b/lib/web_ui/lib/src/engine/dom_renderer.dart @@ -444,6 +444,7 @@ flt-glass-pane * { glassPaneElement.insertBefore(_accesibilityPlaceholder, _sceneHostElement); PointerBinding.initInstance(glassPaneElement); + KeyboardBinding.initInstance(glassPaneElement); // Hide the DOM nodes used to render the scene from accessibility, because // the accessibility tree is built from the SemanticsNode tree as a parallel diff --git a/lib/web_ui/lib/src/engine/key_map.dart b/lib/web_ui/lib/src/engine/key_map.dart new file mode 100644 index 000000000..03ef664c1 --- /dev/null +++ b/lib/web_ui/lib/src/engine/key_map.dart @@ -0,0 +1,706 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// DO NOT EDIT -- DO NOT EDIT -- DO NOT EDIT +// This file is generated by dev/tools/gen_keycodes/bin/gen_keycodes.dart and +// should not be edited directly. +// +// Edit the template dev/tools/gen_keycodes/data/web_key_map_dart.tmpl instead. +// See dev/tools/gen_keycodes/README.md for more information. + +// @dart = 2.12 +part of engine; + +/// Maps Web KeyboardEvent codes to the matching LogicalKeyboardKey id. +const Map kWebToLogicalKey = { + 'None': 0x0000000000, + 'Unidentified': 0x0000000001, + 'Backspace': 0x0000000008, + 'Tab': 0x0000000009, + 'Enter': 0x000000000d, + 'Escape': 0x000000001b, + 'Space': 0x0000000020, + 'Exclamation': 0x0000000021, + 'Quote': 0x0000000022, + 'NumberSign': 0x0000000023, + 'Dollar': 0x0000000024, + 'Ampersand': 0x0000000026, + 'QuoteSingle': 0x0000000027, + 'ParenthesisLeft': 0x0000000028, + 'ParenthesisRight': 0x0000000029, + 'Asterisk': 0x000000002a, + 'Add': 0x000000002b, + 'Comma': 0x000000002c, + 'Minus': 0x000000002d, + 'Period': 0x000000002e, + 'Slash': 0x000000002f, + 'Digit0': 0x0000000030, + 'Digit1': 0x0000000031, + 'Digit2': 0x0000000032, + 'Digit3': 0x0000000033, + 'Digit4': 0x0000000034, + 'Digit5': 0x0000000035, + 'Digit6': 0x0000000036, + 'Digit7': 0x0000000037, + 'Digit8': 0x0000000038, + 'Digit9': 0x0000000039, + 'Colon': 0x000000003a, + 'Semicolon': 0x000000003b, + 'Less': 0x000000003c, + 'Equal': 0x000000003d, + 'Greater': 0x000000003e, + 'Question': 0x000000003f, + 'At': 0x0000000040, + 'BracketLeft': 0x000000005b, + 'Backslash': 0x000000005c, + 'BracketRight': 0x000000005d, + 'Caret': 0x000000005e, + 'Underscore': 0x000000005f, + 'Backquote': 0x0000000060, + 'KeyA': 0x0000000061, + 'KeyB': 0x0000000062, + 'KeyC': 0x0000000063, + 'KeyD': 0x0000000064, + 'KeyE': 0x0000000065, + 'KeyF': 0x0000000066, + 'KeyG': 0x0000000067, + 'KeyH': 0x0000000068, + 'KeyI': 0x0000000069, + 'KeyJ': 0x000000006a, + 'KeyK': 0x000000006b, + 'KeyL': 0x000000006c, + 'KeyM': 0x000000006d, + 'KeyN': 0x000000006e, + 'KeyO': 0x000000006f, + 'KeyP': 0x0000000070, + 'KeyQ': 0x0000000071, + 'KeyR': 0x0000000072, + 'KeyS': 0x0000000073, + 'KeyT': 0x0000000074, + 'KeyU': 0x0000000075, + 'KeyV': 0x0000000076, + 'KeyW': 0x0000000077, + 'KeyX': 0x0000000078, + 'KeyY': 0x0000000079, + 'KeyZ': 0x000000007a, + 'BraceLeft': 0x000000007b, + 'Bar': 0x000000007c, + 'BraceRight': 0x000000007d, + 'Tilde': 0x000000007e, + 'Delete': 0x000000007f, + 'Accel': 0x0000000101, + 'AltGraph': 0x0000000103, + 'CapsLock': 0x0000000104, + 'Fn': 0x0000000106, + 'FnLock': 0x0000000107, + 'Hyper': 0x0000000108, + 'NumLock': 0x000000010a, + 'ScrollLock': 0x000000010c, + 'Super': 0x000000010e, + 'Symbol': 0x000000010f, + 'SymbolLock': 0x0000000110, + 'ShiftLevel5': 0x0000000111, + 'AltGraphLatch': 0x0000000112, + 'ArrowDown': 0x0000000301, + 'ArrowLeft': 0x0000000302, + 'ArrowRight': 0x0000000303, + 'ArrowUp': 0x0000000304, + 'End': 0x0000000305, + 'Home': 0x0000000306, + 'PageDown': 0x0000000307, + 'PageUp': 0x0000000308, + 'Clear': 0x0000000401, + 'Copy': 0x0000000402, + 'CrSel': 0x0000000403, + 'Cut': 0x0000000404, + 'EraseEof': 0x0000000405, + 'ExSel': 0x0000000406, + 'Insert': 0x0000000407, + 'Paste': 0x0000000408, + 'Redo': 0x0000000409, + 'Undo': 0x000000040a, + 'Accept': 0x0000000501, + 'Again': 0x0000000502, + 'Attn': 0x0000000503, + 'Cancel': 0x0000000504, + 'ContextMenu': 0x0000000505, + 'Execute': 0x0000000506, + 'Find': 0x0000000507, + 'Help': 0x0000000508, + 'Pause': 0x0000000509, + 'Play': 0x000000050a, + 'Props': 0x000000050b, + 'Select': 0x000000050c, + 'ZoomIn': 0x000000050d, + 'ZoomOut': 0x000000050e, + 'BrightnessDown': 0x0000000601, + 'BrightnessUp': 0x0000000602, + 'Camera': 0x0000000603, + 'Eject': 0x0000000604, + 'LogOff': 0x0000000605, + 'Power': 0x0000000606, + 'PowerOff': 0x0000000607, + 'PrintScreen': 0x0000000608, + 'Hibernate': 0x0000000609, + 'Standby': 0x000000060a, + 'WakeUp': 0x000000060b, + 'AllCandidates': 0x0000000701, + 'Alphanumeric': 0x0000000702, + 'CodeInput': 0x0000000703, + 'Compose': 0x0000000704, + 'Convert': 0x0000000705, + 'FinalMode': 0x0000000706, + 'GroupFirst': 0x0000000707, + 'GroupLast': 0x0000000708, + 'GroupNext': 0x0000000709, + 'GroupPrevious': 0x000000070a, + 'ModeChange': 0x000000070b, + 'NextCandidate': 0x000000070c, + 'NonConvert': 0x000000070d, + 'PreviousCandidate': 0x000000070e, + 'Process': 0x000000070f, + 'SingleCandidate': 0x0000000710, + 'HangulMode': 0x0000000711, + 'HanjaMode': 0x0000000712, + 'JunjaMode': 0x0000000713, + 'Eisu': 0x0000000714, + 'Hankaku': 0x0000000715, + 'Hiragana': 0x0000000716, + 'HiraganaKatakana': 0x0000000717, + 'KanaMode': 0x0000000718, + 'KanjiMode': 0x0000000719, + 'Katakana': 0x000000071a, + 'Romaji': 0x000000071b, + 'Zenkaku': 0x000000071c, + 'ZenkakuHankaku': 0x000000071d, + 'F1': 0x0000000801, + 'F2': 0x0000000802, + 'F3': 0x0000000803, + 'F4': 0x0000000804, + 'F5': 0x0000000805, + 'F6': 0x0000000806, + 'F7': 0x0000000807, + 'F8': 0x0000000808, + 'F9': 0x0000000809, + 'F10': 0x000000080a, + 'F11': 0x000000080b, + 'F12': 0x000000080c, + 'F13': 0x000000080d, + 'F14': 0x000000080e, + 'F15': 0x000000080f, + 'F16': 0x0000000810, + 'F17': 0x0000000811, + 'F18': 0x0000000812, + 'F19': 0x0000000813, + 'F20': 0x0000000814, + 'F21': 0x0000000815, + 'F22': 0x0000000816, + 'F23': 0x0000000817, + 'F24': 0x0000000818, + 'Soft1': 0x0000000901, + 'Soft2': 0x0000000902, + 'Soft3': 0x0000000903, + 'Soft4': 0x0000000904, + 'Soft5': 0x0000000905, + 'Soft6': 0x0000000906, + 'Soft7': 0x0000000907, + 'Soft8': 0x0000000908, + 'Close': 0x0000000a01, + 'MailForward': 0x0000000a02, + 'MailReply': 0x0000000a03, + 'MailSend': 0x0000000a04, + 'MediaPlayPause': 0x0000000a05, + 'MediaStop': 0x0000000a07, + 'MediaTrackNext': 0x0000000a08, + 'MediaTrackPrevious': 0x0000000a09, + 'New': 0x0000000a0a, + 'Open': 0x0000000a0b, + 'Print': 0x0000000a0c, + 'Save': 0x0000000a0d, + 'SpellCheck': 0x0000000a0e, + 'AudioVolumeDown': 0x0000000a0f, + 'AudioVolumeUp': 0x0000000a10, + 'AudioVolumeMute': 0x0000000a11, + 'LaunchApplication2': 0x0000000b01, + 'LaunchCalendar': 0x0000000b02, + 'LaunchMail': 0x0000000b03, + 'LaunchMediaPlayer': 0x0000000b04, + 'LaunchMusicPlayer': 0x0000000b05, + 'LaunchApplication1': 0x0000000b06, + 'LaunchScreenSaver': 0x0000000b07, + 'LaunchSpreadsheet': 0x0000000b08, + 'LaunchWebBrowser': 0x0000000b09, + 'LaunchWebCam': 0x0000000b0a, + 'LaunchWordProcessor': 0x0000000b0b, + 'LaunchContacts': 0x0000000b0c, + 'LaunchPhone': 0x0000000b0d, + 'LaunchAssistant': 0x0000000b0e, + 'LaunchControlPanel': 0x0000000b0f, + 'BrowserBack': 0x0000000c01, + 'BrowserFavorites': 0x0000000c02, + 'BrowserForward': 0x0000000c03, + 'BrowserHome': 0x0000000c04, + 'BrowserRefresh': 0x0000000c05, + 'BrowserSearch': 0x0000000c06, + 'BrowserStop': 0x0000000c07, + 'AudioBalanceLeft': 0x0000000d01, + 'AudioBalanceRight': 0x0000000d02, + 'AudioBassBoostDown': 0x0000000d03, + 'AudioBassBoostUp': 0x0000000d04, + 'AudioFaderFront': 0x0000000d05, + 'AudioFaderRear': 0x0000000d06, + 'AudioSurroundModeNext': 0x0000000d07, + 'AVRInput': 0x0000000d08, + 'AVRPower': 0x0000000d09, + 'ChannelDown': 0x0000000d0a, + 'ChannelUp': 0x0000000d0b, + 'ColorF0Red': 0x0000000d0c, + 'ColorF1Green': 0x0000000d0d, + 'ColorF2Yellow': 0x0000000d0e, + 'ColorF3Blue': 0x0000000d0f, + 'ColorF4Grey': 0x0000000d10, + 'ColorF5Brown': 0x0000000d11, + 'ClosedCaptionToggle': 0x0000000d12, + 'Dimmer': 0x0000000d13, + 'DisplaySwap': 0x0000000d14, + 'Exit': 0x0000000d15, + 'FavoriteClear0': 0x0000000d16, + 'FavoriteClear1': 0x0000000d17, + 'FavoriteClear2': 0x0000000d18, + 'FavoriteClear3': 0x0000000d19, + 'FavoriteRecall0': 0x0000000d1a, + 'FavoriteRecall1': 0x0000000d1b, + 'FavoriteRecall2': 0x0000000d1c, + 'FavoriteRecall3': 0x0000000d1d, + 'FavoriteStore0': 0x0000000d1e, + 'FavoriteStore1': 0x0000000d1f, + 'FavoriteStore2': 0x0000000d20, + 'FavoriteStore3': 0x0000000d21, + 'Guide': 0x0000000d22, + 'GuideNextDay': 0x0000000d23, + 'GuidePreviousDay': 0x0000000d24, + 'Info': 0x0000000d25, + 'InstantReplay': 0x0000000d26, + 'Link': 0x0000000d27, + 'ListProgram': 0x0000000d28, + 'LiveContent': 0x0000000d29, + 'Lock': 0x0000000d2a, + 'MediaApps': 0x0000000d2b, + 'MediaFastForward': 0x0000000d2c, + 'MediaLast': 0x0000000d2d, + 'MediaPause': 0x0000000d2e, + 'MediaPlay': 0x0000000d2f, + 'MediaRecord': 0x0000000d30, + 'MediaRewind': 0x0000000d31, + 'MediaSkip': 0x0000000d32, + 'NextFavoriteChannel': 0x0000000d33, + 'NextUserProfile': 0x0000000d34, + 'OnDemand': 0x0000000d35, + 'PinPDown': 0x0000000d36, + 'PinPMove': 0x0000000d37, + 'PinPToggle': 0x0000000d38, + 'PinPUp': 0x0000000d39, + 'PlaySpeedDown': 0x0000000d3a, + 'PlaySpeedReset': 0x0000000d3b, + 'PlaySpeedUp': 0x0000000d3c, + 'RandomToggle': 0x0000000d3d, + 'RcLowBattery': 0x0000000d3e, + 'RecordSpeedNext': 0x0000000d3f, + 'RfBypass': 0x0000000d40, + 'ScanChannelsToggle': 0x0000000d41, + 'ScreenModeNext': 0x0000000d42, + 'Settings': 0x0000000d43, + 'SplitScreenToggle': 0x0000000d44, + 'STBInput': 0x0000000d45, + 'STBPower': 0x0000000d46, + 'Subtitle': 0x0000000d47, + 'Teletext': 0x0000000d48, + 'TV': 0x0000000d49, + 'TVInput': 0x0000000d4a, + 'TVPower': 0x0000000d4b, + 'VideoModeNext': 0x0000000d4c, + 'Wink': 0x0000000d4d, + 'ZoomToggle': 0x0000000d4e, + 'DVR': 0x0000000d4f, + 'MediaAudioTrack': 0x0000000d50, + 'MediaSkipBackward': 0x0000000d51, + 'MediaSkipForward': 0x0000000d52, + 'MediaStepBackward': 0x0000000d53, + 'MediaStepForward': 0x0000000d54, + 'MediaTopMenu': 0x0000000d55, + 'NavigateIn': 0x0000000d56, + 'NavigateNext': 0x0000000d57, + 'NavigateOut': 0x0000000d58, + 'NavigatePrevious': 0x0000000d59, + 'Pairing': 0x0000000d5a, + 'MediaClose': 0x0000000d5b, + 'AudioBassBoostToggle': 0x0000000e02, + 'AudioTrebleDown': 0x0000000e04, + 'AudioTrebleUp': 0x0000000e05, + 'MicrophoneToggle': 0x0000000e06, + 'MicrophoneVolumeDown': 0x0000000e07, + 'MicrophoneVolumeUp': 0x0000000e08, + 'MicrophoneVolumeMute': 0x0000000e09, + 'SpeechCorrectionList': 0x0000000f01, + 'SpeechInputToggle': 0x0000000f02, + 'AppSwitch': 0x0000001001, + 'Call': 0x0000001002, + 'CameraFocus': 0x0000001003, + 'EndCall': 0x0000001004, + 'GoBack': 0x0000001005, + 'GoHome': 0x0000001006, + 'HeadsetHook': 0x0000001007, + 'LastNumberRedial': 0x0000001008, + 'Notification': 0x0000001009, + 'MannerMode': 0x000000100a, + 'VoiceDial': 0x000000100b, + 'TV3DMode': 0x0000001101, + 'TVAntennaCable': 0x0000001102, + 'TVAudioDescription': 0x0000001103, + 'TVAudioDescriptionMixDown': 0x0000001104, + 'TVAudioDescriptionMixUp': 0x0000001105, + 'TVContentsMenu': 0x0000001106, + 'TVDataService': 0x0000001107, + 'TVInputComponent1': 0x0000001108, + 'TVInputComponent2': 0x0000001109, + 'TVInputComposite1': 0x000000110a, + 'TVInputComposite2': 0x000000110b, + 'TVInputHDMI1': 0x000000110c, + 'TVInputHDMI2': 0x000000110d, + 'TVInputHDMI3': 0x000000110e, + 'TVInputHDMI4': 0x000000110f, + 'TVInputVGA1': 0x0000001110, + 'TVMediaContext': 0x0000001111, + 'TVNetwork': 0x0000001112, + 'TVNumberEntry': 0x0000001113, + 'TVRadioService': 0x0000001114, + 'TVSatellite': 0x0000001115, + 'TVSatelliteBS': 0x0000001116, + 'TVSatelliteCS': 0x0000001117, + 'TVSatelliteToggle': 0x0000001118, + 'TVTerrestrialAnalog': 0x0000001119, + 'TVTerrestrialDigital': 0x000000111a, + 'TVTimer': 0x000000111b, + 'Key11': 0x0000001201, + 'Key12': 0x0000001202, + 'GameButton1': 0x000005ff01, + 'GameButton2': 0x000005ff02, + 'GameButton3': 0x000005ff03, + 'GameButton4': 0x000005ff04, + 'GameButton5': 0x000005ff05, + 'GameButton6': 0x000005ff06, + 'GameButton7': 0x000005ff07, + 'GameButton8': 0x000005ff08, + 'GameButton9': 0x000005ff09, + 'GameButton10': 0x000005ff0a, + 'GameButton11': 0x000005ff0b, + 'GameButton12': 0x000005ff0c, + 'GameButton13': 0x000005ff0d, + 'GameButton14': 0x000005ff0e, + 'GameButton15': 0x000005ff0f, + 'GameButton16': 0x000005ff10, + 'GameButtonA': 0x000005ff11, + 'GameButtonB': 0x000005ff12, + 'GameButtonC': 0x000005ff13, + 'GameButtonLeft1': 0x000005ff14, + 'GameButtonLeft2': 0x000005ff15, + 'GameButtonMode': 0x000005ff16, + 'GameButtonRight1': 0x000005ff17, + 'GameButtonRight2': 0x000005ff18, + 'GameButtonSelect': 0x000005ff19, + 'GameButtonStart': 0x000005ff1a, + 'GameButtonThumbLeft': 0x000005ff1b, + 'GameButtonThumbRight': 0x000005ff1c, + 'GameButtonX': 0x000005ff1d, + 'GameButtonY': 0x000005ff1e, + 'GameButtonZ': 0x000005ff1f, + 'Suspend': 0x0100000014, + 'Resume': 0x0100000015, + 'Sleep': 0x0100010082, + 'IntlBackslash': 0x0100070064, + 'IntlRo': 0x0100070087, + 'IntlYen': 0x0100070089, + 'Lang1': 0x0100070090, + 'Lang2': 0x0100070091, + 'Lang3': 0x0100070092, + 'Lang4': 0x0100070093, + 'Lang5': 0x0100070094, + 'Abort': 0x010007009b, +}; + +/// Maps Web KeyboardEvent codes to the matching PhysicalKeyboardKey USB HID code. +const Map kWebToPhysicalKey = { + 'None': 0x00000000, + 'Hyper': 0x00000010, + 'Super': 0x00000011, + 'FnLock': 0x00000013, + 'Suspend': 0x00000014, + 'Resume': 0x00000015, + 'Turbo': 0x00000016, + 'PrivacyScreenToggle': 0x00000017, + 'Sleep': 0x00010082, + 'WakeUp': 0x00010083, + 'DisplayToggleIntExt': 0x000100b5, + 'KeyA': 0x00070004, + 'KeyB': 0x00070005, + 'KeyC': 0x00070006, + 'KeyD': 0x00070007, + 'KeyE': 0x00070008, + 'KeyF': 0x00070009, + 'KeyG': 0x0007000a, + 'KeyH': 0x0007000b, + 'KeyI': 0x0007000c, + 'KeyJ': 0x0007000d, + 'KeyK': 0x0007000e, + 'KeyL': 0x0007000f, + 'KeyM': 0x00070010, + 'KeyN': 0x00070011, + 'KeyO': 0x00070012, + 'KeyP': 0x00070013, + 'KeyQ': 0x00070014, + 'KeyR': 0x00070015, + 'KeyS': 0x00070016, + 'KeyT': 0x00070017, + 'KeyU': 0x00070018, + 'KeyV': 0x00070019, + 'KeyW': 0x0007001a, + 'KeyX': 0x0007001b, + 'KeyY': 0x0007001c, + 'KeyZ': 0x0007001d, + 'Digit1': 0x0007001e, + 'Digit2': 0x0007001f, + 'Digit3': 0x00070020, + 'Digit4': 0x00070021, + 'Digit5': 0x00070022, + 'Digit6': 0x00070023, + 'Digit7': 0x00070024, + 'Digit8': 0x00070025, + 'Digit9': 0x00070026, + 'Digit0': 0x00070027, + 'Enter': 0x00070028, + 'Escape': 0x00070029, + 'Backspace': 0x0007002a, + 'Tab': 0x0007002b, + 'Space': 0x0007002c, + 'Minus': 0x0007002d, + 'Equal': 0x0007002e, + 'BracketLeft': 0x0007002f, + 'BracketRight': 0x00070030, + 'Backslash': 0x00070031, + 'Semicolon': 0x00070033, + 'Quote': 0x00070034, + 'Backquote': 0x00070035, + 'Comma': 0x00070036, + 'Period': 0x00070037, + 'Slash': 0x00070038, + 'CapsLock': 0x00070039, + 'F1': 0x0007003a, + 'F2': 0x0007003b, + 'F3': 0x0007003c, + 'F4': 0x0007003d, + 'F5': 0x0007003e, + 'F6': 0x0007003f, + 'F7': 0x00070040, + 'F8': 0x00070041, + 'F9': 0x00070042, + 'F10': 0x00070043, + 'F11': 0x00070044, + 'F12': 0x00070045, + 'PrintScreen': 0x00070046, + 'ScrollLock': 0x00070047, + 'Pause': 0x00070048, + 'Insert': 0x00070049, + 'Home': 0x0007004a, + 'PageUp': 0x0007004b, + 'Delete': 0x0007004c, + 'End': 0x0007004d, + 'PageDown': 0x0007004e, + 'ArrowRight': 0x0007004f, + 'ArrowLeft': 0x00070050, + 'ArrowDown': 0x00070051, + 'ArrowUp': 0x00070052, + 'NumLock': 0x00070053, + 'NumpadDivide': 0x00070054, + 'NumpadMultiply': 0x00070055, + 'NumpadSubtract': 0x00070056, + 'NumpadAdd': 0x00070057, + 'NumpadEnter': 0x00070058, + 'Numpad1': 0x00070059, + 'Numpad2': 0x0007005a, + 'Numpad3': 0x0007005b, + 'Numpad4': 0x0007005c, + 'Numpad5': 0x0007005d, + 'Numpad6': 0x0007005e, + 'Numpad7': 0x0007005f, + 'Numpad8': 0x00070060, + 'Numpad9': 0x00070061, + 'Numpad0': 0x00070062, + 'NumpadDecimal': 0x00070063, + 'IntlBackslash': 0x00070064, + 'ContextMenu': 0x00070065, + 'Power': 0x00070066, + 'NumpadEqual': 0x00070067, + 'F13': 0x00070068, + 'F14': 0x00070069, + 'F15': 0x0007006a, + 'F16': 0x0007006b, + 'F17': 0x0007006c, + 'F18': 0x0007006d, + 'F19': 0x0007006e, + 'F20': 0x0007006f, + 'F21': 0x00070070, + 'F22': 0x00070071, + 'F23': 0x00070072, + 'F24': 0x00070073, + 'Open': 0x00070074, + 'Help': 0x00070075, + 'Select': 0x00070077, + 'Again': 0x00070079, + 'Undo': 0x0007007a, + 'Cut': 0x0007007b, + 'Copy': 0x0007007c, + 'Paste': 0x0007007d, + 'Find': 0x0007007e, + 'AudioVolumeMute': 0x0007007f, + 'AudioVolumeUp': 0x00070080, + 'AudioVolumeDown': 0x00070081, + 'NumpadComma': 0x00070085, + 'IntlRo': 0x00070087, + 'KanaMode': 0x00070088, + 'IntlYen': 0x00070089, + 'Convert': 0x0007008a, + 'NonConvert': 0x0007008b, + 'Lang1': 0x00070090, + 'Lang2': 0x00070091, + 'Lang3': 0x00070092, + 'Lang4': 0x00070093, + 'Lang5': 0x00070094, + 'Abort': 0x0007009b, + 'Props': 0x000700a3, + 'NumpadParenLeft': 0x000700b6, + 'NumpadParenRight': 0x000700b7, + 'NumpadBackspace': 0x000700bb, + 'NumpadMemoryStore': 0x000700d0, + 'NumpadMemoryRecall': 0x000700d1, + 'NumpadMemoryClear': 0x000700d2, + 'NumpadMemoryAdd': 0x000700d3, + 'NumpadMemorySubtract': 0x000700d4, + 'NumpadClear': 0x000700d8, + 'NumpadClearEntry': 0x000700d9, + 'ControlLeft': 0x000700e0, + 'ShiftLeft': 0x000700e1, + 'AltLeft': 0x000700e2, + 'MetaLeft': 0x000700e3, + 'ControlRight': 0x000700e4, + 'ShiftRight': 0x000700e5, + 'AltRight': 0x000700e6, + 'MetaRight': 0x000700e7, + 'BrightnessUp': 0x000c006f, + 'BrightnessDown': 0x000c0070, + 'MediaPlay': 0x000c00b0, + 'MediaPause': 0x000c00b1, + 'MediaRecord': 0x000c00b2, + 'MediaFastForward': 0x000c00b3, + 'MediaRewind': 0x000c00b4, + 'MediaTrackNext': 0x000c00b5, + 'MediaTrackPrevious': 0x000c00b6, + 'MediaStop': 0x000c00b7, + 'Eject': 0x000c00b8, + 'MediaPlayPause': 0x000c00cd, + 'MediaSelect': 0x000c0183, + 'LaunchMail': 0x000c018a, + 'LaunchApp2': 0x000c0192, + 'LaunchApp1': 0x000c0194, + 'LaunchControlPanel': 0x000c019f, + 'SelectTask': 0x000c01a2, + 'LaunchScreenSaver': 0x000c01b1, + 'LaunchAssistant': 0x000c01cb, + 'BrowserSearch': 0x000c0221, + 'BrowserHome': 0x000c0223, + 'BrowserBack': 0x000c0224, + 'BrowserForward': 0x000c0225, + 'BrowserStop': 0x000c0226, + 'BrowserRefresh': 0x000c0227, + 'BrowserFavorites': 0x000c022a, + 'ZoomToggle': 0x000c0232, + 'MailReply': 0x000c0289, + 'MailForward': 0x000c028b, + 'MailSend': 0x000c028c, + 'KeyboardLayoutSelect': 0x000c029d, + 'ShowAllWindows': 0x000c029f, + 'GameButton1': 0x0005ff01, + 'GameButton2': 0x0005ff02, + 'GameButton3': 0x0005ff03, + 'GameButton4': 0x0005ff04, + 'GameButton5': 0x0005ff05, + 'GameButton6': 0x0005ff06, + 'GameButton7': 0x0005ff07, + 'GameButton8': 0x0005ff08, + 'GameButton9': 0x0005ff09, + 'GameButton10': 0x0005ff0a, + 'GameButton11': 0x0005ff0b, + 'GameButton12': 0x0005ff0c, + 'GameButton13': 0x0005ff0d, + 'GameButton14': 0x0005ff0e, + 'GameButton15': 0x0005ff0f, + 'GameButton16': 0x0005ff10, + 'GameButtonA': 0x0005ff11, + 'GameButtonB': 0x0005ff12, + 'GameButtonC': 0x0005ff13, + 'GameButtonLeft1': 0x0005ff14, + 'GameButtonLeft2': 0x0005ff15, + 'GameButtonMode': 0x0005ff16, + 'GameButtonRight1': 0x0005ff17, + 'GameButtonRight2': 0x0005ff18, + 'GameButtonSelect': 0x0005ff19, + 'GameButtonStart': 0x0005ff1a, + 'GameButtonThumbLeft': 0x0005ff1b, + 'GameButtonThumbRight': 0x0005ff1c, + 'GameButtonX': 0x0005ff1d, + 'GameButtonY': 0x0005ff1e, + 'GameButtonZ': 0x0005ff1f, + 'Fn': 0x00000012, +}; + +/// Maps Web KeyboardEvent keys to Flutter logical IDs that depend on locations. +/// +/// `KeyboardEvent.location` is defined as: +/// +/// * 0: Standard +/// * 1: Left +/// * 2: Right +/// * 3: Numpad +const Map> kWebLogicalLocationMap = >{ + '0': [0x0000000030, null, null, 0x0200000030], + '1': [0x0000000031, null, null, 0x0200000031], + '2': [0x0000000032, null, null, 0x0200000032], + '3': [0x0000000033, null, null, 0x0200000033], + '4': [0x0000000034, null, null, 0x0200000034], + '5': [0x0000000035, null, null, 0x0200000035], + '6': [0x0000000036, null, null, 0x0200000036], + '7': [0x0000000037, null, null, 0x0200000037], + '8': [0x0000000038, null, null, 0x0200000038], + '9': [0x0000000039, null, null, 0x0200000039], + '.': [0x000000002e, null, null, 0x020000002e], + 'Insert': [0x0000000407, null, null, 0x0200000030], + 'End': [0x0000000305, null, null, 0x0200000031], + 'ArrowDown': [0x0000000301, null, null, 0x0200000032], + 'PageDown': [0x0000000307, null, null, 0x0200000033], + 'ArrowLeft': [0x0000000302, null, null, 0x0200000034], + 'Clear': [0x0000000401, null, null, 0x0200000035], + 'ArrowRight': [0x0000000303, null, null, 0x0200000036], + 'Home': [0x0000000306, null, null, 0x0200000037], + 'ArrowUp': [0x0000000304, null, null, 0x0200000038], + 'PageUp': [0x0000000308, null, null, 0x0200000039], + 'Delete': [0x000000007f, null, null, 0x020000002e], + '/': [0x000000002f, null, null, 0x020000002f], + '*': [0x000000002a, null, null, 0x020000002a], + '-': [0x000000002d, null, null, 0x020000002d], + '+': [0x000000002b, null, null, 0x020000002b], + 'Enter': [0x000000000d, null, null, 0x020000000d], + 'Shift': [null, 0x030000010d, 0x040000010d, null], + 'Control': [null, 0x0300000105, 0x0400000105, null], + 'Alt': [null, 0x0300000102, 0x0400000102, null], + 'Meta': [null, 0x0300000109, 0x0400000109, null], +}; diff --git a/lib/web_ui/lib/src/engine/keyboard_binding.dart b/lib/web_ui/lib/src/engine/keyboard_binding.dart new file mode 100644 index 000000000..40e5a39fc --- /dev/null +++ b/lib/web_ui/lib/src/engine/keyboard_binding.dart @@ -0,0 +1,481 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.12 +part of engine; + +typedef _VoidCallback = void Function(); +typedef ValueGetter = T Function(); +typedef _ModifierGetter = bool Function(FlutterHtmlKeyboardEvent event); + +// Set this flag to true to see all the fired events in the console. +const bool _debugLogKeyEvents = false; + +const int _kLogicalAltLeft = 0x0300000102; +const int _kLogicalAltRight = 0x0400000102; +const int _kLogicalControlLeft = 0x0300000105; +const int _kLogicalControlRight = 0x0400000105; +const int _kLogicalShiftLeft = 0x030000010d; +const int _kLogicalShiftRight = 0x040000010d; +const int _kLogicalMetaLeft = 0x0300000109; +const int _kLogicalMetaRight = 0x0400000109; +// Map logical keys for modifier keys to the functions that can get their +// modifier flag out of an event. +final Map _kLogicalKeyToModifierGetter = { + _kLogicalAltLeft: (FlutterHtmlKeyboardEvent event) => event.altKey, + _kLogicalAltRight: (FlutterHtmlKeyboardEvent event) => event.altKey, + _kLogicalControlLeft: (FlutterHtmlKeyboardEvent event) => event.ctrlKey, + _kLogicalControlRight: (FlutterHtmlKeyboardEvent event) => event.ctrlKey, + _kLogicalShiftLeft: (FlutterHtmlKeyboardEvent event) => event.shiftKey, + _kLogicalShiftRight: (FlutterHtmlKeyboardEvent event) => event.shiftKey, + _kLogicalMetaLeft: (FlutterHtmlKeyboardEvent event) => event.metaKey, + _kLogicalMetaRight: (FlutterHtmlKeyboardEvent event) => event.metaKey, +}; + +// After a keydown is received, this is the duration we wait for a repeat event +// before we decide to synthesize a keyup event. +// +// On Linux and Windows, the typical ranges for keyboard repeat delay go up to +// 1000ms. On Mac, the range goes up to 2000ms. +const Duration _kKeydownCancelDurationNormal = Duration(milliseconds: 1000); +const Duration _kKeydownCancelDurationMacOs = Duration(milliseconds: 2000); + +// ASCII for a, z, A, and Z +const int _kCharLowerA = 0x61; +const int _kCharLowerZ = 0x7a; +const int _kCharUpperA = 0x41; +const int _kCharUpperZ = 0x5a; +bool isAlphabet(int charCode) { + return (charCode >= _kCharLowerA && charCode <= _kCharLowerZ) + || (charCode >= _kCharUpperA && charCode <= _kCharUpperZ); +} + +const String _kPhysicalCapsLock = 'CapsLock'; + +const String _kLogicalDead = 'Dead'; + +const int _kWebKeyIdPlane = 0x00800000000; +const int _kAutogeneratedMask = 0x10000000000; + +// Bits in a Flutter logical event to generate the logical key for dead keys. +// +// Logical keys for dead keys are generated by annotating physical keys with +// modifiers (see `_getLogicalCode`). +const int _kDeadKeyCtrl = 0x100000000000; +const int _kDeadKeyShift = 0x200000000000; +const int _kDeadKeyAlt = 0x400000000000; +const int _kDeadKeyMeta = 0x800000000000; + +typedef DispatchKeyData = bool Function(ui.KeyData data); + +/// Converts a floating number timestamp (in milliseconds) to a [Duration] by +/// splitting it into two integer components: milliseconds + microseconds. +Duration _eventTimeStampToDuration(num milliseconds) { + final int ms = milliseconds.toInt(); + final int micro = ((milliseconds - ms) * Duration.microsecondsPerMillisecond).toInt(); + return Duration(milliseconds: ms, microseconds: micro); +} + +class KeyboardBinding { + /// The singleton instance of this object. + static KeyboardBinding? get instance => _instance; + static KeyboardBinding? _instance; + + static void initInstance(html.Element glassPaneElement) { + if (_instance == null) { + _instance = KeyboardBinding._(glassPaneElement); + assert(() { + registerHotRestartListener(_instance!._reset); + return true; + }()); + } + } + + KeyboardBinding._(this.glassPaneElement) { + _setup(); + } + + final html.Element glassPaneElement; + late KeyboardConverter _converter; + final Map _listeners = {}; + + void _addEventListener(String eventName, html.EventListener handler) { + final html.EventListener loggedHandler = (html.Event event) { + if (_debugLogKeyEvents) { + print(event.type); + } + if (EngineSemanticsOwner.instance.receiveGlobalEvent(event)) { + return handler(event); + } + }; + assert(!_listeners.containsKey(eventName)); + _listeners[eventName] = loggedHandler; + html.window.addEventListener(eventName, loggedHandler, true); + } + + /// Remove all active event listeners. + void _clearListeners() { + _listeners.forEach((String eventName, html.EventListener listener) { + html.window.removeEventListener(eventName, listener, true); + }); + _listeners.clear(); + } + bool _onKeyData(ui.KeyData data) { + bool? result; + // This callback is designed to be invoked synchronously. This is enforced + // by `result`, which starts null and is asserted non-null when returned. + EnginePlatformDispatcher.instance.invokeOnKeyData(data, + (bool handled) { result = handled; }); + return result!; + } + + void _setup() { + _addEventListener('keydown', (html.Event event) { + return _converter.handleEvent(FlutterHtmlKeyboardEvent(event as html.KeyboardEvent)); + }); + _addEventListener('keyup', (html.Event event) { + return _converter.handleEvent(FlutterHtmlKeyboardEvent(event as html.KeyboardEvent)); + }); + _converter = KeyboardConverter(_onKeyData, onMacOs: operatingSystem == OperatingSystem.macOs); + } + + void _reset() { + _clearListeners(); + _converter.dispose(); + } +} + +class AsyncKeyboardDispatching { + AsyncKeyboardDispatching({ + required this.keyData, + this.callback, + }); + + final ui.KeyData keyData; + final _VoidCallback? callback; +} + +// A wrapper of [html.KeyboardEvent] with reduced methods delegated to the event +// for the convenience of testing. +class FlutterHtmlKeyboardEvent { + FlutterHtmlKeyboardEvent(this._event); + + final html.KeyboardEvent _event; + + String get type => _event.type; + String? get code => _event.code; + String? get key => _event.key; + bool? get repeat => _event.repeat; + int? get location => _event.location; + num? get timeStamp => _event.timeStamp; + bool get altKey => _event.altKey; + bool get ctrlKey => _event.ctrlKey; + bool get shiftKey => _event.shiftKey; + bool get metaKey => _event.metaKey; + + bool getModifierState(String key) => _event.getModifierState(key); + void preventDefault() => _event.preventDefault(); +} + +// Reads [html.KeyboardEvent], then [dispatches ui.KeyData] accordingly. +// +// The events are read through [handleEvent], and dispatched through the +// [dispatchKeyData] as given in the constructor. Some key data might be +// dispatched asynchronously. +class KeyboardConverter { + KeyboardConverter(this.dispatchKeyData, {this.onMacOs = false}); + + final DispatchKeyData dispatchKeyData; + final bool onMacOs; + + bool _disposed = false; + void dispose() { + _disposed = true; + } + + // On macOS, CapsLock behaves differently in that, a keydown event occurs when + // the key is pressed and the light turns on, while a keyup event occurs when the + // key is pressed and the light turns off. Flutter considers both events as + // key down, and synthesizes immediate cancel events following them. The state + // of "whether CapsLock is on" should be accessed by "activeLocks". + bool _shouldSynthesizeCapsLockUp() { + return onMacOs; + } + + Duration get _keydownCancelDuration => onMacOs ? _kKeydownCancelDurationMacOs : _kKeydownCancelDurationNormal; + + static int _getPhysicalCode(String code) { + return kWebToPhysicalKey[code] ?? (code.hashCode + _kWebKeyIdPlane + _kAutogeneratedMask); + } + + static int _getModifierMask(FlutterHtmlKeyboardEvent event) { + final bool altDown = event.altKey; + final bool ctrlDown = event.ctrlKey; + final bool shiftDown = event.shiftKey; + final bool metaDown = event.metaKey; + return (altDown ? _kDeadKeyAlt : 0) + + (ctrlDown ? _kDeadKeyCtrl : 0) + + (shiftDown ? _kDeadKeyShift : 0) + + (metaDown ? _kDeadKeyMeta : 0); + } + + // Whether `event.key` should be considered a key name. + // + // The `event.key` can either be a key name or the printable character. If the + // first character is an alphabet, it must be either 'A' to 'Z' ( and return + // true), or be a key name (and return false). Otherwise, return true. + static bool _eventKeyIsKeyname(String key) { + assert(key.length > 0); + return isAlphabet(key.codeUnitAt(0)) && key.length > 1; + } + + static int _characterToLogicalKey(String key) { + // Assume the length being <= 2 to be sufficient in all cases. If not, + // extend the algorithm. + assert(key.length <= 2); + int result = key.codeUnitAt(0) & 0xffff; + if (key.length == 2) { + result += key.codeUnitAt(1) << 16; + } + // Convert upper letters to lower letters + if (result >= _kCharUpperA && result <= _kCharUpperZ) { + result = result + _kCharLowerA - _kCharUpperA; + } + return result; + } + + static int _deadKeyToLogicalKey(int physicalKey, FlutterHtmlKeyboardEvent event) { + // 'Dead' is used to represent dead keys, such as a diacritic to the + // following base letter (such as Option-e results in ´). + // + // Assume they can be told apart with the physical key and the modifiers + // pressed. + return physicalKey + _getModifierMask(event) + _kWebKeyIdPlane + _kAutogeneratedMask; + } + + static int _otherLogicalKey(String key) { + return kWebToLogicalKey[key] ?? (key.hashCode + _kWebKeyIdPlane + _kAutogeneratedMask); + } + + // Map from pressed physical key to corresponding pressed logical key. + // + // Multiple physical keys can be mapped to the same logical key, usually due + // to positioned keys (left/right/numpad) or multiple keyboards. + final Map _pressingRecords = {}; + + // Schedule the dispatching of an event in the future. The `callback` will + // invoked before that. + // + // Returns a callback that cancels the schedule. Disposal of + // `KeyBoardConverter` also cancels the shedule automatically. + _VoidCallback _scheduleAsyncEvent(Duration duration, ValueGetter getData, _VoidCallback callback) { + bool canceled = false; + Future.delayed(duration).then((_) { + if (!canceled && !_disposed) { + callback(); + dispatchKeyData(getData()); + } + }); + return () { canceled = true; }; + } + + // ## About Key guards + // + // When the user enters a browser/system shortcut (e.g. `cmd+alt+i`) the + // browser doesn't send a keyup for it. This puts the framework in a corrupt + // state because it thinks the key was never released. + // + // To avoid this, we rely on the fact that browsers send repeat events + // while the key is held down by the user. If we don't receive a repeat + // event within a specific duration ([_keydownCancelDuration]) we assume + // the user has released the key and we synthesize a keyup event. + final Map _keyGuards = {}; + // Call this method on the down or repeated event of a non-modifier key. + void _startGuardingKey(int physicalKey, int logicalKey, Duration currentTimeStamp) { + final _VoidCallback cancelingCallback = _scheduleAsyncEvent( + _keydownCancelDuration, + () => ui.KeyData( + timeStamp: currentTimeStamp + _keydownCancelDuration, + type: ui.KeyEventType.up, + physical: physicalKey, + logical: logicalKey, + character: null, + synthesized: true, + ), + () { + _pressingRecords.remove(physicalKey); + } + ); + _keyGuards.remove(physicalKey)?.call(); + _keyGuards[physicalKey] = cancelingCallback; + } + // Call this method on an up event event of a non-modifier key. + void _stopGuardingKey(int physicalKey) { + _keyGuards.remove(physicalKey)?.call(); + } + + // Parse the HTML event, update states, and dispatch Flutter key data through + // [dispatchKeyData]. + // + // * The method might dispatch some synthesized key data first to update states, + // results discarded. + // * Then it dispatches exactly one non-synthesized key data that corresponds + // to the `event`, i.e. the primary key data. If this dispatching returns + // true, then this event will be invoked `preventDefault`. + // * Some key data might be synthesized to update states after the main key + // data. They are always scheduled asynchronously with results discarded. + void handleEvent(FlutterHtmlKeyboardEvent event) { + final Duration timeStamp = _eventTimeStampToDuration(event.timeStamp!); + + final String eventKey = event.key!; + + final int physicalKey = _getPhysicalCode(event.code!); + final bool logicalKeyIsCharacter = !_eventKeyIsKeyname(eventKey); + final String? character = logicalKeyIsCharacter ? eventKey : null; + final int logicalKey = () { + if (kWebLogicalLocationMap.containsKey(event.key!)) { + final int? result = kWebLogicalLocationMap[event.key!]?[event.location!]; + assert(result != null, 'Invalid modifier location: ${event.key}, ${event.location}'); + return result!; + } + if (character != null) + return _characterToLogicalKey(character); + if (eventKey == _kLogicalDead) + return _deadKeyToLogicalKey(physicalKey, event); + return _otherLogicalKey(eventKey); + }(); + + assert(event.type == 'keydown' || event.type == 'keyup'); + final bool isPhysicalDown = event.type == 'keydown' || + // On macOS, both keydown and keyup events of CapsLock should be considered keydown, + // followed by an immediate cancel event. + (_shouldSynthesizeCapsLockUp() && event.code! == _kPhysicalCapsLock); + + final int? lastLogicalRecord = _pressingRecords[physicalKey]; + + ui.KeyEventType type; + + if (_shouldSynthesizeCapsLockUp() && event.code! == _kPhysicalCapsLock) { + // Case 1: Handle CapsLock on macOS + // + // On macOS, both keydown and keyup events of CapsLock are considered + // keydown, followed by an immediate synchronized up event. + + _scheduleAsyncEvent( + Duration.zero, + () => ui.KeyData( + timeStamp: timeStamp, + type: ui.KeyEventType.up, + physical: physicalKey, + logical: logicalKey, + character: null, + synthesized: true, + ), + () { + _pressingRecords.remove(physicalKey); + } + ); + type = ui.KeyEventType.down; + + } else if (isPhysicalDown) { + // Case 2: Handle key down of normal keys + type = ui.KeyEventType.down; + if (lastLogicalRecord != null) { + // This physical key is being pressed according to the record. + if (event.repeat ?? false) { + // A normal repeated key. + type = ui.KeyEventType.repeat; + } else { + // A non-repeated key has been pressed that has the exact physical key as + // a currently pressed one, usually indicating multiple keyboards are + // pressing keys with the same physical key, or the up event was lost + // during a loss of focus. The down event is ignored. + return; + } + } else { + // This physical key is not being pressed according to the record. It's a + // normal down event, whether the system event is a repeat or not. + } + + } else { // isPhysicalDown is false and not CapsLock + // Case 2: Handle key up of normal keys + if (lastLogicalRecord == null) { + // The physical key has been released before. It indicates multiple + // keyboards pressed keys with the same physical key. Ignore the up event. + return; + } + + type = ui.KeyEventType.up; + } + + final int? nextLogicalRecord; + switch (type) { + case ui.KeyEventType.down: + assert(lastLogicalRecord == null); + nextLogicalRecord = logicalKey; + break; + case ui.KeyEventType.up: + assert(lastLogicalRecord != null); + nextLogicalRecord = null; + break; + case ui.KeyEventType.repeat: + assert(lastLogicalRecord != null); + nextLogicalRecord = lastLogicalRecord; + break; + } + if (nextLogicalRecord == null) { + _pressingRecords.remove(physicalKey); + } else { + _pressingRecords[physicalKey] = nextLogicalRecord; + } + + // After updating _pressingRecords, synchronize modifier states. The + // `event.***Key` fields can be used to reduce some omitted modifier key + // events. We can deduce key cancel events if they are false. Key sync + // events can not be deduced since we don't know which physical key they + // represent. + _kLogicalKeyToModifierGetter.forEach((int logicalKey, _ModifierGetter getModifier) { + if (_pressingRecords.containsValue(logicalKey) && !getModifier(event)) { + _pressingRecords.removeWhere((int physicalKey, int logicalRecord) { + if (logicalRecord != logicalKey) + return false; + + dispatchKeyData(ui.KeyData( + timeStamp: timeStamp, + type: ui.KeyEventType.up, + physical: physicalKey, + logical: logicalKey, + character: null, + synthesized: true, + )); + + return true; + }); + } + }); + + // Update key guards + if (logicalKeyIsCharacter) { + if (nextLogicalRecord != null) { + _startGuardingKey(physicalKey, logicalKey, timeStamp); + } else { + _stopGuardingKey(physicalKey); + } + } + + final ui.KeyData keyData = ui.KeyData( + timeStamp: timeStamp, + type: type, + physical: physicalKey, + logical: lastLogicalRecord ?? logicalKey, + character: type == ui.KeyEventType.up ? null : character, + synthesized: false, + ); + + bool primaryHandled = dispatchKeyData(keyData); + if (primaryHandled) { + event.preventDefault(); + } + } +} diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 7f6ce039c..10faac0b9 100644 --- a/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -10,6 +10,8 @@ part of engine; /// This may be overridden in tests, for example, to pump fake frames. ui.VoidCallback? scheduleFrameCallback; +typedef _KeyDataResponseCallback = void Function(bool handled); + /// Platform event dispatcher. /// /// This is the central entry point for platform messages and configuration @@ -170,6 +172,34 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { invoke1(_onPointerDataPacket, _onPointerDataPacketZone, dataPacket); } + /// A callback that is invoked when key data is available. + /// + /// The framework invokes this callback in the same zone in which the + /// callback was set. + /// + /// See also: + /// + /// * [GestureBinding], the Flutter framework class which manages pointer + /// events. + @override + ui.KeyDataCallback? get onKeyData => _onKeyData; + ui.KeyDataCallback? _onKeyData; + Zone? _onKeyDataZone; + @override + set onKeyData(ui.KeyDataCallback? callback) { + _onKeyData = callback; + _onKeyDataZone = Zone.current; + } + + /// Engine code should use this method instead of the callback directly. + /// Otherwise zones won't work properly. + void invokeOnKeyData(ui.KeyData data, _KeyDataResponseCallback callback) { + invoke( + () { callback(onKeyData == null ? false : onKeyData!(data)); }, + _onKeyDataZone, + ); + } + /// A callback that is invoked to report the [FrameTiming] of recently /// rasterized frames. /// @@ -947,4 +977,3 @@ void invoke3(void Function(A1 a1, A2 a2, A3 a3)? callback, Zone? zon }); } } - diff --git a/lib/web_ui/lib/src/ui/key.dart b/lib/web_ui/lib/src/ui/key.dart new file mode 100644 index 000000000..14fb39393 --- /dev/null +++ b/lib/web_ui/lib/src/ui/key.dart @@ -0,0 +1,108 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.12 + +part of ui; + +/// The type of a key event. +// Must match the KeyEventType enum in ui/window/key_data.h. +enum KeyEventType { + /// The key is pressed. + down, + + /// The key is released. + up, + + /// The key is held, causing a repeated key input. + repeat, +} + +/// Information about a key event. +class KeyData { + /// Creates an object that represents a key event. + const KeyData({ + required this.timeStamp, + required this.type, + required this.physical, + required this.logical, + required this.character, + required this.synthesized, + }); + + /// Time of event dispatch, relative to an arbitrary timeline. + /// + /// For [KeyEventType.synchronize] and [KeyEventType.cancel] events, the [timeStamp] + /// might not be the actual time that the key press or release happens. + final Duration timeStamp; + + /// The type of the event. + final KeyEventType type; + + /// The key code for the physical key that has changed. + final int physical; + + /// The key code for the logical key that has changed. + final int logical; + + /// Character input from the event. + /// + /// Ignored for up events. + final String? character; + + /// If [synthesized] is true, this event does not correspond to a native event. + /// + /// Although most of Flutter's keyboard events are transformed from native + /// events, some events are not based on native events, and are synthesized + /// only to conform Flutter's key event model (as documented in + /// the `HardwareKeyboard` class in the framework). + /// + /// For example, some key downs or ups might be lost when the window loses + /// focus. Some platforms provides ways to query whether a key is being held. + /// If Flutter detects an inconsistancy between the state Flutter records and + /// the state returned by the system, Flutter will synthesize a corresponding + /// event to synchronize the state without breaking the event model. + /// + /// As another example, macOS treats CapsLock in a special way by sending + /// down and up events at the down of alterate presses to indicate the + /// direction in which the lock is toggled instead of that the physical key is + /// going. Flutter normalizes the behavior by converting a native down event + /// into a down event followed immediately by a synthesized up event, and + /// the native up event also into a down event followed immediately by a + /// synthesized up event. + /// + /// Synthesized events do not have a trustworthy [timeStamp], and should not be + /// processed as if the key actually went down or up at the time of the + /// callback. + /// + /// [KeyRepeatEvent] is never synthesized. + final bool synthesized; + + @override + String toString() => 'KeyData(type: ${_typeToString(type)}, physical: 0x${physical.toRadixString(16)}, ' + 'logical: 0x${logical.toRadixString(16)}, character: $character)'; + + /// Returns a complete textual description of the information in this object. + String toStringFull() { + return '$runtimeType(' + 'type: ${_typeToString(type)}, ' + 'timeStamp: $timeStamp, ' + 'physical: 0x${physical.toRadixString(16)}, ' + 'logical: 0x${logical.toRadixString(16)}, ' + 'character: $character, ' + 'synthesized: $synthesized' + ')'; + } + + static String _typeToString(KeyEventType type) { + switch (type) { + case KeyEventType.up: + return 'up'; + case KeyEventType.down: + return 'down'; + case KeyEventType.repeat: + return 'repeat'; + } + } +} diff --git a/lib/web_ui/lib/src/ui/platform_dispatcher.dart b/lib/web_ui/lib/src/ui/platform_dispatcher.dart index 122e2befd..87ee101c4 100644 --- a/lib/web_ui/lib/src/ui/platform_dispatcher.dart +++ b/lib/web_ui/lib/src/ui/platform_dispatcher.dart @@ -9,6 +9,7 @@ typedef VoidCallback = void Function(); typedef FrameCallback = void Function(Duration duration); typedef TimingsCallback = void Function(List timings); typedef PointerDataPacketCallback = void Function(PointerDataPacket packet); +typedef KeyDataCallback = bool Function(KeyData packet); typedef SemanticsActionCallback = void Function(int id, SemanticsAction action, ByteData? args); typedef PlatformMessageResponseCallback = void Function(ByteData? data); typedef PlatformMessageCallback = void Function( @@ -36,6 +37,9 @@ abstract class PlatformDispatcher { PointerDataPacketCallback? get onPointerDataPacket; set onPointerDataPacket(PointerDataPacketCallback? callback); + KeyDataCallback? get onKeyData; + set onKeyData(KeyDataCallback? callback); + TimingsCallback? get onReportTimings; set onReportTimings(TimingsCallback? callback); @@ -417,4 +421,4 @@ class Locale { } return out.toString(); } -} \ No newline at end of file +} diff --git a/lib/web_ui/lib/src/ui/window.dart b/lib/web_ui/lib/src/ui/window.dart index 6fc8b901c..ef3452947 100644 --- a/lib/web_ui/lib/src/ui/window.dart +++ b/lib/web_ui/lib/src/ui/window.dart @@ -81,6 +81,11 @@ abstract class SingletonFlutterWindow extends FlutterWindow { platformDispatcher.onPointerDataPacket = callback; } + KeyDataCallback? get onKeyData => platformDispatcher.onKeyData; + set onKeyData(KeyDataCallback? callback) { + platformDispatcher.onKeyData = callback; + } + String get defaultRouteName => platformDispatcher.defaultRouteName; void scheduleFrame() => platformDispatcher.scheduleFrame(); diff --git a/lib/web_ui/lib/ui.dart b/lib/web_ui/lib/ui.dart index c64360fec..334887d87 100644 --- a/lib/web_ui/lib/ui.dart +++ b/lib/web_ui/lib/ui.dart @@ -24,6 +24,7 @@ part 'src/ui/compositing.dart'; part 'src/ui/geometry.dart'; part 'src/ui/hash_codes.dart'; part 'src/ui/initialization.dart'; +part 'src/ui/key.dart'; part 'src/ui/lerp.dart'; part 'src/ui/natives.dart'; part 'src/ui/painting.dart'; diff --git a/lib/web_ui/test/keyboard_converter_test.dart b/lib/web_ui/test/keyboard_converter_test.dart new file mode 100644 index 000000000..8c2126792 --- /dev/null +++ b/lib/web_ui/test/keyboard_converter_test.dart @@ -0,0 +1,969 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.10 + +import 'package:quiver/testing/async.dart'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; +import 'package:meta/meta.dart' show isTest; + +const int kLocationLeft = 1; +const int kLocationRight = 2; +const int kLocationNumpad = 3; + +const int kPhysicalKeyA = 0x00070004; +const int kPhysicalKeyE = 0x00070008; +const int kPhysicalKeyU = 0x00070018; +const int kPhysicalDigit1 = 0x0007001e; +const int kPhysicalNumpad1 = 0x00070059; +const int kPhysicalShiftLeft = 0x000700e1; +const int kPhysicalShiftRight = 0x000700e5; +const int kPhysicalMetaLeft = 0x000700e3; +const int kPhysicalTab = 0x0007002b; +const int kPhysicalCapsLock = 0x00070039; +const int kPhysicalScrollLock = 0x00070047; + +const int kLogicalKeyA = 0x00000000061; +const int kLogicalKeyU = 0x00000000075; +const int kLogicalDigit1 = 0x00000000031; +const int kLogicalNumpad1 = 0x00200000031; +const int kLogicalShiftLeft = 0x030000010d; +const int kLogicalShiftRight = 0x040000010d; +const int kLogicalMetaLeft = 0x0300000109; +const int kLogicalTab = 0x0000000009; +const int kLogicalCapsLock = 0x00000000104; +const int kLogicalScrollLock = 0x0000000010c; + +typedef VoidCallback = void Function(); + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + test('Single key press, repeat, and release', () { + final List keyDataList = []; + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + // Only handle down events + return key.type == ui.KeyEventType.down; + }); + bool preventedDefault = false; + final onPreventDefault = () { preventedDefault = true; }; + + converter.handleEvent(keyDownEvent('KeyA', 'a') + ..timeStamp = 1 + ..onPreventDefault = onPreventDefault + ); + expectKeyData(keyDataList.last, + timeStamp: Duration(milliseconds: 1), + type: ui.KeyEventType.down, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: 'a', + ); + expect(preventedDefault, true); + preventedDefault = false; + + converter.handleEvent(keyRepeatedDownEvent('KeyA', 'a') + ..timeStamp = 1.5 + ..onPreventDefault = onPreventDefault + ); + expectKeyData(keyDataList.last, + timeStamp: Duration(milliseconds: 1, microseconds: 500), + type: ui.KeyEventType.repeat, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: 'a', + ); + expect(preventedDefault, false); + + converter.handleEvent(keyRepeatedDownEvent('KeyA', 'a') + ..timeStamp = 1500 + ..onPreventDefault = onPreventDefault + ); + expectKeyData(keyDataList.last, + timeStamp: Duration(seconds: 1, milliseconds: 500), + type: ui.KeyEventType.repeat, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: 'a', + ); + expect(preventedDefault, false); + + converter.handleEvent(keyUpEvent('KeyA', 'a') + ..timeStamp = 2000.5 + ..onPreventDefault = onPreventDefault + ); + expectKeyData(keyDataList.last, + timeStamp: Duration(seconds: 2, microseconds: 500), + type: ui.KeyEventType.up, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: null, + ); + expect(preventedDefault, false); + }); + + test('Release modifier during a repeated sequence', () { + final List keyDataList = []; + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + // Only handle down events + return key.type == ui.KeyEventType.down; + }); + bool preventedDefault = false; + final onPreventDefault = () { preventedDefault = true; }; + + converter.handleEvent(keyDownEvent('ShiftLeft', 'Shift', kShift, kLocationLeft) + ..onPreventDefault = onPreventDefault + ); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalShiftLeft, + logical: kLogicalShiftLeft, + character: null, + ); + expect(preventedDefault, true); + preventedDefault = false; + + converter.handleEvent(keyDownEvent('KeyA', 'A', kShift) + ..onPreventDefault = onPreventDefault + ); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: 'A', + ); + expect(preventedDefault, true); + preventedDefault = false; + + converter.handleEvent(keyRepeatedDownEvent('KeyA', 'A', kShift) + ..onPreventDefault = onPreventDefault + ); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.repeat, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: 'A', + ); + expect(preventedDefault, false); + + converter.handleEvent(keyUpEvent('ShiftLeft', 'Shift', 0, kLocationLeft) + ..onPreventDefault = onPreventDefault + ); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: kPhysicalShiftLeft, + logical: kLogicalShiftLeft, + character: null, + ); + expect(preventedDefault, false); + + converter.handleEvent(keyRepeatedDownEvent('KeyA', 'a') + ..onPreventDefault = onPreventDefault + ); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.repeat, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: 'a', + ); + expect(preventedDefault, false); + + converter.handleEvent(keyRepeatedDownEvent('KeyA', 'a') + ..onPreventDefault = onPreventDefault + ); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.repeat, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: 'a', + ); + expect(preventedDefault, false); + + converter.handleEvent(keyUpEvent('KeyA', 'a')); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: null, + ); + expect(preventedDefault, false); + }); + + test('Distinguish between left and right modifiers', () { + final List keyDataList = []; + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + return true; + }); + + converter.handleEvent(keyDownEvent('ShiftLeft', 'Shift', kShift, kLocationLeft)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalShiftLeft, + logical: kLogicalShiftLeft, + character: null, + ); + + converter.handleEvent(keyDownEvent('ShiftRight', 'Shift', kShift, kLocationRight)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalShiftRight, + logical: kLogicalShiftRight, + character: null, + ); + + converter.handleEvent(keyUpEvent('ShiftLeft', 'Shift', kShift, kLocationLeft)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: kPhysicalShiftLeft, + logical: kLogicalShiftLeft, + character: null, + ); + + converter.handleEvent(keyUpEvent('ShiftRight', 'Shift', 0, kLocationRight)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: kPhysicalShiftRight, + logical: kLogicalShiftRight, + character: null, + ); + }); + + test('Distinguish between normal and numpad digits', () { + final List keyDataList = []; + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + return true; + }); + + converter.handleEvent(keyDownEvent('Digit1', '1', 0, 0)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalDigit1, + logical: kLogicalDigit1, + character: '1', + ); + + converter.handleEvent(keyDownEvent('Numpad1', '1', 0, kLocationNumpad)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalNumpad1, + logical: kLogicalNumpad1, + character: '1', + ); + + converter.handleEvent(keyUpEvent('Digit1', '1', 0, 0)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: kPhysicalDigit1, + logical: kLogicalDigit1, + character: null, + ); + + converter.handleEvent(keyUpEvent('Numpad1', '1', 0, kLocationNumpad)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: kPhysicalNumpad1, + logical: kLogicalNumpad1, + character: null, + ); + }); + + test('Dead keys are distinguishable', () { + final List keyDataList = []; + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + return true; + }); + + // The absolute values of the following logical keys are not guaranteed. + const int kLogicalAltE = 0x410800070008; + const int kLogicalAltU = 0x410800070018; + const int kLogicalAltShiftE = 0x610800070008; + // The values must be distinguishable. + expect(kLogicalAltE, isNot(equals(kLogicalAltU))); + expect(kLogicalAltE, isNot(equals(kLogicalAltShiftE))); + + converter.handleEvent(keyDownEvent('AltLeft', 'Alt', kAlt, kLocationLeft)); + + converter.handleEvent(keyDownEvent('KeyE', 'Dead', kAlt)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalKeyE, + logical: kLogicalAltE, + character: null, + ); + + converter.handleEvent(keyUpEvent('KeyE', 'Dead', kAlt)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: kPhysicalKeyE, + logical: kLogicalAltE, + character: null, + ); + + converter.handleEvent(keyDownEvent('KeyU', 'Dead', kAlt)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalKeyU, + logical: kLogicalAltU, + character: null, + ); + + converter.handleEvent(keyUpEvent('KeyU', 'Dead', kAlt)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: kPhysicalKeyU, + logical: kLogicalAltU, + character: null, + ); + + converter.handleEvent(keyDownEvent('ShiftLeft', 'Shift', kAlt | kShift, kLocationLeft)); + + // This does not actually produce a Dead key on macOS (US layout); just for + // testing. + converter.handleEvent(keyDownEvent('KeyE', 'Dead', kAlt | kShift)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalKeyE, + logical: kLogicalAltShiftE, + character: null, + ); + + converter.handleEvent(keyUpEvent('AltLeft', 'Alt', kShift, kLocationLeft)); + + converter.handleEvent(keyUpEvent('KeyE', 'e', kShift)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: kPhysicalKeyE, + logical: kLogicalAltShiftE, + character: null, + ); + + converter.handleEvent(keyUpEvent('ShiftLeft', 'Shift', 0, kLocationLeft)); + }); + + test('Duplicate down is ignored', () { + final List keyDataList = []; + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + return true; + }); + bool preventedDefault = false; + final onPreventDefault = () { preventedDefault = true; }; + + converter.handleEvent(keyDownEvent('ShiftLeft', 'Shift', kShift, kLocationLeft) + ..onPreventDefault = onPreventDefault + ); + expect(preventedDefault, true); + preventedDefault = false; + // A KeyUp of ShiftLeft is missed due to loss of focus. + + keyDataList.clear(); + converter.handleEvent(keyDownEvent('ShiftLeft', 'Shift', kShift, kLocationLeft) + ..onPreventDefault = onPreventDefault + ); + expect(keyDataList, isEmpty); + expect(preventedDefault, false); + + converter.handleEvent(keyUpEvent('ShiftLeft', 'Shift', 0, kLocationLeft) + ..onPreventDefault = onPreventDefault + ); + expect(keyDataList, hasLength(1)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: kPhysicalShiftLeft, + logical: kLogicalShiftLeft, + character: null, + ); + expect(preventedDefault, true); + }); + + test('Duplicate ups are skipped', () { + final List keyDataList = []; + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + return true; + }); + bool preventedDefault = false; + final onPreventDefault = () { preventedDefault = true; }; + + // A KeyDown of ShiftRight is missed due to loss of focus. + converter.handleEvent(keyUpEvent('ShiftRight', 'Shift', 0, kLocationRight) + ..onPreventDefault = onPreventDefault + ); + expect(keyDataList, isEmpty); + expect(preventedDefault, false); + }); + + test('Conflict from multiple keyboards do not crash', () { + final List keyDataList = []; + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + return true; + }); + + // Same layout + converter.handleEvent(keyDownEvent('KeyA', 'a')); + converter.handleEvent(keyDownEvent('KeyA', 'a')); + converter.handleEvent(keyUpEvent('KeyA', 'a')); + converter.handleEvent(keyUpEvent('KeyA', 'a')); + + // Different layout + converter.handleEvent(keyDownEvent('KeyA', 'a')); + converter.handleEvent(keyDownEvent('KeyA', 'u')); + converter.handleEvent(keyUpEvent('KeyA', 'u')); + converter.handleEvent(keyUpEvent('KeyA', 'a')); + + // Passes if there's no crash, and states are reset after everything is released. + keyDataList.clear(); + converter.handleEvent(keyDownEvent('KeyA', 'a')); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: 'a', + ); + + converter.handleEvent(keyDownEvent('KeyU', 'u')); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalKeyU, + logical: kLogicalKeyU, + character: 'u', + ); + }); + + testFakeAsync('CapsLock down synthesizes an immediate cancel on macOS', (FakeAsync async) { + final List keyDataList = []; + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + return true; + }, onMacOs: true); + bool preventedDefault = false; + final onPreventDefault = () { preventedDefault = true; }; + + // A KeyDown of ShiftRight is missed due to loss of focus. + converter.handleEvent(keyDownEvent('CapsLock', 'CapsLock') + ..onPreventDefault = onPreventDefault + ); + expect(keyDataList, hasLength(1)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalCapsLock, + logical: kLogicalCapsLock, + character: null, + ); + expect(preventedDefault, true); + keyDataList.clear(); + preventedDefault = false; + + async.elapse(Duration(microseconds: 1)); + expect(keyDataList, hasLength(1)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: kPhysicalCapsLock, + logical: kLogicalCapsLock, + character: null, + synthesized: true, + ); + expect(preventedDefault, false); + keyDataList.clear(); + + converter.handleEvent(keyUpEvent('CapsLock', 'CapsLock') + ..onPreventDefault = onPreventDefault + ); + expect(keyDataList, hasLength(1)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalCapsLock, + logical: kLogicalCapsLock, + character: null, + ); + expect(preventedDefault, true); + keyDataList.clear(); + preventedDefault = false; + + async.elapse(Duration(microseconds: 1)); + expect(keyDataList, hasLength(1)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: kPhysicalCapsLock, + logical: kLogicalCapsLock, + character: null, + synthesized: true, + ); + expect(preventedDefault, false); + keyDataList.clear(); + + // Another key down works + converter.handleEvent(keyDownEvent('CapsLock', 'CapsLock')); + expect(keyDataList, hasLength(1)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalCapsLock, + logical: kLogicalCapsLock, + character: null, + ); + keyDataList.clear(); + + + // Schedules are canceled after disposal + converter.dispose(); + async.elapse(Duration(seconds: 10)); + expect(keyDataList, isEmpty); + }); + + testFakeAsync('CapsLock behaves normally on non-macOS', (FakeAsync async) { + final List keyDataList = []; + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + return true; + }, onMacOs: false); + + converter.handleEvent(keyDownEvent('CapsLock', 'CapsLock')); + expect(keyDataList, hasLength(1)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalCapsLock, + logical: kLogicalCapsLock, + character: null, + ); + keyDataList.clear(); + + async.elapse(Duration(seconds: 10)); + expect(keyDataList, isEmpty); + + converter.handleEvent(keyUpEvent('CapsLock', 'CapsLock')); + expect(keyDataList, hasLength(1)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: kPhysicalCapsLock, + logical: kLogicalCapsLock, + character: null, + ); + keyDataList.clear(); + + async.elapse(Duration(seconds: 10)); + expect(keyDataList, isEmpty); + + converter.handleEvent(keyDownEvent('CapsLock', 'CapsLock')); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalCapsLock, + logical: kLogicalCapsLock, + character: null, + ); + + converter.handleEvent(keyUpEvent('CapsLock', 'CapsLock')); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: kPhysicalCapsLock, + logical: kLogicalCapsLock, + character: null, + ); + }); + + testFakeAsync('Key guards: key down events are guarded', (FakeAsync async) { + final List keyDataList = []; + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + return true; + }); + + converter.handleEvent(keyDownEvent('MetaLeft', 'Meta', kMeta, kLocationLeft)..timeStamp = 100); + async.elapse(Duration(milliseconds: 100)); + + converter.handleEvent(keyDownEvent('KeyA', 'a', kMeta)..timeStamp = 200); + expectKeyData(keyDataList.last, + timeStamp: const Duration(milliseconds: 200), + type: ui.KeyEventType.down, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: 'a', + ); + keyDataList.clear(); + + // Keyup of KeyA is omitted due to being a shortcut. + + async.elapse(Duration(milliseconds: 2500)); + expectKeyData(keyDataList.last, + timeStamp: const Duration(milliseconds: 1200), + type: ui.KeyEventType.up, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: null, + synthesized: true, + ); + keyDataList.clear(); + + converter.handleEvent(keyUpEvent('MetaLeft', 'Meta', 0, kLocationLeft)..timeStamp = 2700); + expectKeyData(keyDataList.last, + timeStamp: const Duration(milliseconds: 2700), + type: ui.KeyEventType.up, + physical: kPhysicalMetaLeft, + logical: kLogicalMetaLeft, + character: null, + ); + async.elapse(Duration(milliseconds: 100)); + + // Key A states are cleared + converter.handleEvent(keyDownEvent('KeyA', 'a')..timeStamp = 2800); + expectKeyData(keyDataList.last, + timeStamp: const Duration(milliseconds: 2800), + type: ui.KeyEventType.down, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: 'a', + ); + async.elapse(Duration(milliseconds: 100)); + + converter.handleEvent(keyUpEvent('KeyA', 'a')..timeStamp = 2900); + expectKeyData(keyDataList.last, + timeStamp: const Duration(milliseconds: 2900), + type: ui.KeyEventType.up, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: null, + ); + }); + + testFakeAsync('Key guards: key repeated down events refreshes guards', (FakeAsync async) { + final List keyDataList = []; + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + return true; + }); + + converter.handleEvent(keyDownEvent('MetaLeft', 'Meta', kMeta, kLocationLeft)..timeStamp = 100); + async.elapse(Duration(milliseconds: 100)); + + converter.handleEvent(keyDownEvent('KeyA', 'a', kMeta)..timeStamp = 200); + async.elapse(Duration(milliseconds: 400)); + + converter.handleEvent(keyRepeatedDownEvent('KeyA', 'a', kMeta)..timeStamp = 600); + async.elapse(Duration(milliseconds: 50)); + converter.handleEvent(keyRepeatedDownEvent('KeyA', 'a', kMeta)..timeStamp = 650); + async.elapse(Duration(milliseconds: 50)); + converter.handleEvent(keyRepeatedDownEvent('KeyA', 'a', kMeta)..timeStamp = 700); + + // Keyup of KeyA is omitted due to being a shortcut. + + async.elapse(Duration(milliseconds: 2500)); + expectKeyData(keyDataList.last, + timeStamp: const Duration(milliseconds: 1700), + type: ui.KeyEventType.up, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: null, + synthesized: true, + ); + keyDataList.clear(); + + converter.handleEvent(keyUpEvent('MetaLeft', 'Meta', 0, kLocationLeft)..timeStamp = 3200); + expectKeyData(keyDataList.last, + timeStamp: const Duration(milliseconds: 3200), + type: ui.KeyEventType.up, + physical: kPhysicalMetaLeft, + logical: kLogicalMetaLeft, + character: null, + ); + async.elapse(Duration(milliseconds: 100)); + + // Key A states are cleared + converter.handleEvent(keyDownEvent('KeyA', 'a')..timeStamp = 3300); + expectKeyData(keyDataList.last, + timeStamp: const Duration(milliseconds: 3300), + type: ui.KeyEventType.down, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: 'a', + ); + async.elapse(Duration(milliseconds: 100)); + + converter.handleEvent(keyUpEvent('KeyA', 'a')..timeStamp = 3400); + expectKeyData(keyDataList.last, + timeStamp: const Duration(milliseconds: 3400), + type: ui.KeyEventType.up, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: null, + ); + }); + + testFakeAsync('Key guards: cleared by keyups', (FakeAsync async) { + final List keyDataList = []; + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + return true; + }); + + converter.handleEvent(keyDownEvent('MetaLeft', 'Meta', kMeta, kLocationLeft)..timeStamp = 100); + async.elapse(Duration(milliseconds: 100)); + + converter.handleEvent(keyDownEvent('KeyA', 'a', kCtrl)..timeStamp = 200); + expectKeyData(keyDataList.last, + timeStamp: const Duration(milliseconds: 200), + type: ui.KeyEventType.down, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: 'a', + ); + keyDataList.clear(); + async.elapse(Duration(milliseconds: 500)); + + converter.handleEvent(keyUpEvent('MetaLeft', 'Meta', 0, kLocationLeft)..timeStamp = 700); + async.elapse(Duration(milliseconds: 100)); + + converter.handleEvent(keyUpEvent('KeyA', 'a')..timeStamp = 800); + expectKeyData(keyDataList.last, + timeStamp: const Duration(milliseconds: 800), + type: ui.KeyEventType.up, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: null, + ); + keyDataList.clear(); + async.elapse(Duration(milliseconds: 2000)); + expect(keyDataList, isEmpty); + + // Key A states are cleared + converter.handleEvent(keyDownEvent('KeyA', 'a')..timeStamp = 2800); + expectKeyData(keyDataList.last, + timeStamp: const Duration(milliseconds: 2800), + type: ui.KeyEventType.down, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: 'a', + ); + async.elapse(Duration(milliseconds: 100)); + + converter.handleEvent(keyUpEvent('KeyA', 'a')..timeStamp = 2900); + expectKeyData(keyDataList.last, + timeStamp: const Duration(milliseconds: 2900), + type: ui.KeyEventType.up, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: null, + ); + }); + + testFakeAsync('Lock flags of other keys', (FakeAsync async) { + final List keyDataList = []; + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + return true; + }, onMacOs: false); + + converter.handleEvent(keyDownEvent('ScrollLock', 'ScrollLock')); + expect(keyDataList, hasLength(1)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalScrollLock, + logical: kLogicalScrollLock, + character: null, + ); + keyDataList.clear(); + + async.elapse(Duration(seconds: 10)); + expect(keyDataList, isEmpty); + + converter.handleEvent(keyUpEvent('ScrollLock', 'ScrollLock')); + expect(keyDataList, hasLength(1)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: kPhysicalScrollLock, + logical: kLogicalScrollLock, + character: null, + ); + keyDataList.clear(); + + converter.handleEvent(keyDownEvent('ScrollLock', 'ScrollLock')); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalScrollLock, + logical: kLogicalScrollLock, + character: null, + ); + + converter.handleEvent(keyUpEvent('ScrollLock', 'ScrollLock')); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.up, + physical: kPhysicalScrollLock, + logical: kLogicalScrollLock, + character: null, + ); + }); + + test('Deduce modifier key up from modifier field', () { + final List keyDataList = []; + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + return true; + }, onMacOs: false); + + converter.handleEvent(keyDownEvent('ShiftRight', 'Shift', kShift, kLocationRight)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalShiftRight, + logical: kLogicalShiftRight, + character: null, + ); + + converter.handleEvent(keyDownEvent('ShiftLeft', 'Shift', kShift, kLocationLeft)); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalShiftLeft, + logical: kLogicalShiftLeft, + character: null, + ); + keyDataList.clear(); + + // The release of the shift keys are omitted + + converter.handleEvent(keyDownEvent('KeyA', 'a')); + expect(keyDataList, hasLength(3)); + expectKeyData(keyDataList[0], + type: ui.KeyEventType.up, + physical: kPhysicalShiftLeft, + logical: kLogicalShiftLeft, + character: null, + synthesized: true, + ); + expectKeyData(keyDataList[1], + type: ui.KeyEventType.up, + physical: kPhysicalShiftRight, + logical: kLogicalShiftRight, + character: null, + synthesized: true, + ); + expectKeyData(keyDataList.last, + type: ui.KeyEventType.down, + physical: kPhysicalKeyA, + logical: kLogicalKeyA, + character: 'a', + ); + }); +} + +class MockKeyboardEvent implements FlutterHtmlKeyboardEvent { + MockKeyboardEvent({ + required this.type, + required this.code, + required this.key, + this.timeStamp = 0, + this.repeat = false, + this.altKey = false, + this.ctrlKey = false, + this.shiftKey = false, + this.metaKey = false, + this.location = 0, + this.onPreventDefault, + }); + + String type; + String? code; + String? key; + bool? repeat; + num? timeStamp; + bool altKey; + bool ctrlKey; + bool shiftKey; + bool metaKey; + int? location; + + bool getModifierState(String key) => modifierState.contains(key); + final Set modifierState = {}; + + void preventDefault() { onPreventDefault?.call(); } + VoidCallback? onPreventDefault; +} + +// Flags used for the `modifiers` argument of `key***Event` functions. +const kAlt = 0x1; +const kCtrl = 0x2; +const kShift = 0x4; +const kMeta = 0x8; + +// Utility functions to make code more concise. +// +// To add timeStamp or onPreventDefault, use syntax like `..timeStamp = `. +MockKeyboardEvent keyDownEvent(String code, String key, [int modifiers = 0, int location = 0]) { + return MockKeyboardEvent( + type: 'keydown', + code: code, + key: key, + altKey: modifiers & kAlt != 0, + ctrlKey: modifiers & kCtrl != 0, + shiftKey: modifiers & kShift != 0, + metaKey: modifiers & kMeta != 0, + location: location, + ); +} + +MockKeyboardEvent keyUpEvent(String code, String key, [int modifiers = 0, int location = 0]) { + return MockKeyboardEvent( + type: 'keyup', + code: code, + key: key, + altKey: modifiers & kAlt != 0, + ctrlKey: modifiers & kCtrl != 0, + shiftKey: modifiers & kShift != 0, + metaKey: modifiers & kMeta != 0, + location: location, + ); +} + +MockKeyboardEvent keyRepeatedDownEvent(String code, String key, [int modifiers = 0, int location = 0]) { + return MockKeyboardEvent( + type: 'keydown', + code: code, + key: key, + altKey: modifiers & kAlt != 0, + ctrlKey: modifiers & kCtrl != 0, + shiftKey: modifiers & kShift != 0, + metaKey: modifiers & kMeta != 0, + repeat: true, + location: location, + ); +} + +// Flags used for the `activeLocks` argument of expectKeyData. +const kCapsLock = 0x1; +const kNumlLock = 0x2; +const kScrollLock = 0x4; + +void expectKeyData( + ui.KeyData target, { + required ui.KeyEventType type, + required int physical, + required int logical, + required String? character, + Duration? timeStamp, + bool synthesized = false, +}) { + expect(target.type, type); + expect(target.physical, physical); + expect(target.logical, logical); + expect(target.character, character); + expect(target.synthesized, synthesized); + if (timeStamp != null) + expect(target.timeStamp, equals(timeStamp)); +} + +typedef FakeAsyncTest = void Function(FakeAsync); + +@isTest +void testFakeAsync(String description, FakeAsyncTest fn) { + test(description, () { + FakeAsync().run(fn); + }); +} diff --git a/runtime/runtime_controller.cc b/runtime/runtime_controller.cc index 42a6ea6bc..33b977a7d 100644 --- a/runtime/runtime_controller.cc +++ b/runtime/runtime_controller.cc @@ -262,6 +262,20 @@ bool RuntimeController::DispatchPointerDataPacket( return false; } +bool RuntimeController::DispatchKeyDataPacket(const KeyDataPacket& packet, + KeyDataResponse callback) { + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + TRACE_EVENT1("flutter", "RuntimeController::DispatchKeyDataPacket", "mode", + "basic"); + uint64_t response_id = + platform_configuration->RegisterKeyDataResponse(std::move(callback)); + platform_configuration->get_window(0)->DispatchKeyDataPacket(packet, + response_id); + return true; + } + return false; +} + bool RuntimeController::DispatchSemanticsAction(int32_t id, SemanticsAction action, std::vector args) { diff --git a/runtime/runtime_controller.h b/runtime/runtime_controller.h index 09ff67781..6793b1a32 100644 --- a/runtime/runtime_controller.h +++ b/runtime/runtime_controller.h @@ -424,6 +424,20 @@ class RuntimeController : public PlatformConfigurationClient { /// bool DispatchPointerDataPacket(const PointerDataPacket& packet); + //---------------------------------------------------------------------------- + /// @brief Dispatch the specified pointer data message to the running + /// root isolate. + /// + /// @param[in] packet The key data message to dispatch to the isolate. + /// @param[in] callback Called when the framework has decided whether + /// to handle this key data. + /// + /// @return If the key data message was dispatched. This may fail is + /// an isolate is not running. + /// + bool DispatchKeyDataPacket(const KeyDataPacket& packet, + KeyDataResponse callback); + //---------------------------------------------------------------------------- /// @brief Dispatch the semantics action to the specified accessibility /// node. diff --git a/shell/common/engine.cc b/shell/common/engine.cc index c25511da1..669a7cf87 100644 --- a/shell/common/engine.cc +++ b/shell/common/engine.cc @@ -415,6 +415,14 @@ void Engine::DispatchPointerDataPacket( pointer_data_dispatcher_->DispatchPacket(std::move(packet), trace_flow_id); } +void Engine::DispatchKeyDataPacket(std::unique_ptr packet, + KeyDataResponse callback) { + TRACE_EVENT0("flutter", "Engine::DispatchKeyDataPacket"); + if (runtime_controller_) { + runtime_controller_->DispatchKeyDataPacket(*packet, std::move(callback)); + } +} + void Engine::DispatchSemanticsAction(int id, SemanticsAction action, std::vector args) { diff --git a/shell/common/engine.h b/shell/common/engine.h index a5581611e..4249ceb2e 100644 --- a/shell/common/engine.h +++ b/shell/common/engine.h @@ -728,6 +728,21 @@ class Engine final : public RuntimeDelegate, void DispatchPointerDataPacket(std::unique_ptr packet, uint64_t trace_flow_id); + //---------------------------------------------------------------------------- + /// @brief Notifies the engine that the embedder has sent it a key data + /// packet. A key data packet contains one key event. This call + /// originates in the platform view and the shell has forwarded + /// the same to the engine on the UI task runner here. The engine + /// will decide whether to handle this event, and send the + /// result using `callback`, which will be called exactly once. + /// + /// @param[in] packet The key data packet. + /// @param[in] callback Called when the framework has decided whether + /// to handle this key data. + /// + void DispatchKeyDataPacket(std::unique_ptr packet, + KeyDataResponse callback); + //---------------------------------------------------------------------------- /// @brief Notifies the engine that the embedder encountered an /// accessibility related action on the specified node. This call diff --git a/shell/common/platform_view.cc b/shell/common/platform_view.cc index 22c2b395b..27a398e5c 100644 --- a/shell/common/platform_view.cc +++ b/shell/common/platform_view.cc @@ -42,6 +42,12 @@ void PlatformView::DispatchPointerDataPacket( pointer_data_packet_converter_.Convert(std::move(packet))); } +void PlatformView::DispatchKeyDataPacket(std::unique_ptr packet, + KeyDataResponse callback) { + delegate_.OnPlatformViewDispatchKeyDataPacket(std::move(packet), + std::move(callback)); +} + void PlatformView::DispatchSemanticsAction(int32_t id, SemanticsAction action, std::vector args) { diff --git a/shell/common/platform_view.h b/shell/common/platform_view.h index 92e670677..ea7588370 100644 --- a/shell/common/platform_view.h +++ b/shell/common/platform_view.h @@ -5,6 +5,7 @@ #ifndef COMMON_PLATFORM_VIEW_H_ #define COMMON_PLATFORM_VIEW_H_ +#include #include #include "flow/embedded_views.h" @@ -16,6 +17,7 @@ #include "flutter/fml/memory/weak_ptr.h" #include "flutter/lib/ui/semantics/custom_accessibility_action.h" #include "flutter/lib/ui/semantics/semantics_node.h" +#include "flutter/lib/ui/window/key_data_packet.h" #include "flutter/lib/ui/window/platform_message.h" #include "flutter/lib/ui/window/pointer_data_packet.h" #include "flutter/lib/ui/window/pointer_data_packet_converter.h" @@ -52,6 +54,7 @@ class PlatformView { /// class Delegate { public: + using KeyDataResponse = std::function; //-------------------------------------------------------------------------- /// @brief Notifies the delegate that the platform view was created /// with the given render surface. This surface is platform @@ -125,6 +128,20 @@ class PlatformView { virtual void OnPlatformViewDispatchPointerDataPacket( std::unique_ptr packet) = 0; + //-------------------------------------------------------------------------- + /// @brief Notifies the delegate that the platform view has encountered + /// a key event. This key event and the callback needs to be + /// forwarded to the running root isolate hosted by the engine + /// on the UI thread. + /// + /// @param[in] packet The key data packet containing one key event. + /// @param[in] callback Called when the framework has decided whether + /// to handle this key data. + /// + virtual void OnPlatformViewDispatchKeyDataPacket( + std::unique_ptr packet, + std::function callback) = 0; + //-------------------------------------------------------------------------- /// @brief Notifies the delegate that the platform view has encountered /// an accessibility related action on the specified node. This @@ -575,6 +592,17 @@ class PlatformView { /// void DispatchPointerDataPacket(std::unique_ptr packet); + //---------------------------------------------------------------------------- + /// @brief Dispatches key events from the embedder to the framework. Each + /// key data packet contains one physical event and multiple + /// logical key events. Each call to this method wakes up the UI + /// thread. + /// + /// @param[in] packet The key data packet to dispatch to the framework. + /// + void DispatchKeyDataPacket(std::unique_ptr packet, + Delegate::KeyDataResponse callback); + //-------------------------------------------------------------------------- /// @brief Used by the embedder to specify a texture that it wants the /// rasterizer to composite within the Flutter layer tree. All diff --git a/shell/common/shell.cc b/shell/common/shell.cc index 435138d7c..fbf1e22cc 100644 --- a/shell/common/shell.cc +++ b/shell/common/shell.cc @@ -961,6 +961,23 @@ void Shell::OnPlatformViewDispatchPointerDataPacket( next_pointer_flow_id_++; } +// |PlatformView::Delegate| +void Shell::OnPlatformViewDispatchKeyDataPacket( + std::unique_ptr packet, + std::function callback) { + TRACE_EVENT0("flutter", "Shell::OnPlatformViewDispatchKeyDataPacket"); + FML_DCHECK(is_setup_); + FML_DCHECK(task_runners_.GetPlatformTaskRunner()->RunsTasksOnCurrentThread()); + + task_runners_.GetUITaskRunner()->PostTask( + fml::MakeCopyable([engine = weak_engine_, packet = std::move(packet), + callback = std::move(callback)]() mutable { + if (engine) { + engine->DispatchKeyDataPacket(std::move(packet), std::move(callback)); + } + })); +} + // |PlatformView::Delegate| void Shell::OnPlatformViewDispatchSemanticsAction(int32_t id, SemanticsAction action, diff --git a/shell/common/shell.h b/shell/common/shell.h index 3749d4727..fab937891 100644 --- a/shell/common/shell.h +++ b/shell/common/shell.h @@ -504,6 +504,11 @@ class Shell final : public PlatformView::Delegate, void OnPlatformViewDispatchPointerDataPacket( std::unique_ptr packet) override; + // |PlatformView::Delegate| + void OnPlatformViewDispatchKeyDataPacket( + std::unique_ptr packet, + std::function callback) override; + // |PlatformView::Delegate| void OnPlatformViewDispatchSemanticsAction( int32_t id, diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index aca67acd5..0b9a98f21 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -62,6 +62,10 @@ class MockPlatformViewDelegate : public PlatformView::Delegate { MOCK_METHOD1(OnPlatformViewDispatchPointerDataPacket, void(std::unique_ptr packet)); + MOCK_METHOD2(OnPlatformViewDispatchKeyDataPacket, + void(std::unique_ptr packet, + KeyDataResponse callback)); + MOCK_METHOD3(OnPlatformViewDispatchSemanticsAction, void(int32_t id, SemanticsAction action, diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm index c81910f89..53d868807 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm @@ -25,6 +25,8 @@ class MockDelegate : public PlatformView::Delegate { void OnPlatformViewDispatchPlatformMessage(fml::RefPtr message) override {} void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr packet) override { } + void OnPlatformViewDispatchKeyDataPacket(std::unique_ptr packet, + std::function callback) override {} void OnPlatformViewDispatchSemanticsAction(int32_t id, SemanticsAction action, std::vector args) override {} diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 8f4b3e7a9..c826a2e97 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -95,6 +95,8 @@ class FlutterPlatformViewsTestMockPlatformViewDelegate : public PlatformView::De void OnPlatformViewDispatchPlatformMessage(fml::RefPtr message) override {} void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr packet) override { } + void OnPlatformViewDispatchKeyDataPacket(std::unique_ptr packet, + std::function callback) override {} void OnPlatformViewDispatchSemanticsAction(int32_t id, SemanticsAction action, std::vector args) override {} diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm index 08f01b7c7..41f8072af 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm @@ -78,6 +78,8 @@ class MockDelegate : public PlatformView::Delegate { void OnPlatformViewDispatchPlatformMessage(fml::RefPtr message) override {} void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr packet) override { } + void OnPlatformViewDispatchKeyDataPacket(std::unique_ptr packet, + std::function callback) override {} void OnPlatformViewDispatchSemanticsAction(int32_t id, SemanticsAction action, std::vector args) override {} diff --git a/shell/platform/embedder/embedder.cc b/shell/platform/embedder/embedder.cc index b2b9f26a5..00fd7633a 100644 --- a/shell/platform/embedder/embedder.cc +++ b/shell/platform/embedder/embedder.cc @@ -1492,6 +1492,58 @@ FlutterEngineResult FlutterEngineSendPointerEvent( "running Flutter application."); } +static inline flutter::KeyEventType MapKeyEventType( + FlutterKeyEventType event_kind) { + switch (event_kind) { + case kFlutterKeyEventTypeUp: + return flutter::KeyEventType::kUp; + case kFlutterKeyEventTypeDown: + return flutter::KeyEventType::kDown; + case kFlutterKeyEventTypeRepeat: + return flutter::KeyEventType::kRepeat; + } + return flutter::KeyEventType::kUp; +} + +FlutterEngineResult FlutterEngineSendKeyEvent(FLUTTER_API_SYMBOL(FlutterEngine) + engine, + const FlutterKeyEvent* event, + FlutterKeyEventCallback callback, + void* user_data) { + if (engine == nullptr) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, "Engine handle was invalid."); + } + + if (event == nullptr) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, "Invalid key event."); + } + + const char* character = SAFE_ACCESS(event, character, nullptr); + + flutter::KeyData key_data; + key_data.Clear(); + key_data.timestamp = (uint64_t)SAFE_ACCESS(event, timestamp, 0); + key_data.type = MapKeyEventType( + SAFE_ACCESS(event, type, FlutterKeyEventType::kFlutterKeyEventTypeUp)); + key_data.physical = SAFE_ACCESS(event, physical, 0); + key_data.logical = SAFE_ACCESS(event, logical, 0); + key_data.synthesized = SAFE_ACCESS(event, synthesized, false); + + auto packet = std::make_unique(key_data, character); + + auto response = [callback, user_data](bool handled) { + if (callback != nullptr) + callback(handled, user_data); + }; + + return reinterpret_cast(engine) + ->DispatchKeyDataPacket(std::move(packet), response) + ? kSuccess + : LOG_EMBEDDER_ERROR(kInternalInconsistency, + "Could not dispatch the key event to the " + "running Flutter application."); +} + FlutterEngineResult FlutterEngineSendPlatformMessage( FLUTTER_API_SYMBOL(FlutterEngine) engine, const FlutterPlatformMessage* flutter_message) { @@ -2166,6 +2218,7 @@ FlutterEngineResult FlutterEngineGetProcAddresses( SET_PROC(RunInitialized, FlutterEngineRunInitialized); SET_PROC(SendWindowMetricsEvent, FlutterEngineSendWindowMetricsEvent); SET_PROC(SendPointerEvent, FlutterEngineSendPointerEvent); + SET_PROC(SendKeyEvent, FlutterEngineSendKeyEvent); SET_PROC(SendPlatformMessage, FlutterEngineSendPlatformMessage); SET_PROC(PlatformMessageCreateResponseHandle, FlutterPlatformMessageCreateResponseHandle); diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index 712286963..6489edcf9 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -611,6 +611,66 @@ typedef struct { int64_t buttons; } FlutterPointerEvent; +typedef enum { + kFlutterKeyEventTypeUp = 1, + kFlutterKeyEventTypeDown, + kFlutterKeyEventTypeRepeat, +} FlutterKeyEventType; + +/// A structure to represent a key event. +/// +/// Sending `FlutterKeyEvent` via `FlutterEngineSendKeyEvent` results in a +/// corresponding `FlutterKeyEvent` to be dispatched in the framework. It is +/// embedder's responsibility to ensure the regularity of sent events, since the +/// framework only performs simple one-to-one mapping. The events must conform +/// the following rules: +/// +/// * Each key press sequence shall consist of one key down event (`kind` being +/// `kFlutterKeyEventTypeDown`), zero or more repeat events, and one key up +/// event, representing a physical key button being pressed, held, and +/// released. +/// * All events throughout a key press sequence shall have the same `physical` +/// and `logical`. Having different `character`s is allowed. +typedef struct { + /// The size of this struct. Must be sizeof(FlutterKeyEvent). + size_t struct_size; + /// The timestamp at which the key event was generated. The timestamp should + /// be specified in microseconds and the clock should be the same as that used + /// by `FlutterEngineGetCurrentTime`. + double timestamp; + /// The event kind. + FlutterKeyEventType type; + /// The USB HID code for the physical key of the event. + /// + /// For the full definition and list of pre-defined physical keys, see + /// `PhysicalKeyboardKey` from the framework. + uint64_t physical; + /// The key ID for the logical key of this event. + /// + /// For the full definition and a list of pre-defined logical keys, see + /// `LogicalKeyboardKey` from the framework. + uint64_t logical; + /// Null-terminated character input from the event. Can be null. Ignored for + /// up events. + const char* character; + /// True if this event does not correspond to a native event. + /// + /// The embedder is likely to skip events and/or construct new events that do + /// not correspond to any native events in order to conform the regularity + /// of events (as documented in `FlutterKeyEvent`). An example is when a key + /// up is missed due to loss of window focus, on a platform that provides + /// query to key pressing status, the embedder might realize that the key has + /// been released at the next key event, and should construct a synthesized up + /// event immediately before the actual event. + /// + /// An event being synthesized means that the `timestamp` might greatly + /// deviate from the actual time when the event occurs physically. + bool synthesized; +} FlutterKeyEvent; + +typedef void (*FlutterKeyEventCallback)(bool /* handled */, + void* /* user_data */); + struct _FlutterPlatformMessageResponseHandle; typedef struct _FlutterPlatformMessageResponseHandle FlutterPlatformMessageResponseHandle; @@ -1549,6 +1609,32 @@ FlutterEngineResult FlutterEngineSendPointerEvent( const FlutterPointerEvent* events, size_t events_count); +//------------------------------------------------------------------------------ +/// @brief Sends a key event to the engine. The framework will decide +/// whether to handle this event in a synchronous fashion, although +/// due to technical limitation, the result is always reported +/// asynchronously. The `callback` is guaranteed to be called +/// exactly once. +/// +/// @param[in] engine A running engine instance. +/// @param[in] event The event data to be sent. This function will no +/// longer access `event` after returning. +/// @param[in] callback The callback invoked by the engine when the +/// Flutter application has decided whether it +/// handles this event. Accepts nullptr. +/// @param[in] user_data The context associated with the callback. The +/// exact same value will used to invoke `callback`. +/// Accepts nullptr. +/// +/// @return The result of the call. +/// +FLUTTER_EXPORT +FlutterEngineResult FlutterEngineSendKeyEvent(FLUTTER_API_SYMBOL(FlutterEngine) + engine, + const FlutterKeyEvent* event, + FlutterKeyEventCallback callback, + void* user_data); + FLUTTER_EXPORT FlutterEngineResult FlutterEngineSendPlatformMessage( FLUTTER_API_SYMBOL(FlutterEngine) engine, @@ -2062,6 +2148,11 @@ typedef FlutterEngineResult (*FlutterEngineSendPointerEventFnPtr)( FLUTTER_API_SYMBOL(FlutterEngine) engine, const FlutterPointerEvent* events, size_t events_count); +typedef FlutterEngineResult (*FlutterEngineSendKeyEventFnPtr)( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + const FlutterKeyEvent* event, + FlutterKeyEventCallback callback, + void* user_data); typedef FlutterEngineResult (*FlutterEngineSendPlatformMessageFnPtr)( FLUTTER_API_SYMBOL(FlutterEngine) engine, const FlutterPlatformMessage* message); @@ -2155,6 +2246,7 @@ typedef struct { FlutterEngineRunInitializedFnPtr RunInitialized; FlutterEngineSendWindowMetricsEventFnPtr SendWindowMetricsEvent; FlutterEngineSendPointerEventFnPtr SendPointerEvent; + FlutterEngineSendKeyEventFnPtr SendKeyEvent; FlutterEngineSendPlatformMessageFnPtr SendPlatformMessage; FlutterEnginePlatformMessageCreateResponseHandleFnPtr PlatformMessageCreateResponseHandle; diff --git a/shell/platform/embedder/embedder_engine.cc b/shell/platform/embedder/embedder_engine.cc index bb0aa08bb..c1e40cc41 100644 --- a/shell/platform/embedder/embedder_engine.cc +++ b/shell/platform/embedder/embedder_engine.cc @@ -136,6 +136,22 @@ bool EmbedderEngine::DispatchPointerDataPacket( return true; } +bool EmbedderEngine::DispatchKeyDataPacket( + std::unique_ptr packet, + KeyDataResponse callback) { + if (!IsValid() || !packet) { + return false; + } + + auto platform_view = shell_->GetPlatformView(); + if (!platform_view) { + return false; + } + + platform_view->DispatchKeyDataPacket(std::move(packet), std::move(callback)); + return true; +} + bool EmbedderEngine::SendPlatformMessage( fml::RefPtr message) { if (!IsValid() || !message) { diff --git a/shell/platform/embedder/embedder_engine.h b/shell/platform/embedder/embedder_engine.h index 4f38ba441..55afebdba 100644 --- a/shell/platform/embedder/embedder_engine.h +++ b/shell/platform/embedder/embedder_engine.h @@ -60,6 +60,22 @@ class EmbedderEngine { bool DispatchPointerDataPacket( std::unique_ptr packet); + //---------------------------------------------------------------------------- + /// @brief Notifies the platform view that the embedder has sent it a key + /// data packet. A key data packet contains one key event. This + /// call originates in the platform view and the shell has + /// forwarded the same to the engine on the UI task runner here. + /// The platform view will decide whether to handle this event, + /// and send the result using `callback`, which will be called + /// exactly once. + /// + /// @param[in] packet The key data packet. + /// @param[in] callback Called when the framework has decided whether + /// to handle this key data. + /// + bool DispatchKeyDataPacket(std::unique_ptr packet, + KeyDataResponse callback); + bool SendPlatformMessage(fml::RefPtr message); bool RegisterTexture(int64_t texture); diff --git a/shell/platform/embedder/fixtures/main.dart b/shell/platform/embedder/fixtures/main.dart index 81fc2e0c3..9d0458af6 100644 --- a/shell/platform/embedder/fixtures/main.dart +++ b/shell/platform/embedder/fixtures/main.dart @@ -495,6 +495,47 @@ Picture CreateGradientBox(Size size) { return baseRecorder.endRecording(); } +void _echoKeyEvent( + int change, + int timestamp, + int physical, + int logical, + int charCode, + bool synthesized) + native 'EchoKeyEvent'; + +// Convert `kind` in enum form to its integer form. +// +// It performs a revesed mapping from `unserializeKeyEventKind` +// in shell/platform/embedder/tests/embedder_unittests.cc. +int _serializeKeyEventType(KeyEventType change) { + switch(change) { + case KeyEventType.up: + return 1; + case KeyEventType.down: + return 2; + case KeyEventType.repeat: + return 3; + } +} + +// Echo the event data with `_echoKeyEvent`, and returns synthesized as handled. +@pragma('vm:entry-point') +void key_data_echo() async { // ignore: non_constant_identifier_names + PlatformDispatcher.instance.onKeyData = (KeyData data) { + _echoKeyEvent( + _serializeKeyEventType(data.type), + data.timeStamp.inMicroseconds, + data.physical, + data.logical, + data.character == null ? 0 : data.character!.codeUnitAt(0), + data.synthesized, + ); + return data.synthesized; + }; + signalNativeTest(); +} + @pragma('vm:entry-point') void render_gradient() { PlatformDispatcher.instance.onBeginFrame = (Duration duration) { diff --git a/shell/platform/embedder/tests/embedder_unittests.cc b/shell/platform/embedder/tests/embedder_unittests.cc index 469b20bce..64b3bfed8 100644 --- a/shell/platform/embedder/tests/embedder_unittests.cc +++ b/shell/platform/embedder/tests/embedder_unittests.cc @@ -1191,5 +1191,208 @@ TEST_F(EmbedderTest, CanLaunchAndShutdownWithAValidElfSource) { engine.reset(); } +//------------------------------------------------------------------------------ +// Key Data +//------------------------------------------------------------------------------ + +typedef struct { + std::shared_ptr latch; + bool returned; +} KeyEventUserData; + +// Convert `kind` in integer form to its enum form. +// +// It performs a revesed mapping from `_serializeKeyEventType` +// in shell/platform/embedder/fixtures/main.dart. +FlutterKeyEventType UnserializeKeyEventKind(uint64_t kind) { + switch (kind) { + case 1: + return kFlutterKeyEventTypeUp; + case 2: + return kFlutterKeyEventTypeDown; + case 3: + return kFlutterKeyEventTypeRepeat; + default: + FML_UNREACHABLE(); + return kFlutterKeyEventTypeUp; + } +} + +// Checks the equality of two `FlutterKeyEvent` by each of their members except +// for `character`. The `character` must be checked separately. +void ExpectKeyEventEq(const FlutterKeyEvent& subject, + const FlutterKeyEvent& baseline) { + EXPECT_EQ(subject.timestamp, baseline.timestamp); + EXPECT_EQ(subject.type, baseline.type); + EXPECT_EQ(subject.physical, baseline.physical); + EXPECT_EQ(subject.logical, baseline.logical); + EXPECT_EQ(subject.synthesized, baseline.synthesized); +} + +TEST_F(EmbedderTest, KeyDataIsCorrectlySerialized) { + auto message_latch = std::make_shared(); + uint64_t echoed_char; + FlutterKeyEvent echoed_event; + + auto native_echo_event = [&](Dart_NativeArguments args) { + echoed_event.type = + UnserializeKeyEventKind(tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 0))); + echoed_event.timestamp = tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 1)); + echoed_event.physical = tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 2)); + echoed_event.logical = tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 3)); + echoed_char = tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 4)); + echoed_event.synthesized = + tonic::DartConverter::FromDart(Dart_GetNativeArgument(args, 5)); + + message_latch->Signal(); + }; + + auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); + builder.SetDartEntrypoint("key_data_echo"); + fml::AutoResetWaitableEvent ready; + context.AddNativeCallback( + "SignalNativeTest", + CREATE_NATIVE_ENTRY( + [&ready](Dart_NativeArguments args) { ready.Signal(); })); + + context.AddNativeCallback("EchoKeyEvent", + CREATE_NATIVE_ENTRY(native_echo_event)); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + ready.Wait(); + + // A normal down event + const FlutterKeyEvent down_event_upper_a{ + .struct_size = sizeof(FlutterKeyEvent), + .timestamp = 1, + .type = kFlutterKeyEventTypeDown, + .physical = 0x00070004, + .logical = 0x00000000061, + .character = "A", + .synthesized = false, + }; + FlutterEngineSendKeyEvent( + engine.get(), &down_event_upper_a, [](bool handled, void* user_data) {}, + nullptr); + message_latch->Wait(); + + ExpectKeyEventEq(echoed_event, down_event_upper_a); + EXPECT_EQ(echoed_char, 0x41llu); + + // A repeat event with multi-byte character + const FlutterKeyEvent repeat_event_wide_char{ + .struct_size = sizeof(FlutterKeyEvent), + .timestamp = 1000, + .type = kFlutterKeyEventTypeRepeat, + .physical = 0x00070005, + .logical = 0x00000000062, + .character = "∆", + .synthesized = false, + }; + FlutterEngineSendKeyEvent( + engine.get(), &repeat_event_wide_char, + [](bool handled, void* user_data) {}, nullptr); + message_latch->Wait(); + + ExpectKeyEventEq(echoed_event, repeat_event_wide_char); + EXPECT_EQ(echoed_char, 0x2206llu); + + // An up event with no character, synthesized + const FlutterKeyEvent up_event{ + .struct_size = sizeof(FlutterKeyEvent), + .timestamp = 1000000, + .type = kFlutterKeyEventTypeUp, + .physical = 0x00070006, + .logical = 0x00000000063, + .character = nullptr, + .synthesized = true, + }; + FlutterEngineSendKeyEvent( + engine.get(), &up_event, [](bool handled, void* user_data) {}, nullptr); + message_latch->Wait(); + + ExpectKeyEventEq(echoed_event, up_event); + EXPECT_EQ(echoed_char, 0llu); +} + +TEST_F(EmbedderTest, KeyDataResponseIsCorrectlyInvoked) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); + builder.SetDartEntrypoint("key_data_echo"); + fml::AutoResetWaitableEvent ready; + context.AddNativeCallback( + "SignalNativeTest", + CREATE_NATIVE_ENTRY( + [&ready](Dart_NativeArguments args) { ready.Signal(); })); + + context.AddNativeCallback( + "EchoKeyEvent", CREATE_NATIVE_ENTRY([](Dart_NativeArguments args) {})); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + ready.Wait(); + + // Dispatch a single event + FlutterKeyEvent event{ + .struct_size = sizeof(FlutterKeyEvent), + .timestamp = 1000, + .type = kFlutterKeyEventTypeDown, + .physical = 0x00070005, + .logical = 0x00000000062, + .character = nullptr, + }; + + KeyEventUserData user_data1{ + .latch = std::make_shared(), + }; + // Entrypoint `key_data_echo` uses `event.synthesized` as `handled`. + event.synthesized = true; + FlutterEngineSendKeyEvent( + engine.get(), &event, + [](bool handled, void* untyped_user_data) { + KeyEventUserData* user_data = + reinterpret_cast(untyped_user_data); + EXPECT_EQ(handled, true); + user_data->latch->Signal(); + }, + &user_data1); + user_data1.latch->Wait(); + + // Dispatch two events back to back, using the same callback on different + // user_data + KeyEventUserData user_data2{ + .latch = std::make_shared(), + .returned = false, + }; + KeyEventUserData user_data3{ + .latch = std::make_shared(), + .returned = false, + }; + auto callback23 = [](bool handled, void* untyped_user_data) { + KeyEventUserData* user_data = + reinterpret_cast(untyped_user_data); + EXPECT_EQ(handled, false); + user_data->returned = true; + user_data->latch->Signal(); + }; + + event.synthesized = false; + FlutterEngineSendKeyEvent(engine.get(), &event, callback23, &user_data2); + FlutterEngineSendKeyEvent(engine.get(), &event, callback23, &user_data3); + user_data2.latch->Wait(); + user_data3.latch->Wait(); + EXPECT_TRUE(user_data2.returned); + EXPECT_TRUE(user_data3.returned); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/fuchsia/flutter/platform_view_unittest.cc b/shell/platform/fuchsia/flutter/platform_view_unittest.cc index 628eefc7d..608c133a2 100644 --- a/shell/platform/fuchsia/flutter/platform_view_unittest.cc +++ b/shell/platform/fuchsia/flutter/platform_view_unittest.cc @@ -81,6 +81,10 @@ class MockPlatformViewDelegate : public flutter::PlatformView::Delegate { void OnPlatformViewDispatchPointerDataPacket( std::unique_ptr packet) {} // |flutter::PlatformView::Delegate| + void OnPlatformViewDispatchKeyDataPacket( + std::unique_ptr packet, + std::function callback) {} + // |flutter::PlatformView::Delegate| void OnPlatformViewDispatchSemanticsAction(int32_t id, flutter::SemanticsAction action, std::vector args) {} diff --git a/testing/dart/window_hooks_integration_test.dart b/testing/dart/window_hooks_integration_test.dart index 91c2036db..3a1aac710 100644 --- a/testing/dart/window_hooks_integration_test.dart +++ b/testing/dart/window_hooks_integration_test.dart @@ -24,6 +24,7 @@ part '../../lib/ui/compositing.dart'; part '../../lib/ui/geometry.dart'; part '../../lib/ui/hash_codes.dart'; part '../../lib/ui/hooks.dart'; +part '../../lib/ui/key.dart'; part '../../lib/ui/lerp.dart'; part '../../lib/ui/natives.dart'; part '../../lib/ui/painting.dart'; -- GitLab