未验证 提交 d325ca78 编写于 作者: T Tong Mu 提交者: GitHub

Hardware keyboard: Web, embedder, and dart:ui (#23466)

上级 ba1ac543
......@@ -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
......
......@@ -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",
......
......@@ -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",
......
......@@ -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) {
......
// 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';
}
}
}
......@@ -28,6 +28,12 @@ typedef TimingsCallback = void Function(List<FrameTiming> 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, _KeyDataResponseCallback>(
(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
}
......@@ -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';
......
......@@ -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.
///
......
// 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 <cstring>
namespace flutter {
static_assert(sizeof(KeyData) == kBytesPerKeyField * kKeyDataFieldCount,
"KeyData has the wrong size");
void KeyData::Clear() {
memset(this, 0, sizeof(KeyData));
}
} // namespace flutter
// 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 <cstdint>
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_
// 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 <cstring>
#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
// 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 <functional>
#include <vector>
#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<uint8_t>& 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<uint8_t> data_;
};
} // namespace flutter
#endif // FLUTTER_LIB_UI_WINDOW_POINTER_DATA_MESSAGE_H_
......@@ -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<uint8_t>& 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<tonic::DartState> dart_state =
begin_frame_.dart_state().lock();
......@@ -424,6 +440,21 @@ void PlatformConfiguration::CompletePlatformMessageResponse(
response->Complete(std::make_unique<fml::DataMapping>(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<std::string> supportedLocales =
tonic::DartConverter<std::vector<std::string>>::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,
......
......@@ -5,6 +5,7 @@
#ifndef FLUTTER_LIB_UI_WINDOW_PLATFORM_CONFIGURATION_H_
#define FLUTTER_LIB_UI_WINDOW_PLATFORM_CONFIGURATION_H_
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
......@@ -22,6 +23,8 @@ class FontCollection;
class PlatformMessage;
class Scene;
typedef std::function<void(bool /* handled */)> 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<uint8_t> 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<int64_t, std::unique_ptr<Window>> 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<int, fml::RefPtr<PlatformMessageResponse>>
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<uint64_t, KeyDataResponse> pending_key_responses_;
};
} // namespace flutter
......
......@@ -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<tonic::DartState> dart_state = library_.dart_state().lock();
if (!dart_state)
return;
tonic::DartState::Scope scope(dart_state);
const std::vector<uint8_t>& 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;
......
......@@ -5,10 +5,12 @@
#ifndef FLUTTER_LIB_UI_WINDOW_WINDOW_H_
#define FLUTTER_LIB_UI_WINDOW_WINDOW_H_
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
#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:
......
......@@ -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';
......
......@@ -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
......
// 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<String, int> kWebToLogicalKey = <String, int>{
'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<String, int> kWebToPhysicalKey = <String, int>{
'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<String, List<int?>> kWebLogicalLocationMap = <String, List<int?>>{
'0': <int?>[0x0000000030, null, null, 0x0200000030],
'1': <int?>[0x0000000031, null, null, 0x0200000031],
'2': <int?>[0x0000000032, null, null, 0x0200000032],
'3': <int?>[0x0000000033, null, null, 0x0200000033],
'4': <int?>[0x0000000034, null, null, 0x0200000034],
'5': <int?>[0x0000000035, null, null, 0x0200000035],
'6': <int?>[0x0000000036, null, null, 0x0200000036],
'7': <int?>[0x0000000037, null, null, 0x0200000037],
'8': <int?>[0x0000000038, null, null, 0x0200000038],
'9': <int?>[0x0000000039, null, null, 0x0200000039],
'.': <int?>[0x000000002e, null, null, 0x020000002e],
'Insert': <int?>[0x0000000407, null, null, 0x0200000030],
'End': <int?>[0x0000000305, null, null, 0x0200000031],
'ArrowDown': <int?>[0x0000000301, null, null, 0x0200000032],
'PageDown': <int?>[0x0000000307, null, null, 0x0200000033],
'ArrowLeft': <int?>[0x0000000302, null, null, 0x0200000034],
'Clear': <int?>[0x0000000401, null, null, 0x0200000035],
'ArrowRight': <int?>[0x0000000303, null, null, 0x0200000036],
'Home': <int?>[0x0000000306, null, null, 0x0200000037],
'ArrowUp': <int?>[0x0000000304, null, null, 0x0200000038],
'PageUp': <int?>[0x0000000308, null, null, 0x0200000039],
'Delete': <int?>[0x000000007f, null, null, 0x020000002e],
'/': <int?>[0x000000002f, null, null, 0x020000002f],
'*': <int?>[0x000000002a, null, null, 0x020000002a],
'-': <int?>[0x000000002d, null, null, 0x020000002d],
'+': <int?>[0x000000002b, null, null, 0x020000002b],
'Enter': <int?>[0x000000000d, null, null, 0x020000000d],
'Shift': <int?>[null, 0x030000010d, 0x040000010d, null],
'Control': <int?>[null, 0x0300000105, 0x0400000105, null],
'Alt': <int?>[null, 0x0300000102, 0x0400000102, null],
'Meta': <int?>[null, 0x0300000109, 0x0400000109, null],
};
// 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> = 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<int, _ModifierGetter> _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<String, html.EventListener> _listeners = <String, html.EventListener>{};
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<int, int> _pressingRecords = <int, int>{};
// 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<ui.KeyData> getData, _VoidCallback callback) {
bool canceled = false;
Future<void>.delayed(duration).then<void>((_) {
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<int, _VoidCallback> _keyGuards = <int, _VoidCallback>{};
// 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();
}
}
}
......@@ -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<ui.PointerDataPacket>(_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<A1, A2, A3>(void Function(A1 a1, A2 a2, A3 a3)? callback, Zone? zon
});
}
}
// 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';
}
}
}
......@@ -9,6 +9,7 @@ typedef VoidCallback = void Function();
typedef FrameCallback = void Function(Duration duration);
typedef TimingsCallback = void Function(List<FrameTiming> 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
}
......@@ -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();
......
......@@ -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';
......
// 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<ui.KeyData> keyDataList = <ui.KeyData>[];
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<ui.KeyData> keyDataList = <ui.KeyData>[];
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<ui.KeyData> keyDataList = <ui.KeyData>[];
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<ui.KeyData> keyDataList = <ui.KeyData>[];
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<ui.KeyData> keyDataList = <ui.KeyData>[];
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<ui.KeyData> keyDataList = <ui.KeyData>[];
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<ui.KeyData> keyDataList = <ui.KeyData>[];
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<ui.KeyData> keyDataList = <ui.KeyData>[];
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<ui.KeyData> keyDataList = <ui.KeyData>[];
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<ui.KeyData> keyDataList = <ui.KeyData>[];
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<ui.KeyData> keyDataList = <ui.KeyData>[];
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<ui.KeyData> keyDataList = <ui.KeyData>[];
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<ui.KeyData> keyDataList = <ui.KeyData>[];
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<ui.KeyData> keyDataList = <ui.KeyData>[];
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<ui.KeyData> keyDataList = <ui.KeyData>[];
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<String> modifierState = <String>{};
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);
});
}
......@@ -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<uint8_t> args) {
......
......@@ -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.
......
......@@ -415,6 +415,14 @@ void Engine::DispatchPointerDataPacket(
pointer_data_dispatcher_->DispatchPacket(std::move(packet), trace_flow_id);
}
void Engine::DispatchKeyDataPacket(std::unique_ptr<KeyDataPacket> 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<uint8_t> args) {
......
......@@ -728,6 +728,21 @@ class Engine final : public RuntimeDelegate,
void DispatchPointerDataPacket(std::unique_ptr<PointerDataPacket> 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<KeyDataPacket> packet,
KeyDataResponse callback);
//----------------------------------------------------------------------------
/// @brief Notifies the engine that the embedder encountered an
/// accessibility related action on the specified node. This call
......
......@@ -42,6 +42,12 @@ void PlatformView::DispatchPointerDataPacket(
pointer_data_packet_converter_.Convert(std::move(packet)));
}
void PlatformView::DispatchKeyDataPacket(std::unique_ptr<KeyDataPacket> packet,
KeyDataResponse callback) {
delegate_.OnPlatformViewDispatchKeyDataPacket(std::move(packet),
std::move(callback));
}
void PlatformView::DispatchSemanticsAction(int32_t id,
SemanticsAction action,
std::vector<uint8_t> args) {
......
......@@ -5,6 +5,7 @@
#ifndef COMMON_PLATFORM_VIEW_H_
#define COMMON_PLATFORM_VIEW_H_
#include <functional>
#include <memory>
#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<void(bool)>;
//--------------------------------------------------------------------------
/// @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<PointerDataPacket> 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<KeyDataPacket> packet,
std::function<void(bool /* handled */)> 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<PointerDataPacket> 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<KeyDataPacket> 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
......
......@@ -961,6 +961,23 @@ void Shell::OnPlatformViewDispatchPointerDataPacket(
next_pointer_flow_id_++;
}
// |PlatformView::Delegate|
void Shell::OnPlatformViewDispatchKeyDataPacket(
std::unique_ptr<KeyDataPacket> packet,
std::function<void(bool /* handled */)> 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,
......
......@@ -504,6 +504,11 @@ class Shell final : public PlatformView::Delegate,
void OnPlatformViewDispatchPointerDataPacket(
std::unique_ptr<PointerDataPacket> packet) override;
// |PlatformView::Delegate|
void OnPlatformViewDispatchKeyDataPacket(
std::unique_ptr<KeyDataPacket> packet,
std::function<void(bool /* handled */)> callback) override;
// |PlatformView::Delegate|
void OnPlatformViewDispatchSemanticsAction(
int32_t id,
......
......@@ -62,6 +62,10 @@ class MockPlatformViewDelegate : public PlatformView::Delegate {
MOCK_METHOD1(OnPlatformViewDispatchPointerDataPacket,
void(std::unique_ptr<PointerDataPacket> packet));
MOCK_METHOD2(OnPlatformViewDispatchKeyDataPacket,
void(std::unique_ptr<KeyDataPacket> packet,
KeyDataResponse callback));
MOCK_METHOD3(OnPlatformViewDispatchSemanticsAction,
void(int32_t id,
SemanticsAction action,
......
......@@ -25,6 +25,8 @@ class MockDelegate : public PlatformView::Delegate {
void OnPlatformViewDispatchPlatformMessage(fml::RefPtr<PlatformMessage> message) override {}
void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr<PointerDataPacket> packet) override {
}
void OnPlatformViewDispatchKeyDataPacket(std::unique_ptr<KeyDataPacket> packet,
std::function<void(bool)> callback) override {}
void OnPlatformViewDispatchSemanticsAction(int32_t id,
SemanticsAction action,
std::vector<uint8_t> args) override {}
......
......@@ -95,6 +95,8 @@ class FlutterPlatformViewsTestMockPlatformViewDelegate : public PlatformView::De
void OnPlatformViewDispatchPlatformMessage(fml::RefPtr<PlatformMessage> message) override {}
void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr<PointerDataPacket> packet) override {
}
void OnPlatformViewDispatchKeyDataPacket(std::unique_ptr<KeyDataPacket> packet,
std::function<void(bool)> callback) override {}
void OnPlatformViewDispatchSemanticsAction(int32_t id,
SemanticsAction action,
std::vector<uint8_t> args) override {}
......
......@@ -78,6 +78,8 @@ class MockDelegate : public PlatformView::Delegate {
void OnPlatformViewDispatchPlatformMessage(fml::RefPtr<PlatformMessage> message) override {}
void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr<PointerDataPacket> packet) override {
}
void OnPlatformViewDispatchKeyDataPacket(std::unique_ptr<KeyDataPacket> packet,
std::function<void(bool)> callback) override {}
void OnPlatformViewDispatchSemanticsAction(int32_t id,
SemanticsAction action,
std::vector<uint8_t> args) override {}
......
......@@ -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<flutter::KeyDataPacket>(key_data, character);
auto response = [callback, user_data](bool handled) {
if (callback != nullptr)
callback(handled, user_data);
};
return reinterpret_cast<flutter::EmbedderEngine*>(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);
......
......@@ -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;
......
......@@ -136,6 +136,22 @@ bool EmbedderEngine::DispatchPointerDataPacket(
return true;
}
bool EmbedderEngine::DispatchKeyDataPacket(
std::unique_ptr<flutter::KeyDataPacket> 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<flutter::PlatformMessage> message) {
if (!IsValid() || !message) {
......
......@@ -60,6 +60,22 @@ class EmbedderEngine {
bool DispatchPointerDataPacket(
std::unique_ptr<flutter::PointerDataPacket> 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<flutter::KeyDataPacket> packet,
KeyDataResponse callback);
bool SendPlatformMessage(fml::RefPtr<flutter::PlatformMessage> message);
bool RegisterTexture(int64_t texture);
......
......@@ -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) {
......
......@@ -1191,5 +1191,208 @@ TEST_F(EmbedderTest, CanLaunchAndShutdownWithAValidElfSource) {
engine.reset();
}
//------------------------------------------------------------------------------
// Key Data
//------------------------------------------------------------------------------
typedef struct {
std::shared_ptr<fml::AutoResetWaitableEvent> 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<fml::AutoResetWaitableEvent>();
uint64_t echoed_char;
FlutterKeyEvent echoed_event;
auto native_echo_event = [&](Dart_NativeArguments args) {
echoed_event.type =
UnserializeKeyEventKind(tonic::DartConverter<uint64_t>::FromDart(
Dart_GetNativeArgument(args, 0)));
echoed_event.timestamp = tonic::DartConverter<uint64_t>::FromDart(
Dart_GetNativeArgument(args, 1));
echoed_event.physical = tonic::DartConverter<uint64_t>::FromDart(
Dart_GetNativeArgument(args, 2));
echoed_event.logical = tonic::DartConverter<uint64_t>::FromDart(
Dart_GetNativeArgument(args, 3));
echoed_char = tonic::DartConverter<uint64_t>::FromDart(
Dart_GetNativeArgument(args, 4));
echoed_event.synthesized =
tonic::DartConverter<bool>::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<fml::AutoResetWaitableEvent>(),
};
// 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<KeyEventUserData*>(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<fml::AutoResetWaitableEvent>(),
.returned = false,
};
KeyEventUserData user_data3{
.latch = std::make_shared<fml::AutoResetWaitableEvent>(),
.returned = false,
};
auto callback23 = [](bool handled, void* untyped_user_data) {
KeyEventUserData* user_data =
reinterpret_cast<KeyEventUserData*>(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
......@@ -81,6 +81,10 @@ class MockPlatformViewDelegate : public flutter::PlatformView::Delegate {
void OnPlatformViewDispatchPointerDataPacket(
std::unique_ptr<flutter::PointerDataPacket> packet) {}
// |flutter::PlatformView::Delegate|
void OnPlatformViewDispatchKeyDataPacket(
std::unique_ptr<flutter::KeyDataPacket> packet,
std::function<void(bool)> callback) {}
// |flutter::PlatformView::Delegate|
void OnPlatformViewDispatchSemanticsAction(int32_t id,
flutter::SemanticsAction action,
std::vector<uint8_t> args) {}
......
......@@ -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';
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册