diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 0c85ab787829e..6044416e4fa0b 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1658,6 +1658,7 @@ FILE: ../../../flutter/shell/platform/windows/keyboard_key_embedder_handler_unit FILE: ../../../flutter/shell/platform/windows/keyboard_key_handler.cc FILE: ../../../flutter/shell/platform/windows/keyboard_key_handler.h FILE: ../../../flutter/shell/platform/windows/keyboard_key_handler_unittests.cc +FILE: ../../../flutter/shell/platform/windows/keyboard_unittests.cc FILE: ../../../flutter/shell/platform/windows/platform_handler.cc FILE: ../../../flutter/shell/platform/windows/platform_handler.h FILE: ../../../flutter/shell/platform/windows/platform_handler_unittests.cc diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn index 4ed1b0a8e7cfe..df8a6c11751fe 100644 --- a/shell/platform/windows/BUILD.gn +++ b/shell/platform/windows/BUILD.gn @@ -243,12 +243,18 @@ executable("flutter_windows_unittests") { "keyboard_key_channel_handler_unittests.cc", "keyboard_key_embedder_handler_unittests.cc", "keyboard_key_handler_unittests.cc", + "keyboard_unittests.cc", "testing/flutter_window_win32_test.cc", "testing/flutter_window_win32_test.h", "testing/mock_window_binding_handler.cc", "testing/mock_window_binding_handler.h", "testing/mock_window_win32.cc", "testing/mock_window_win32.h", + "testing/test_keyboard.cc", + "testing/test_keyboard.h", + "testing/test_keyboard_unittests.cc", + "testing/wm_builders.cc", + "testing/wm_builders.h", "text_input_plugin_unittest.cc", "window_proc_delegate_manager_win32_unittests.cc", "window_win32_unittests.cc", diff --git a/shell/platform/windows/flutter_window_win32_unittests.cc b/shell/platform/windows/flutter_window_win32_unittests.cc index 7f01ed5c9930e..93763e3e8414b 100644 --- a/shell/platform/windows/flutter_window_win32_unittests.cc +++ b/shell/platform/windows/flutter_window_win32_unittests.cc @@ -11,6 +11,7 @@ #include "flutter/shell/platform/windows/testing/engine_modifier.h" #include "flutter/shell/platform/windows/testing/flutter_window_win32_test.h" #include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h" +#include "flutter/shell/platform/windows/testing/test_keyboard.h" #include "flutter/shell/platform/windows/text_input_plugin.h" #include "flutter/shell/platform/windows/text_input_plugin_delegate.h" @@ -27,26 +28,6 @@ namespace flutter { namespace testing { namespace { -// Creates a valid Windows LPARAM for WM_KEYDOWN and WM_CHAR from parameters -// given. -static LPARAM CreateKeyEventLparam(USHORT scancode, - bool extended = false, - bool was_down = 1, - USHORT repeat_count = 1, - bool context_code = 0, - bool transition_state = 1) { - return ((LPARAM(transition_state) << 31) | (LPARAM(was_down) << 30) | - (LPARAM(context_code) << 29) | (LPARAM(extended ? 0x1 : 0x0) << 24) | - (LPARAM(scancode) << 16) | LPARAM(repeat_count)); -} - -// A struc to hold simulated events that will be delivered after the framework -// response is handled. -struct SimulatedEvent { - UINT message; - WPARAM wparam; - LPARAM lparam; -}; // A key event handler that can be spied on while it forwards calls to the real // key event handler. @@ -123,7 +104,8 @@ class SpyTextInputPlugin : public KeyboardHandlerBase, std::unique_ptr real_implementation_; }; -class MockFlutterWindowWin32 : public FlutterWindowWin32 { +class MockFlutterWindowWin32 : public FlutterWindowWin32, + public MockMessageQueue { public: MockFlutterWindowWin32() : FlutterWindowWin32(800, 600) { ON_CALL(*this, GetDpiScale()) @@ -142,7 +124,19 @@ class MockFlutterWindowWin32 : public FlutterWindowWin32 { LRESULT InjectWindowMessage(UINT const message, WPARAM const wparam, LPARAM const lparam) { - return HandleMessage(message, wparam, lparam); + return Win32SendMessage(NULL, message, wparam, lparam); + } + + void InjectMessages(int count, Win32Message message1, ...) { + Win32Message messages[count]; + messages[0] = message1; + va_list args; + va_start(args, message1); + for (int i = 1; i < count; i += 1) { + messages[i] = va_arg(args, Win32Message); + } + va_end(args); + InjectMessageList(count, messages); } MOCK_METHOD1(OnDpiScale, void(unsigned int)); @@ -153,10 +147,34 @@ class MockFlutterWindowWin32 : public FlutterWindowWin32 { MOCK_METHOD0(OnPointerLeave, void()); MOCK_METHOD0(OnSetCursor, void()); MOCK_METHOD2(OnScroll, void(double, double)); - MOCK_METHOD4(DefaultWindowProc, LRESULT(HWND, UINT, WPARAM, LPARAM)); MOCK_METHOD0(GetDpiScale, float()); MOCK_METHOD0(IsVisible, bool()); MOCK_METHOD1(UpdateCursorRect, void(const Rect&)); + + protected: + virtual BOOL Win32PeekMessage(LPMSG lpMsg, + HWND hWnd, + UINT wMsgFilterMin, + UINT wMsgFilterMax, + UINT wRemoveMsg) override { + return MockMessageQueue::Win32PeekMessage(lpMsg, hWnd, wMsgFilterMin, + wMsgFilterMax, wRemoveMsg); + } + + LRESULT Win32DefWindowProc(HWND hWnd, + UINT Msg, + WPARAM wParam, + LPARAM lParam) override { + return kWmResultDefault; + } + + private: + LRESULT Win32SendMessage(HWND hWnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) override { + return HandleMessage(message, wparam, lparam); + } }; class MockWindowBindingHandlerDelegate : public WindowBindingHandlerDelegate { @@ -206,16 +224,17 @@ class TestFlutterWindowsView : public FlutterWindowsView { SpyTextInputPlugin* text_input_plugin; void InjectPendingEvents(MockFlutterWindowWin32* win32window) { - while (pending_responds_.size() > 0) { - SimulatedEvent event = pending_responds_.front(); - win32window->InjectWindowMessage(event.message, event.wparam, - event.lparam); - pending_responds_.pop_front(); - } + win32window->InjectMessageList(pending_responds_.size(), + pending_responds_.data()); + pending_responds_.clear(); } protected: - void RegisterKeyboardHandlers(flutter::BinaryMessenger* messenger) override { + void RegisterKeyboardHandlers( + flutter::BinaryMessenger* messenger, + flutter::KeyboardKeyHandler::EventDispatcher dispatch_event, + flutter::KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state) + override { auto spy_key_event_handler = std::make_unique( messenger, [this](UINT cInputs, LPINPUT pInputs, int cbSize) -> UINT { return this->SendInput(cInputs, pInputs, cbSize); @@ -235,34 +254,32 @@ class TestFlutterWindowsView : public FlutterWindowsView { const KEYBDINPUT kbdinput = pInputs->ki; const UINT message = (kbdinput.dwFlags & KEYEVENTF_KEYUP) ? WM_KEYUP : WM_KEYDOWN; + const bool is_key_up = kbdinput.dwFlags & KEYEVENTF_KEYUP; const LPARAM lparam = CreateKeyEventLparam( - kbdinput.wScan, kbdinput.dwFlags & KEYEVENTF_EXTENDEDKEY); + kbdinput.wScan, kbdinput.dwFlags & KEYEVENTF_EXTENDEDKEY, is_key_up); // Windows would normally fill in the virtual key code for us, so we // simulate it for the test with the key we know is in the test. The // KBDINPUT we're passed doesn't have it filled in (on purpose, so that // Windows will fill it in). - pending_responds_.push_back(SimulatedEvent{message, virtual_key_, lparam}); + // + // TODO(dkwingsmt): Don't check the message results for redispatched + // messages for now, because making them work takes non-trivial rework + // to our current structure. https://github.com/flutter/flutter/issues/87843 + // If this is resolved, change them to kWmResultDefault. + pending_responds_.push_back( + Win32Message{message, virtual_key_, lparam, kWmResultDontCheck}); if (is_printable_ && (kbdinput.dwFlags & KEYEVENTF_KEYUP) == 0) { pending_responds_.push_back( - SimulatedEvent{WM_CHAR, virtual_key_, lparam}); + Win32Message{WM_CHAR, virtual_key_, lparam, kWmResultDontCheck}); } return 1; } - std::deque pending_responds_; + std::vector pending_responds_; WPARAM virtual_key_; bool is_printable_; }; -// A struct to use as a FlutterPlatformMessageResponseHandle so it can keep the -// callbacks and user data passed to the engine's -// PlatformMessageCreateResponseHandle for use in the SendPlatformMessage -// overridden function. -struct TestResponseHandle { - FlutterDesktopBinaryReply callback; - void* user_data; -}; - // The static value to return as the "handled" value from the framework for key // events. Individual tests set this to change the framework response that the // test engine simulates. @@ -280,46 +297,9 @@ std::unique_ptr GetTestEngine() { auto engine = std::make_unique(project); EngineModifier modifier(engine.get()); - // Force the non-AOT path unless overridden by the test. - modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; }; - - modifier.embedder_api().PlatformMessageCreateResponseHandle = - [](auto engine, auto data_callback, auto user_data, auto response_out) { - TestResponseHandle* response_handle = new TestResponseHandle(); - response_handle->user_data = user_data; - response_handle->callback = data_callback; - *response_out = reinterpret_cast( - response_handle); - return kSuccess; - }; - - modifier.embedder_api().SendPlatformMessage = - [](FLUTTER_API_SYMBOL(FlutterEngine) engine, - const FlutterPlatformMessage* message) { - rapidjson::Document document; - auto& allocator = document.GetAllocator(); - document.SetObject(); - document.AddMember("handled", test_response, allocator); - auto encoded = - flutter::JsonMessageCodec::GetInstance().EncodeMessage(document); - const TestResponseHandle* response_handle = - reinterpret_cast( - message->response_handle); - if (response_handle->callback != nullptr) { - response_handle->callback(encoded->data(), encoded->size(), - response_handle->user_data); - } - return kSuccess; - }; - - modifier.embedder_api().PlatformMessageReleaseResponseHandle = - [](FLUTTER_API_SYMBOL(FlutterEngine) engine, - FlutterPlatformMessageResponseHandle* response) { - const TestResponseHandle* response_handle = - reinterpret_cast(response); - delete response_handle; - return kSuccess; - }; + MockEmbedderApiForKeyboard( + modifier, [] { return test_response; }, + [](const FlutterKeyEvent* event) { return false; }); return engine; } @@ -339,14 +319,12 @@ TEST(FlutterWindowWin32Test, NonPrintableKeyDownPropagation) { constexpr WPARAM scan_code = 10; constexpr char32_t character = 0; MockFlutterWindowWin32 win32window; - std::deque pending_events; auto window_binding_handler = std::make_unique<::testing::NiceMock>(); TestFlutterWindowsView flutter_windows_view( std::move(window_binding_handler), virtual_key, false /* is_printable */); win32window.SetView(&flutter_windows_view); - LPARAM lparam = CreateKeyEventLparam(scan_code, false /* extended */, - false /* PrevState */); + LPARAM lparam = CreateKeyEventLparam(scan_code, false, false); // Test an event not handled by the framework { @@ -361,15 +339,12 @@ TEST(FlutterWindowWin32Test, NonPrintableKeyDownPropagation) { KeyboardHook(_, _, _, _, _, _, _)) .Times(1) .RetiresOnSaturation(); - EXPECT_CALL(win32window, DefaultWindowProc(_, _, _, _)) - .Times(0) - .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.key_event_handler, TextHook(_, _)) .Times(0); EXPECT_CALL(*flutter_windows_view.text_input_plugin, TextHook(_, _)) .Times(0); - EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), - 0); + win32window.InjectMessages(1, + Win32Message{WM_KEYDOWN, virtual_key, lparam}); flutter_windows_view.InjectPendingEvents(&win32window); } @@ -384,8 +359,8 @@ TEST(FlutterWindowWin32Test, NonPrintableKeyDownPropagation) { EXPECT_CALL(*flutter_windows_view.text_input_plugin, KeyboardHook(_, _, _, _, _, _, _)) .Times(0); - EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), - 0); + win32window.InjectMessages(1, + Win32Message{WM_KEYDOWN, virtual_key, lparam}); flutter_windows_view.InjectPendingEvents(&win32window); } } @@ -406,16 +381,15 @@ TEST(FlutterWindowWin32Test, CharKeyDownPropagation) { TestFlutterWindowsView flutter_windows_view( std::move(window_binding_handler), virtual_key, true /* is_printable */); win32window.SetView(&flutter_windows_view); - LPARAM lparam = CreateKeyEventLparam(scan_code, false /* extended */, - true /* PrevState */); + LPARAM lparam = CreateKeyEventLparam(scan_code, false, false); + flutter_windows_view.SetEngine(std::move(GetTestEngine())); // Test an event not handled by the framework { test_response = false; - flutter_windows_view.SetEngine(std::move(GetTestEngine())); EXPECT_CALL(*flutter_windows_view.key_event_handler, KeyboardHook(_, virtual_key, scan_code, WM_KEYDOWN, character, - false, true)) + false, false)) .Times(2) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.text_input_plugin, @@ -428,19 +402,17 @@ TEST(FlutterWindowWin32Test, CharKeyDownPropagation) { EXPECT_CALL(*flutter_windows_view.text_input_plugin, TextHook(_, _)) .Times(1) .RetiresOnSaturation(); - EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), - 0); - EXPECT_EQ(win32window.InjectWindowMessage(WM_CHAR, virtual_key, lparam), 0); + win32window.InjectMessages(2, Win32Message{WM_KEYDOWN, virtual_key, lparam}, + Win32Message{WM_CHAR, virtual_key, lparam}); flutter_windows_view.InjectPendingEvents(&win32window); } - return; // Test an event handled by the framework { test_response = true; EXPECT_CALL(*flutter_windows_view.key_event_handler, KeyboardHook(_, virtual_key, scan_code, WM_KEYDOWN, character, - false /* is_printable */, true)) + false, false)) .Times(1) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.text_input_plugin, @@ -450,28 +422,26 @@ TEST(FlutterWindowWin32Test, CharKeyDownPropagation) { .Times(0); EXPECT_CALL(*flutter_windows_view.text_input_plugin, TextHook(_, _)) .Times(0); - EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), - 0); - EXPECT_EQ(win32window.InjectWindowMessage(WM_CHAR, virtual_key, lparam), 0); + win32window.InjectMessages(2, Win32Message{WM_KEYDOWN, virtual_key, lparam}, + Win32Message{WM_CHAR, virtual_key, lparam}); flutter_windows_view.InjectPendingEvents(&win32window); } } -// Tests key event propagation of modifier key down events. This are different +// Tests key event propagation of modifier key down events. This is different // from non-printable events in that they call MapVirtualKey, resulting in a // slightly different code path. TEST(FlutterWindowWin32Test, ModifierKeyDownPropagation) { constexpr WPARAM virtual_key = VK_LSHIFT; - constexpr WPARAM scan_code = 20; + constexpr WPARAM scan_code = 0x2a; constexpr char32_t character = 0; MockFlutterWindowWin32 win32window; - std::deque pending_events; auto window_binding_handler = std::make_unique<::testing::NiceMock>(); TestFlutterWindowsView flutter_windows_view( std::move(window_binding_handler), virtual_key, false /* is_printable */); win32window.SetView(&flutter_windows_view); - LPARAM lparam = CreateKeyEventLparam(scan_code); + LPARAM lparam = CreateKeyEventLparam(scan_code, false, false); // Test an event not handled by the framework { @@ -479,7 +449,7 @@ TEST(FlutterWindowWin32Test, ModifierKeyDownPropagation) { flutter_windows_view.SetEngine(std::move(GetTestEngine())); EXPECT_CALL(*flutter_windows_view.key_event_handler, KeyboardHook(_, virtual_key, scan_code, WM_KEYDOWN, character, - false /* extended */, true)) + false /* extended */, false)) .Times(2) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.text_input_plugin, @@ -500,7 +470,7 @@ TEST(FlutterWindowWin32Test, ModifierKeyDownPropagation) { test_response = true; EXPECT_CALL(*flutter_windows_view.key_event_handler, KeyboardHook(_, virtual_key, scan_code, WM_KEYDOWN, character, - false /* extended */, true)) + false /* extended */, false)) .Times(1) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.text_input_plugin, diff --git a/shell/platform/windows/flutter_windows_view.cc b/shell/platform/windows/flutter_windows_view.cc index 55cbc43299dce..fee32e5f7da18 100644 --- a/shell/platform/windows/flutter_windows_view.cc +++ b/shell/platform/windows/flutter_windows_view.cc @@ -55,7 +55,17 @@ void FlutterWindowsView::SetEngine( // Set up the system channel handlers. auto internal_plugin_messenger = internal_plugin_registrar_->messenger(); - RegisterKeyboardHandlers(internal_plugin_messenger); +#ifdef WINUWP + flutter::KeyboardKeyHandler::EventDispatcher dispatch_event = nullptr; + flutter::KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state = + nullptr; +#else + flutter::KeyboardKeyHandler::EventDispatcher dispatch_event = SendInput; + flutter::KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state = + GetKeyState; +#endif + RegisterKeyboardHandlers(internal_plugin_messenger, dispatch_event, + get_key_state); platform_handler_ = PlatformHandler::Create(internal_plugin_messenger, this); cursor_handler_ = std::make_unique( internal_plugin_messenger, binding_handler_.get()); @@ -67,7 +77,9 @@ void FlutterWindowsView::SetEngine( } void FlutterWindowsView::RegisterKeyboardHandlers( - flutter::BinaryMessenger* messenger) { + flutter::BinaryMessenger* messenger, + flutter::KeyboardKeyHandler::EventDispatcher dispatch_event, + flutter::KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state) { // There must be only one handler that receives |SendInput|, i.e. only one // handler that might redispatch events. (See the documentation of // |KeyboardKeyHandler| to learn about redispatching.) @@ -76,17 +88,8 @@ void FlutterWindowsView::RegisterKeyboardHandlers( // of the event. In order to allow the same real event in the future, the // handler is "toggled" when events pass through, therefore the redispatching // algorithm does not allow more than 1 handler that takes |SendInput|. -#ifdef WINUWP - flutter::KeyboardKeyHandler::EventDispatcher redispatch_event = nullptr; - flutter::KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state = - nullptr; -#else - flutter::KeyboardKeyHandler::EventDispatcher redispatch_event = SendInput; - flutter::KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state = - GetKeyState; -#endif auto key_handler = - std::make_unique(redispatch_event); + std::make_unique(dispatch_event); key_handler->AddDelegate(std::make_unique( [this](const FlutterKeyEvent& event, FlutterKeyEventCallback callback, void* user_data) { diff --git a/shell/platform/windows/flutter_windows_view.h b/shell/platform/windows/flutter_windows_view.h index f70d1b1aa62d6..4a48499686b2e 100644 --- a/shell/platform/windows/flutter_windows_view.h +++ b/shell/platform/windows/flutter_windows_view.h @@ -19,6 +19,8 @@ #include "flutter/shell/platform/windows/cursor_handler.h" #include "flutter/shell/platform/windows/flutter_windows_engine.h" #include "flutter/shell/platform/windows/keyboard_handler_base.h" +#include "flutter/shell/platform/windows/keyboard_key_embedder_handler.h" +#include "flutter/shell/platform/windows/keyboard_key_handler.h" #include "flutter/shell/platform/windows/platform_handler.h" #include "flutter/shell/platform/windows/public/flutter_windows.h" #include "flutter/shell/platform/windows/text_input_plugin_delegate.h" @@ -144,7 +146,15 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, protected: // Called to create the keyboard hook handlers. - virtual void RegisterKeyboardHandlers(flutter::BinaryMessenger* messenger); + // + // The provided |dispatch_event| is where to inject events into the system, + // while |get_key_state| is where to acquire keyboard states. They will be + // the system APIs in production classes, but might be replaced with mock + // functions in unit tests. + virtual void RegisterKeyboardHandlers( + flutter::BinaryMessenger* messenger, + flutter::KeyboardKeyHandler::EventDispatcher dispatch_event, + flutter::KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state); // Used by RegisterKeyboardHandlers to add a new keyboard hook handler. void AddKeyboardHandler( diff --git a/shell/platform/windows/flutter_windows_view_unittests.cc b/shell/platform/windows/flutter_windows_view_unittests.cc index fc57d9bc1f542..331ede8960976 100644 --- a/shell/platform/windows/flutter_windows_view_unittests.cc +++ b/shell/platform/windows/flutter_windows_view_unittests.cc @@ -12,6 +12,7 @@ #include "flutter/shell/platform/windows/flutter_windows_view.h" #include "flutter/shell/platform/windows/testing/engine_modifier.h" #include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h" +#include "flutter/shell/platform/windows/testing/test_keyboard.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -59,82 +60,16 @@ std::unique_ptr GetTestEngine() { auto engine = std::make_unique(project); EngineModifier modifier(engine.get()); - - // This mock handles channel messages. This mock handles key events sent - // through the message channel is recorded in `key_event_logs`. - modifier.embedder_api().SendPlatformMessage = - [](FLUTTER_API_SYMBOL(FlutterEngine) engine, - const FlutterPlatformMessage* message) { - if (std::string(message->channel) == std::string("flutter/settings")) { - return kSuccess; - } - if (std::string(message->channel) == std::string("flutter/keyevent")) { - key_event_logs.push_back(kKeyEventFromChannel); - auto response = keyHandlingResponse(true); - const TestResponseHandle* response_handle = - reinterpret_cast( - message->response_handle); - if (response_handle->callback != nullptr) { - response_handle->callback(response->data(), response->size(), - response_handle->user_data); - } - return kSuccess; - } - return kSuccess; - }; - - // This mock handles key events sent through the embedder API, - // and records it in `key_event_logs`. - modifier.embedder_api().SendKeyEvent = - [](FLUTTER_API_SYMBOL(FlutterEngine) engine, const FlutterKeyEvent* event, - FlutterKeyEventCallback callback, void* user_data) { + MockEmbedderApiForKeyboard( + modifier, + [] { + key_event_logs.push_back(kKeyEventFromChannel); + return test_response; + }, + [](const FlutterKeyEvent* event) { key_event_logs.push_back(kKeyEventFromEmbedder); - if (callback != nullptr) { - callback(test_response, user_data); - } - return kSuccess; - }; - - // The following mocks enable channel mocking. - modifier.embedder_api().PlatformMessageCreateResponseHandle = - [](auto engine, auto data_callback, auto user_data, auto response_out) { - TestResponseHandle* response_handle = new TestResponseHandle(); - response_handle->user_data = user_data; - response_handle->callback = data_callback; - *response_out = reinterpret_cast( - response_handle); - return kSuccess; - }; - - modifier.embedder_api().PlatformMessageReleaseResponseHandle = - [](FLUTTER_API_SYMBOL(FlutterEngine) engine, - FlutterPlatformMessageResponseHandle* response) { - const TestResponseHandle* response_handle = - reinterpret_cast(response); - delete response_handle; - return kSuccess; - }; - - // The following mocks allows RunWithEntrypoint to be run, which creates a - // non-empty FlutterEngine and enables SendKeyEvent. - - modifier.embedder_api().Run = - [](size_t version, const FlutterRendererConfig* config, - const FlutterProjectArgs* args, void* user_data, - FLUTTER_API_SYMBOL(FlutterEngine) * engine_out) { - *engine_out = reinterpret_cast(1); - - return kSuccess; - }; - modifier.embedder_api().UpdateLocales = - [](auto engine, const FlutterLocale** locales, size_t locales_count) { - return kSuccess; - }; - modifier.embedder_api().SendWindowMetricsEvent = - [](auto engine, const FlutterWindowMetricsEvent* event) { - return kSuccess; - }; - modifier.embedder_api().Shutdown = [](auto engine) { return kSuccess; }; + return test_response; + }); engine->RunWithEntrypoint(nullptr); return engine; diff --git a/shell/platform/windows/keyboard_key_embedder_handler.cc b/shell/platform/windows/keyboard_key_embedder_handler.cc index b1aa06d1bbddb..2d8349621d650 100644 --- a/shell/platform/windows/keyboard_key_embedder_handler.cc +++ b/shell/platform/windows/keyboard_key_embedder_handler.cc @@ -29,6 +29,35 @@ constexpr SHORT kStateMaskPressed = 0x80; const char* empty_character = ""; } // namespace +// Get some bits of the char, from the start'th bit from the right (excluded) +// to the end'th bit from the right (included). +// +// For example, _GetBit(0x1234, 8, 4) => 0x3. +char _GetBit(char32_t ch, size_t start, size_t end) { + return (ch >> end) & ((1 << (start - end)) - 1); +} + +std::string ConvertChar32ToUtf8(char32_t ch) { + std::string result; + assert(0 <= ch && ch <= 0x10FFFF); + if (ch <= 0x007F) { + result.push_back(ch); + } else if (ch <= 0x07FF) { + result.push_back(0b11000000 + _GetBit(ch, 11, 6)); + result.push_back(0b10000000 + _GetBit(ch, 6, 0)); + } else if (ch <= 0xFFFF) { + result.push_back(0b11100000 + _GetBit(ch, 16, 12)); + result.push_back(0b10000000 + _GetBit(ch, 12, 6)); + result.push_back(0b10000000 + _GetBit(ch, 6, 0)); + } else { + result.push_back(0b11110000 + _GetBit(ch, 21, 18)); + result.push_back(0b10000000 + _GetBit(ch, 18, 12)); + result.push_back(0b10000000 + _GetBit(ch, 12, 6)); + result.push_back(0b10000000 + _GetBit(ch, 6, 0)); + } + return result; +} + KeyboardKeyEmbedderHandler::KeyboardKeyEmbedderHandler( SendEvent send_event, GetKeyStateHandler get_key_state) @@ -267,10 +296,10 @@ void KeyboardKeyEmbedderHandler::SynchronizeCritialToggledStates( continue; } assert(key_info.logical_key != 0); - SHORT state = get_key_state_(virtual_key); // Check toggling state first, because it might alter pressing state. if (key_info.check_toggled) { + SHORT state = get_key_state_(virtual_key); bool should_toggled = state & kStateMaskToggled; if (virtual_key == toggle_virtual_key) { key_info.toggled_on = !key_info.toggled_on; @@ -312,8 +341,8 @@ void KeyboardKeyEmbedderHandler::SynchronizeCritialPressedStates() { continue; } assert(key_info.logical_key != 0); - SHORT state = get_key_state_(virtual_key); if (key_info.check_pressed) { + SHORT state = get_key_state_(virtual_key); auto recorded_pressed_iter = pressingRecords_.find(key_info.physical_key); bool recorded_pressed = recorded_pressed_iter != pressingRecords_.end(); bool should_pressed = state & kStateMaskPressed; @@ -388,9 +417,8 @@ void KeyboardKeyEmbedderHandler::ConvertUtf32ToUtf8_(char* out, char32_t ch) { out[0] = '\0'; return; } - // TODO: Correctly handle UTF-32 - std::wstring text({static_cast(ch)}); - strcpy_s(out, kCharacterCacheSize, Utf8FromUtf16(text).c_str()); + std::string result = ConvertChar32ToUtf8(ch); + strcpy_s(out, kCharacterCacheSize, result.c_str()); } FlutterKeyEvent KeyboardKeyEmbedderHandler::CreateEmptyEvent() { diff --git a/shell/platform/windows/keyboard_key_embedder_handler.h b/shell/platform/windows/keyboard_key_embedder_handler.h index 04cf500178d9c..7ee88a33e626d 100644 --- a/shell/platform/windows/keyboard_key_embedder_handler.h +++ b/shell/platform/windows/keyboard_key_embedder_handler.h @@ -15,7 +15,7 @@ namespace flutter { -namespace {} // namespace +std::string ConvertChar32ToUtf8(char32_t ch); // A delegate of |KeyboardKeyHandler| that handles events by sending // converted |FlutterKeyEvent|s through the embedder API. diff --git a/shell/platform/windows/keyboard_key_embedder_handler_unittests.cc b/shell/platform/windows/keyboard_key_embedder_handler_unittests.cc index c6f405faf6edd..4012f0435ef89 100644 --- a/shell/platform/windows/keyboard_key_embedder_handler_unittests.cc +++ b/shell/platform/windows/keyboard_key_embedder_handler_unittests.cc @@ -83,6 +83,32 @@ constexpr uint64_t kVirtualKeyA = 0x41; using namespace ::flutter::testing::keycodes; } // namespace +TEST(KeyboardKeyEmbedderHandlerTest, ConvertChar32ToUtf8) { + std::string result; + + result = ConvertChar32ToUtf8(0x0024); + EXPECT_EQ(result.length(), 1); + EXPECT_EQ(result[0], '\x24'); + + result = ConvertChar32ToUtf8(0x00A2); + EXPECT_EQ(result.length(), 2); + EXPECT_EQ(result[0], '\xC2'); + EXPECT_EQ(result[1], '\xA2'); + + result = ConvertChar32ToUtf8(0x0939); + EXPECT_EQ(result.length(), 3); + EXPECT_EQ(result[0], '\xE0'); + EXPECT_EQ(result[1], '\xA4'); + EXPECT_EQ(result[2], '\xB9'); + + result = ConvertChar32ToUtf8(0x10348); + EXPECT_EQ(result.length(), 4); + EXPECT_EQ(result[0], '\xF0'); + EXPECT_EQ(result[1], '\x90'); + EXPECT_EQ(result[2], '\x8D'); + EXPECT_EQ(result[3], '\x88'); +} + // Test the most basic key events. // // Press, hold, and release key A on an US keyboard. diff --git a/shell/platform/windows/keyboard_key_handler.cc b/shell/platform/windows/keyboard_key_handler.cc index 4264039d658aa..0f80687cbba73 100644 --- a/shell/platform/windows/keyboard_key_handler.cc +++ b/shell/platform/windows/keyboard_key_handler.cc @@ -62,7 +62,7 @@ static bool IsKeyDownAltRight(int action, int virtual_key, bool extended) { #ifdef WINUWP return false; #else - return virtual_key == VK_LMENU && extended && action == WM_KEYDOWN; + return virtual_key == VK_RMENU && extended && action == WM_KEYDOWN; #endif } @@ -73,7 +73,7 @@ static bool IsKeyUpAltRight(int action, int virtual_key, bool extended) { #ifdef WINUWP return false; #else - return virtual_key == VK_LMENU && extended && action == WM_KEYUP; + return virtual_key == VK_RMENU && extended && action == WM_KEYUP; #endif } diff --git a/shell/platform/windows/keyboard_key_handler_unittests.cc b/shell/platform/windows/keyboard_key_handler_unittests.cc index d94a4b6e47778..5bd3c39464a98 100644 --- a/shell/platform/windows/keyboard_key_handler_unittests.cc +++ b/shell/platform/windows/keyboard_key_handler_unittests.cc @@ -470,7 +470,7 @@ TEST(KeyboardKeyHandlerTest, AltGr) { EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LCONTROL, kScanCodeControlLeft, WM_KEYDOWN, 0, false, false), true); - EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LMENU, kScanCodeAltLeft, + EXPECT_EQ(handler.KeyboardHook(nullptr, VK_RMENU, kScanCodeAltLeft, WM_KEYDOWN, 0, true, false), true); EXPECT_EQ(redispatch_scancode, 0); @@ -489,7 +489,7 @@ TEST(KeyboardKeyHandlerTest, AltGr) { EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LCONTROL, kScanCodeControlLeft, WM_KEYDOWN, 0, false, false), false); - EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LMENU, kScanCodeAltLeft, + EXPECT_EQ(handler.KeyboardHook(nullptr, VK_RMENU, kScanCodeAltLeft, WM_KEYDOWN, 0, true, false), false); @@ -498,7 +498,7 @@ TEST(KeyboardKeyHandlerTest, AltGr) { hook_history.clear(); // The key up event only causes a AltRight (extended AltLeft) up. - EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LMENU, kScanCodeAltLeft, WM_KEYUP, + EXPECT_EQ(handler.KeyboardHook(nullptr, VK_RMENU, kScanCodeAltLeft, WM_KEYUP, 0, true, true), true); EXPECT_EQ(hook_history.size(), 1); @@ -523,7 +523,7 @@ TEST(KeyboardKeyHandlerTest, AltGr) { EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LCONTROL, kScanCodeControlLeft, WM_KEYUP, 0, false, true), false); - EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LMENU, kScanCodeAltLeft, WM_KEYUP, + EXPECT_EQ(handler.KeyboardHook(nullptr, VK_RMENU, kScanCodeAltLeft, WM_KEYUP, 0, true, true), false); @@ -556,7 +556,7 @@ TEST(KeyboardKeyHandlerTest, AltGr) { false); // Key down AltRight. - EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LMENU, kScanCodeAltLeft, + EXPECT_EQ(handler.KeyboardHook(nullptr, VK_RMENU, kScanCodeAltLeft, WM_KEYDOWN, 0, true, false), true); EXPECT_EQ(redispatch_scancode, 0); @@ -569,14 +569,14 @@ TEST(KeyboardKeyHandlerTest, AltGr) { hook_history.clear(); // Resolve redispatches. - EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LMENU, kScanCodeAltLeft, + EXPECT_EQ(handler.KeyboardHook(nullptr, VK_RMENU, kScanCodeAltLeft, WM_KEYDOWN, 0, true, false), false); redispatch_scancode = 0; // Key up AltRight. - EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LMENU, kScanCodeAltLeft, WM_KEYUP, + EXPECT_EQ(handler.KeyboardHook(nullptr, VK_RMENU, kScanCodeAltLeft, WM_KEYUP, 0, true, true), true); EXPECT_EQ(hook_history.size(), 1); @@ -601,7 +601,7 @@ TEST(KeyboardKeyHandlerTest, AltGr) { EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LCONTROL, kScanCodeControlLeft, WM_KEYUP, 0, false, true), false); - EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LMENU, kScanCodeAltLeft, WM_KEYUP, + EXPECT_EQ(handler.KeyboardHook(nullptr, VK_RMENU, kScanCodeAltLeft, WM_KEYUP, 0, true, true), false); @@ -635,7 +635,7 @@ TEST(KeyboardKeyHandlerTest, AltGr) { EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LCONTROL, kScanCodeControlLeft, WM_KEYDOWN, 0, false, false), true); - EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LMENU, kScanCodeAltLeft, + EXPECT_EQ(handler.KeyboardHook(nullptr, VK_RMENU, kScanCodeAltLeft, WM_KEYDOWN, 0, true, false), true); EXPECT_EQ(redispatch_scancode, 0); @@ -654,7 +654,7 @@ TEST(KeyboardKeyHandlerTest, AltGr) { EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LCONTROL, kScanCodeControlLeft, WM_KEYDOWN, 0, false, false), false); - EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LMENU, kScanCodeAltLeft, + EXPECT_EQ(handler.KeyboardHook(nullptr, VK_RMENU, kScanCodeAltLeft, WM_KEYDOWN, 0, true, false), false); @@ -666,7 +666,7 @@ TEST(KeyboardKeyHandlerTest, AltGr) { EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LCONTROL, kScanCodeControlLeft, WM_KEYDOWN, 0, false, true), true); - EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LMENU, kScanCodeAltLeft, + EXPECT_EQ(handler.KeyboardHook(nullptr, VK_RMENU, kScanCodeAltLeft, WM_KEYDOWN, 0, true, true), true); EXPECT_EQ(redispatch_scancode, 0); @@ -685,7 +685,7 @@ TEST(KeyboardKeyHandlerTest, AltGr) { EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LCONTROL, kScanCodeControlLeft, WM_KEYDOWN, 0, false, false), false); - EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LMENU, kScanCodeAltLeft, + EXPECT_EQ(handler.KeyboardHook(nullptr, VK_RMENU, kScanCodeAltLeft, WM_KEYDOWN, 0, true, false), false); @@ -694,7 +694,7 @@ TEST(KeyboardKeyHandlerTest, AltGr) { hook_history.clear(); // Key up AltRight. - EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LMENU, kScanCodeAltLeft, WM_KEYUP, + EXPECT_EQ(handler.KeyboardHook(nullptr, VK_RMENU, kScanCodeAltLeft, WM_KEYUP, 0, true, true), true); EXPECT_EQ(hook_history.size(), 1); @@ -719,7 +719,7 @@ TEST(KeyboardKeyHandlerTest, AltGr) { EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LCONTROL, kScanCodeControlLeft, WM_KEYUP, 0, false, true), false); - EXPECT_EQ(handler.KeyboardHook(nullptr, VK_LMENU, kScanCodeAltLeft, WM_KEYUP, + EXPECT_EQ(handler.KeyboardHook(nullptr, VK_RMENU, kScanCodeAltLeft, WM_KEYUP, 0, true, true), false); diff --git a/shell/platform/windows/keyboard_unittests.cc b/shell/platform/windows/keyboard_unittests.cc new file mode 100644 index 0000000000000..7d0cd475059de --- /dev/null +++ b/shell/platform/windows/keyboard_unittests.cc @@ -0,0 +1,958 @@ +// 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/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/embedder/test_utils/key_codes.h" +#include "flutter/shell/platform/windows/flutter_windows_engine.h" +#include "flutter/shell/platform/windows/keyboard_key_channel_handler.h" +#include "flutter/shell/platform/windows/keyboard_key_embedder_handler.h" +#include "flutter/shell/platform/windows/keyboard_key_handler.h" +#include "flutter/shell/platform/windows/testing/engine_modifier.h" +#include "flutter/shell/platform/windows/testing/flutter_window_win32_test.h" +#include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h" +#include "flutter/shell/platform/windows/testing/test_keyboard.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include +#include + +using testing::_; +using testing::Invoke; +using testing::Return; +using namespace ::flutter::testing::keycodes; + +namespace flutter { +namespace testing { + +namespace { + +constexpr SHORT kStateMaskToggled = 0x01; +constexpr SHORT kStateMaskPressed = 0x80; + +static LPARAM CreateKeyEventLparam(USHORT scancode, + bool extended, + bool was_down, + USHORT repeat_count = 1, + bool context_code = 0, + bool transition_state = 0) { + return ((LPARAM(transition_state) << 31) | (LPARAM(was_down) << 30) | + (LPARAM(context_code) << 29) | (LPARAM(extended ? 0x1 : 0x0) << 24) | + (LPARAM(scancode) << 16) | LPARAM(repeat_count)); +} + +class MockFlutterWindowWin32 : public FlutterWindowWin32, + public MockMessageQueue { + public: + typedef std::function U16StringHandler; + + MockFlutterWindowWin32(U16StringHandler on_text) + : FlutterWindowWin32(800, 600), on_text_(std::move(on_text)) { + ON_CALL(*this, GetDpiScale()) + .WillByDefault(Return(this->FlutterWindowWin32::GetDpiScale())); + } + virtual ~MockFlutterWindowWin32() {} + + // Prevent copying. + MockFlutterWindowWin32(MockFlutterWindowWin32 const&) = delete; + MockFlutterWindowWin32& operator=(MockFlutterWindowWin32 const&) = delete; + + // Wrapper for GetCurrentDPI() which is a protected method. + UINT GetDpi() { return GetCurrentDPI(); } + + LRESULT Win32DefWindowProc(HWND hWnd, + UINT Msg, + WPARAM wParam, + LPARAM lParam) override { + return kWmResultDefault; + } + + // Simulates a WindowProc message from the OS. + LRESULT InjectWindowMessage(UINT const message, + WPARAM const wparam, + LPARAM const lparam) { + return Win32SendMessage(NULL, message, wparam, lparam); + } + + void OnText(const std::u16string& text) override { on_text_(text); } + + MOCK_METHOD1(OnDpiScale, void(unsigned int)); + MOCK_METHOD2(OnResize, void(unsigned int, unsigned int)); + MOCK_METHOD2(OnPointerMove, void(double, double)); + MOCK_METHOD3(OnPointerDown, void(double, double, UINT)); + MOCK_METHOD3(OnPointerUp, void(double, double, UINT)); + MOCK_METHOD0(OnPointerLeave, void()); + MOCK_METHOD0(OnSetCursor, void()); + MOCK_METHOD2(OnScroll, void(double, double)); + MOCK_METHOD0(GetDpiScale, float()); + MOCK_METHOD0(IsVisible, bool()); + MOCK_METHOD1(UpdateCursorRect, void(const Rect&)); + + virtual BOOL Win32PeekMessage(LPMSG lpMsg, + HWND hWnd, + UINT wMsgFilterMin, + UINT wMsgFilterMax, + UINT wRemoveMsg) override { + return MockMessageQueue::Win32PeekMessage(lpMsg, hWnd, wMsgFilterMin, + wMsgFilterMax, wRemoveMsg); + } + + private: + U16StringHandler on_text_; + + LRESULT Win32SendMessage(HWND hWnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) override { + return HandleMessage(message, wparam, lparam); + } +}; + +class TestKeystate { + public: + void Set(uint32_t virtual_key, bool pressed, bool toggled_on = false) { + state_[virtual_key] = (pressed ? kStateMaskPressed : 0) | + (toggled_on ? kStateMaskToggled : 0); + } + + SHORT Get(uint32_t virtual_key) { return state_[virtual_key]; } + + KeyboardKeyEmbedderHandler::GetKeyStateHandler Getter() { + return [this](uint32_t virtual_key) { return Get(virtual_key); }; + } + + private: + std::map state_; +}; + +typedef struct { + UINT cInputs; + KEYBDINPUT kbdinput; + int cbSize; +} SendInputInfo; + +// A FlutterWindowsView that overrides the RegisterKeyboardHandlers function +// to register the keyboard hook handlers that can be spied upon. +class TestFlutterWindowsView : public FlutterWindowsView { + public: + TestFlutterWindowsView() + // The WindowBindingHandler is used for window size and such, and doesn't + // affect keyboard. + : FlutterWindowsView( + std::make_unique<::testing::NiceMock>()), + redispatch_char(0) {} + + uint32_t redispatch_char; + + void InjectPendingEvents(MockFlutterWindowWin32* win32window, + uint32_t redispatch_char) { + std::vector messages; + for (const SendInputInfo& input : pending_responds_) { + const KEYBDINPUT kbdinput = input.kbdinput; + const UINT message = + (kbdinput.dwFlags & KEYEVENTF_KEYUP) ? WM_KEYUP : WM_KEYDOWN; + const bool is_key_up = kbdinput.dwFlags & KEYEVENTF_KEYUP; + const LPARAM lparam = CreateKeyEventLparam( + kbdinput.wScan, kbdinput.dwFlags & KEYEVENTF_EXTENDEDKEY, is_key_up); + // TODO(dkwingsmt): Don't check the message results for redispatched + // messages for now, because making them work takes non-trivial rework + // to our current structure. + // https://github.com/flutter/flutter/issues/87843 If this is resolved, + // change them to kWmResultDefault. + messages.push_back( + Win32Message{message, kbdinput.wVk, lparam, kWmResultDontCheck}); + if (redispatch_char != 0 && (kbdinput.dwFlags & KEYEVENTF_KEYUP) == 0) { + messages.push_back( + Win32Message{WM_CHAR, redispatch_char, lparam, kWmResultDontCheck}); + } + } + + win32window->InjectMessageList(messages.size(), messages.data()); + pending_responds_.clear(); + } + + void SetKeyState(uint32_t key, bool pressed, bool toggled_on) { + key_state_.Set(key, pressed, toggled_on); + } + + protected: + void RegisterKeyboardHandlers( + BinaryMessenger* messenger, + KeyboardKeyHandler::EventDispatcher dispatch_event, + KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state) override { + FlutterWindowsView::RegisterKeyboardHandlers( + messenger, + [this](UINT cInputs, LPINPUT pInputs, int cbSize) -> UINT { + return this->SendInput(cInputs, pInputs, cbSize); + }, + key_state_.Getter()); + } + + private: + UINT SendInput(UINT cInputs, LPINPUT pInputs, int cbSize) { + pending_responds_.push_back({cInputs, pInputs->ki, cbSize}); + return 1; + } + + std::vector pending_responds_; + TestKeystate key_state_; +}; + +typedef enum { + kKeyCallOnKey, + kKeyCallOnText, +} KeyCallType; + +typedef struct { + KeyCallType type; + + // Only one of the following fields should be assigned. + FlutterKeyEvent key_event; + std::u16string text; +} KeyCall; + +static std::vector key_calls; + +void clear_key_calls() { + for (KeyCall& key_call : key_calls) { + if (key_call.type == kKeyCallOnKey && + key_call.key_event.character != nullptr) { + delete[] key_call.key_event.character; + } + } + key_calls.clear(); +} + +std::unique_ptr GetTestEngine(); + +class KeyboardTester { + public: + explicit KeyboardTester() { + view_ = std::make_unique(); + view_->SetEngine(std::move(GetTestEngine())); + window_ = std::make_unique( + [](const std::u16string& text) { + key_calls.push_back(KeyCall{ + .type = kKeyCallOnText, + .text = text, + }); + }); + window_->SetView(view_.get()); + } + + void SetKeyState(uint32_t key, bool pressed, bool toggled_on) { + view_->SetKeyState(key, pressed, toggled_on); + } + + void Responding(bool response) { test_response = response; } + + void InjectMessages(int count, Win32Message message1, ...) { + Win32Message messages[count]; + messages[0] = message1; + va_list args; + va_start(args, message1); + for (int i = 1; i < count; i += 1) { + messages[i] = va_arg(args, Win32Message); + } + va_end(args); + window_->InjectMessageList(count, messages); + } + + // Inject all events called with |SendInput| to the event queue, + // then process the event queue. + // + // If |redispatch_char| is not 0, then WM_KEYDOWN events will + // also redispatch a WM_CHAR event with that value as lparam. + void InjectPendingEvents(uint32_t redispatch_char = 0) { + view_->InjectPendingEvents(window_.get(), redispatch_char); + } + + static bool test_response; + + private: + std::unique_ptr view_; + std::unique_ptr window_; +}; + +bool KeyboardTester::test_response = false; + +// Returns an engine instance configured with dummy project path values, and +// overridden methods for sending platform messages, so that the engine can +// respond as if the framework were connected. +std::unique_ptr GetTestEngine() { + FlutterDesktopEngineProperties properties = {}; + properties.assets_path = L"C:\\foo\\flutter_assets"; + properties.icu_data_path = L"C:\\foo\\icudtl.dat"; + properties.aot_library_path = L"C:\\foo\\aot.so"; + FlutterProjectBundle project(properties); + auto engine = std::make_unique(project); + + EngineModifier modifier(engine.get()); + + MockEmbedderApiForKeyboard( + modifier, [] { return KeyboardTester::test_response; }, + [](const FlutterKeyEvent* event) { + FlutterKeyEvent clone_event = *event; + clone_event.character = event->character == nullptr + ? nullptr + : clone_string(event->character); + key_calls.push_back(KeyCall{ + .type = kKeyCallOnKey, + .key_event = clone_event, + }); + return KeyboardTester::test_response; + }); + + engine->RunWithEntrypoint(nullptr); + return engine; +} + +constexpr uint64_t kScanCodeKeyA = 0x1e; +constexpr uint64_t kScanCodeKeyE = 0x12; +constexpr uint64_t kScanCodeKeyQ = 0x10; +constexpr uint64_t kScanCodeKeyW = 0x11; +constexpr uint64_t kScanCodeDigit1 = 0x02; +// constexpr uint64_t kScanCodeNumpad1 = 0x4f; +// constexpr uint64_t kScanCodeNumLock = 0x45; +constexpr uint64_t kScanCodeControl = 0x1d; +constexpr uint64_t kScanCodeAlt = 0x38; +constexpr uint64_t kScanCodeShiftLeft = 0x2a; +// constexpr uint64_t kScanCodeShiftRight = 0x36; +constexpr uint64_t kScanCodeBracketLeft = 0x1a; + +constexpr uint64_t kVirtualDigit1 = 0x31; +constexpr uint64_t kVirtualKeyA = 0x41; +constexpr uint64_t kVirtualKeyE = 0x45; +constexpr uint64_t kVirtualKeyQ = 0x51; +constexpr uint64_t kVirtualKeyW = 0x57; + +constexpr bool kSynthesized = true; +constexpr bool kNotSynthesized = false; + +} // namespace + +// Define compound `expect` in macros. If they're defined in functions, the +// stacktrace wouldn't print where the function is called in the unit tests. + +#define EXPECT_CALL_IS_EVENT(_key_call, ...) \ + EXPECT_EQ(_key_call.type, kKeyCallOnKey); \ + EXPECT_EVENT_EQUALS(_key_call.key_event, __VA_ARGS__); + +#define EXPECT_CALL_IS_TEXT(_key_call, u16_string) \ + EXPECT_EQ(_key_call.type, kKeyCallOnText); \ + EXPECT_EQ(_key_call.text, u16_string); + +TEST(KeyboardTest, LowerCaseAHandled) { + KeyboardTester tester; + tester.Responding(true); + + // US Keyboard layout + + // Press A + tester.InjectMessages( + 2, + WmKeyDownInfo{kVirtualKeyA, kScanCodeKeyA, kNotExtended, kWasUp}.Build( + kWmResultZero), + WmCharInfo{'a', kScanCodeKeyA, kNotExtended, kWasUp}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalKeyA, + kLogicalKeyA, "a", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents('a'); + EXPECT_EQ(key_calls.size(), 0); + + // Release A + tester.InjectMessages( + 1, WmKeyUpInfo{kVirtualKeyA, kScanCodeKeyA, kNotExtended}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalKeyA, + kLogicalKeyA, "", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); +} + +TEST(KeyboardTest, LowerCaseAUnhandled) { + KeyboardTester tester; + tester.Responding(false); + + // US Keyboard layout + + // Press A + tester.InjectMessages( + 2, + WmKeyDownInfo{kVirtualKeyA, kScanCodeKeyA, kNotExtended, kWasUp}.Build( + kWmResultZero), + WmCharInfo{'a', kScanCodeKeyA, kNotExtended, kWasUp}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalKeyA, + kLogicalKeyA, "a", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents('a'); + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_TEXT(key_calls[0], u"a"); + clear_key_calls(); + + // Release A + tester.InjectMessages( + 1, WmKeyUpInfo{kVirtualKeyA, kScanCodeKeyA, kNotExtended}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalKeyA, + kLogicalKeyA, "", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); +} + +// Press Shift-A. This is special because Win32 gives 'A' as character for the +// KeyA press. +TEST(KeyboardTest, ShiftLeftKeyA) { + KeyboardTester tester; + tester.Responding(false); + + // US Keyboard layout + + // Press ShiftLeft + tester.SetKeyState(VK_LSHIFT, true, true); + tester.InjectMessages( + 1, + WmKeyDownInfo{VK_SHIFT, kScanCodeShiftLeft, kNotExtended, kWasUp}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, + kPhysicalShiftLeft, kLogicalShiftLeft, "", + kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); + clear_key_calls(); + + // Press A + tester.InjectMessages( + 2, + WmKeyDownInfo{kVirtualKeyA, kScanCodeKeyA, kNotExtended, kWasUp}.Build( + kWmResultZero), + WmCharInfo{'A', kScanCodeKeyA, kNotExtended, kWasUp}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalKeyA, + kLogicalKeyA, "A", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents('A'); + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_TEXT(key_calls[0], u"A"); + clear_key_calls(); + + // Release ShiftLeft + tester.SetKeyState(VK_LSHIFT, false, true); + tester.InjectMessages( + 1, WmKeyUpInfo{VK_SHIFT, kScanCodeShiftLeft, kNotExtended}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalShiftLeft, + kLogicalShiftLeft, "", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); + clear_key_calls(); + + // Release A + tester.InjectMessages( + 1, WmKeyUpInfo{kVirtualKeyA, kScanCodeKeyA, kNotExtended}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalKeyA, + kLogicalKeyA, "", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); +} + +// Press Ctrl-A. This is special because Win32 gives 0x01 as character for the +// KeyA press. +TEST(KeyboardTest, CtrlLeftKeyA) { + KeyboardTester tester; + tester.Responding(false); + + // US Keyboard layout + + // Press ControlLeft + tester.SetKeyState(VK_LCONTROL, true, true); + tester.InjectMessages( + 1, + WmKeyDownInfo{VK_CONTROL, kScanCodeControl, kNotExtended, kWasUp}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, + kPhysicalControlLeft, kLogicalControlLeft, "", + kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); + clear_key_calls(); + + // Press A + tester.InjectMessages( + 2, + WmKeyDownInfo{kVirtualKeyA, kScanCodeKeyA, kNotExtended, kWasUp}.Build( + kWmResultZero), + WmCharInfo{0x01, kScanCodeKeyA, kNotExtended, kWasUp}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalKeyA, + kLogicalKeyA, "", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(0); + EXPECT_EQ(key_calls.size(), 0); + clear_key_calls(); + + // Release A + tester.InjectMessages( + 1, WmKeyUpInfo{kVirtualKeyA, kScanCodeKeyA, kNotExtended}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalKeyA, + kLogicalKeyA, "", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); + + // Release ControlLeft + tester.SetKeyState(VK_LCONTROL, false, true); + tester.InjectMessages( + 1, WmKeyUpInfo{VK_CONTROL, kScanCodeControl, kNotExtended}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, + kPhysicalControlLeft, kLogicalControlLeft, "", + kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); + clear_key_calls(); +} + +// Press Ctrl-1. This is special because it yields no WM_CHAR for the 1. +TEST(KeyboardTest, CtrlLeftDigit1) { + KeyboardTester tester; + tester.Responding(false); + + // US Keyboard layout + + // Press ControlLeft + tester.SetKeyState(VK_LCONTROL, true, true); + tester.InjectMessages( + 1, + WmKeyDownInfo{VK_CONTROL, kScanCodeControl, kNotExtended, kWasUp}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, + kPhysicalControlLeft, kLogicalControlLeft, "", + kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); + clear_key_calls(); + + // Press 1 + tester.InjectMessages( + 1, WmKeyDownInfo{kVirtualDigit1, kScanCodeDigit1, kNotExtended, kWasUp} + .Build(kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalDigit1, + kLogicalDigit1, "", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(0); + EXPECT_EQ(key_calls.size(), 0); + clear_key_calls(); + + // Release 1 + tester.InjectMessages( + 1, WmKeyUpInfo{kVirtualDigit1, kScanCodeDigit1, kNotExtended}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalDigit1, + kLogicalDigit1, "", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); + + // Release ControlLeft + tester.SetKeyState(VK_LCONTROL, false, true); + tester.InjectMessages( + 1, WmKeyUpInfo{VK_CONTROL, kScanCodeControl, kNotExtended}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, + kPhysicalControlLeft, kLogicalControlLeft, "", + kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); + clear_key_calls(); +} + +// Press 1 on a French keyboard. This is special because it yields WM_CHAR +// with char_code '&'. +TEST(KeyboardTest, Digit1OnFrenchLayout) { + KeyboardTester tester; + tester.Responding(false); + + // French Keyboard layout + + // Press 1 + tester.InjectMessages( + 2, + WmKeyDownInfo{kVirtualDigit1, kScanCodeDigit1, kNotExtended, kWasUp} + .Build(kWmResultZero), + WmCharInfo{'&', kScanCodeDigit1, kNotExtended, kWasUp}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalDigit1, + kLogicalDigit1, "&", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents('&'); + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_TEXT(key_calls[0], u"&"); + clear_key_calls(); + + // Release 1 + tester.InjectMessages( + 1, WmKeyUpInfo{kVirtualDigit1, kScanCodeDigit1, kNotExtended}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalDigit1, + kLogicalDigit1, "", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); +} + +// This tests AltGr-Q on a German keyboard, which should print '@'. +TEST(KeyboardTest, AltGrModifiedKey) { + KeyboardTester tester; + tester.Responding(false); + + // German Keyboard layout + + // Press AltGr, which Win32 precedes with a ContrlLeft down. + tester.SetKeyState(VK_LCONTROL, true, true); + tester.InjectMessages( + 2, + WmKeyDownInfo{VK_LCONTROL, kScanCodeControl, kNotExtended, kWasUp}.Build( + kWmResultZero), + WmKeyDownInfo{VK_MENU, kScanCodeAlt, kExtended, kWasUp}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 2); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, + kPhysicalControlLeft, kLogicalControlLeft, "", + kNotSynthesized); + EXPECT_CALL_IS_EVENT(key_calls[1], kFlutterKeyEventTypeDown, + kPhysicalAltRight, kLogicalAltRight, "", + kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); + clear_key_calls(); + + // Press Q + tester.InjectMessages( + 2, + WmKeyDownInfo{kVirtualKeyQ, kScanCodeKeyQ, kNotExtended, kWasUp}.Build( + kWmResultZero), + WmCharInfo{'@', kScanCodeKeyQ, kNotExtended, kWasUp}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalKeyQ, + kLogicalKeyQ, "@", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents('@'); + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_TEXT(key_calls[0], u"@"); + clear_key_calls(); + + // Release Q + tester.InjectMessages( + 1, WmKeyUpInfo{kVirtualKeyQ, kScanCodeKeyQ, kNotExtended}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalKeyQ, + kLogicalKeyQ, "", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); + + // Release AltGr. Win32 doesn't dispatch ControlLeft up. Instead Flutter will + // dispatch one. + tester.InjectMessages( + 1, WmSysKeyUpInfo{VK_MENU, kScanCodeAlt, kExtended}.Build(kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalAltRight, + kLogicalAltRight, "", kNotSynthesized); + clear_key_calls(); + + tester.SetKeyState(VK_LCONTROL, false, false); + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, + kPhysicalControlLeft, kLogicalControlLeft, "", + kNotSynthesized); + clear_key_calls(); +} + +// This tests dead key ^ then E on a French keyboard, which should be combined +// into ê. +TEST(KeyboardTest, DeadKeyThatCombines) { + KeyboardTester tester; + tester.Responding(false); + + // French Keyboard layout + + // Press ^¨ (US: Left bracket) + tester.InjectMessages( + 2, + WmKeyDownInfo{0xDD, kScanCodeBracketLeft, kNotExtended, kWasUp}.Build( + kWmResultZero), + WmDeadCharInfo{'^', kScanCodeBracketLeft, kNotExtended, kWasUp}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, + kPhysicalBracketLeft, kLogicalBracketRight, "]", + kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(0); // No WM_DEADCHAR messages sent here. + EXPECT_EQ(key_calls.size(), 0); + clear_key_calls(); + + // Release ^¨ + tester.InjectMessages( + 1, WmKeyUpInfo{0xDD, kScanCodeBracketLeft, kNotExtended}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, + kPhysicalBracketLeft, kLogicalBracketRight, "", + kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); + clear_key_calls(); + + // Press E + tester.InjectMessages( + 2, + WmKeyDownInfo{kVirtualKeyE, kScanCodeKeyE, kNotExtended, kWasUp}.Build( + kWmResultZero), + WmCharInfo{0xEA, kScanCodeKeyE, kNotExtended, kWasUp}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalKeyE, + kLogicalKeyE, "ê", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents( + 0xEA); // The redispatched event uses unmodified 'e' + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_TEXT(key_calls[0], u"ê"); + clear_key_calls(); + + // Release E + tester.InjectMessages( + 1, WmKeyUpInfo{kVirtualKeyE, kScanCodeKeyE, kNotExtended}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalKeyE, + kLogicalKeyE, "", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); +} + +// This tests dead key ^ then & (US: 1) on a French keyboard, which do not +// combine and should output "^$". +TEST(KeyboardTest, DeadKeyThatDoesNotCombine) { + KeyboardTester tester; + tester.Responding(false); + + // French Keyboard layout + + // Press ^¨ (US: Left bracket) + tester.InjectMessages( + 2, + WmKeyDownInfo{0xDD, kScanCodeBracketLeft, kNotExtended, kWasUp}.Build( + kWmResultZero), + WmDeadCharInfo{'^', kScanCodeBracketLeft, kNotExtended, kWasUp}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, + kPhysicalBracketLeft, kLogicalBracketRight, "]", + kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(0); // No WM_DEADCHAR messages sent here. + EXPECT_EQ(key_calls.size(), 0); + clear_key_calls(); + + // Release ^¨ + tester.InjectMessages( + 1, WmKeyUpInfo{0xDD, kScanCodeBracketLeft, kNotExtended}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, + kPhysicalBracketLeft, kLogicalBracketRight, "", + kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); + clear_key_calls(); + + // Press 1 + tester.InjectMessages( + 3, + WmKeyDownInfo{kVirtualDigit1, kScanCodeDigit1, kNotExtended, kWasUp} + .Build(kWmResultZero), + WmCharInfo{'^', kScanCodeDigit1, kNotExtended, kWasUp}.Build( + kWmResultZero), + WmCharInfo{'&', kScanCodeDigit1, kNotExtended, kWasUp}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 2); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalDigit1, + kLogicalDigit1, "^", kNotSynthesized); + EXPECT_CALL_IS_TEXT(key_calls[1], u"^"); + clear_key_calls(); + + tester.InjectPendingEvents('&'); + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_TEXT(key_calls[0], u"&"); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); + clear_key_calls(); + + // Release 1 + tester.InjectMessages( + 1, WmKeyUpInfo{kVirtualDigit1, kScanCodeDigit1, kNotExtended}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalDigit1, + kLogicalDigit1, "", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); +} + +// This tests when the resulting character needs to be combined with surrogates. +TEST(KeyboardTest, MultibyteCharacter) { + KeyboardTester tester; + tester.Responding(false); + + // Gothic Keyboard layout. (We need a layout that yields non-BMP characters + // without IME, which that is actually very rare.) + + // Press key W of a US keyboard, which should yield character '𐍅'. + tester.InjectMessages( + 3, + WmKeyDownInfo{kVirtualKeyW, kScanCodeKeyW, kNotExtended, kWasUp}.Build( + kWmResultZero), + WmCharInfo{0xd800, kScanCodeKeyW, kNotExtended, kWasUp}.Build( + kWmResultZero), + WmCharInfo{0xdf45, kScanCodeKeyW, kNotExtended, kWasUp}.Build( + kWmResultZero)); + + const char* st = key_calls[0].key_event.character; + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalKeyW, + kLogicalKeyW, "𐍅", kNotSynthesized); + clear_key_calls(); + + // Inject the redispatched high surrogate. + tester.InjectPendingEvents(0xd800); + // Manually inject the redispatched low surrogate. + tester.InjectMessages( + 1, WmCharInfo{0xdf45, kScanCodeKeyW, kNotExtended, kWasUp}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_TEXT(key_calls[0], u"𐍅"); + clear_key_calls(); + + // Release W + tester.InjectMessages( + 1, WmKeyUpInfo{kVirtualKeyW, kScanCodeKeyW, kNotExtended}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalKeyW, + kLogicalKeyW, "", kNotSynthesized); + clear_key_calls(); + + tester.InjectPendingEvents(); + EXPECT_EQ(key_calls.size(), 0); + clear_key_calls(); +} + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/testing/engine_modifier.h b/shell/platform/windows/testing/engine_modifier.h index 245ad4474d0a5..dee2e6c6c1a96 100644 --- a/shell/platform/windows/testing/engine_modifier.h +++ b/shell/platform/windows/testing/engine_modifier.h @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_ENGINE_MODIFIER_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_ENGINE_MODIFIER_H_ + #include "flutter/shell/platform/windows/flutter_windows_engine.h" namespace flutter { @@ -46,3 +49,5 @@ class EngineModifier { }; } // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_ENGINE_MODIFIER_H_ diff --git a/shell/platform/windows/testing/mock_window_win32.cc b/shell/platform/windows/testing/mock_window_win32.cc index b3e0ac26e3a7c..99cc67898fb37 100644 --- a/shell/platform/windows/testing/mock_window_win32.cc +++ b/shell/platform/windows/testing/mock_window_win32.cc @@ -15,11 +15,25 @@ UINT MockWin32Window::GetDpi() { return GetCurrentDPI(); } +LRESULT MockWin32Window::Win32DefWindowProc(HWND hWnd, + UINT Msg, + WPARAM wParam, + LPARAM lParam) { + return kWmResultDefault; +} + LRESULT MockWin32Window::InjectWindowMessage(UINT const message, WPARAM const wparam, LPARAM const lparam) { return HandleMessage(message, wparam, lparam); } +LRESULT MockWin32Window::Win32SendMessage(HWND hWnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) { + return HandleMessage(message, wparam, lparam); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/windows/testing/mock_window_win32.h b/shell/platform/windows/testing/mock_window_win32.h index e1191a01de499..d3938e7762c59 100644 --- a/shell/platform/windows/testing/mock_window_win32.h +++ b/shell/platform/windows/testing/mock_window_win32.h @@ -7,6 +7,7 @@ #include +#include "flutter/shell/platform/windows/testing/test_keyboard.h" #include "flutter/shell/platform/windows/window_win32.h" #include "gmock/gmock.h" @@ -14,7 +15,7 @@ namespace flutter { namespace testing { /// Mock for the |WindowWin32| base class. -class MockWin32Window : public WindowWin32 { +class MockWin32Window : public WindowWin32, public MockMessageQueue { public: MockWin32Window(); virtual ~MockWin32Window(); @@ -45,7 +46,14 @@ class MockWin32Window : public WindowWin32 { MOCK_METHOD0(OnComposeCommit, void()); MOCK_METHOD0(OnComposeEnd, void()); MOCK_METHOD2(OnComposeChange, void(const std::u16string&, int)); - MOCK_METHOD4(DefaultWindowProc, LRESULT(HWND, UINT, WPARAM, LPARAM)); + + protected: + LRESULT Win32DefWindowProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); + + LRESULT Win32SendMessage(HWND hWnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) override; }; } // namespace testing diff --git a/shell/platform/windows/testing/test_keyboard.cc b/shell/platform/windows/testing/test_keyboard.cc new file mode 100644 index 0000000000000..5cedde95e8ef1 --- /dev/null +++ b/shell/platform/windows/testing/test_keyboard.cc @@ -0,0 +1,217 @@ +// 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/shell/platform/windows/testing/test_keyboard.h" +#include "flutter/shell/platform/common/json_message_codec.h" +#include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" + +#include +#include + +namespace flutter { +namespace testing { + +char* clone_string(const char* string) { + if (string == nullptr) { + return nullptr; + } + size_t len = strlen(string); + char* result = new char[len + 1]; + strcpy(result, string); + return result; +} + +namespace { +std::string _print_character(const char* s) { + if (s == nullptr) { + return "nullptr"; + } + return std::string("\"") + s + "\""; +} + +std::unique_ptr> _keyHandlingResponse(bool handled) { + rapidjson::Document document; + auto& allocator = document.GetAllocator(); + document.SetObject(); + document.AddMember("handled", handled, allocator); + return flutter::JsonMessageCodec::GetInstance().EncodeMessage(document); +} + +// A struct to use as a FlutterPlatformMessageResponseHandle so it can keep the +// callbacks and user data passed to the engine's +// PlatformMessageCreateResponseHandle for use in the SendPlatformMessage +// overridden function. +struct TestResponseHandle { + FlutterDesktopBinaryReply callback; + void* user_data; +}; +} // namespace + +#define _RETURN_IF_NOT_EQUALS(val1, val2) \ + if ((val1) != (val2)) { \ + return ::testing::AssertionFailure() \ + << "Expected equality of these values:\n " #val1 "\n " << val2 \ + << "\n Actual: \n " << val1; \ + } + +::testing::AssertionResult _EventEquals(const char* expr_event, + const char* expr_expected, + const FlutterKeyEvent& event, + const FlutterKeyEvent& expected) { + _RETURN_IF_NOT_EQUALS(event.struct_size, sizeof(FlutterKeyEvent)); + _RETURN_IF_NOT_EQUALS(event.type, expected.type); + _RETURN_IF_NOT_EQUALS(event.physical, expected.physical); + _RETURN_IF_NOT_EQUALS(event.logical, expected.logical); + if ((event.character == nullptr) != (expected.character == nullptr) || + strcmp(event.character, expected.character) != 0) { + return ::testing::AssertionFailure() + << "Expected equality of these values:\n expected.character\n " + << _print_character(expected.character) << "\n Actual: \n " + << _print_character(event.character); + } + _RETURN_IF_NOT_EQUALS(event.synthesized, expected.synthesized); + return ::testing::AssertionSuccess(); +} + +LPARAM CreateKeyEventLparam(USHORT scancode, + bool extended, + bool was_down, + USHORT repeat_count, + bool context_code, + bool transition_state) { + return ((LPARAM(transition_state) << 31) | (LPARAM(was_down) << 30) | + (LPARAM(context_code) << 29) | (LPARAM(extended ? 0x1 : 0x0) << 24) | + (LPARAM(scancode) << 16) | LPARAM(repeat_count)); +} + +static MockKeyEventChannelHandler stored_channel_handler; +static MockKeyEventEmbedderHandler stored_embedder_handler; + +// Set EngineModifier, listen to event messages that go through the channel and +// the embedder API, while disabling other methods so that the engine can be +// run headlessly. +// +// The |channel_handler| and |embedder_handler| should return a boolean +// indicating whether the framework decides to handle the event. +void MockEmbedderApiForKeyboard(EngineModifier& modifier, + MockKeyEventChannelHandler channel_handler, + MockKeyEventEmbedderHandler embedder_handler) { + stored_channel_handler = channel_handler; + stored_embedder_handler = embedder_handler; + // This mock handles channel messages. + modifier.embedder_api().SendPlatformMessage = + [](FLUTTER_API_SYMBOL(FlutterEngine) engine, + const FlutterPlatformMessage* message) { + if (std::string(message->channel) == std::string("flutter/settings")) { + return kSuccess; + } + if (std::string(message->channel) == std::string("flutter/keyevent")) { + bool result = stored_channel_handler(); + auto response = _keyHandlingResponse(result); + const TestResponseHandle* response_handle = + reinterpret_cast( + message->response_handle); + if (response_handle->callback != nullptr) { + response_handle->callback(response->data(), response->size(), + response_handle->user_data); + } + return kSuccess; + } + return kSuccess; + }; + + // This mock handles key events sent through the embedder API, + // and records it in `key_calls`. + modifier.embedder_api().SendKeyEvent = + [](FLUTTER_API_SYMBOL(FlutterEngine) engine, const FlutterKeyEvent* event, + FlutterKeyEventCallback callback, void* user_data) { + bool result = stored_embedder_handler(event); + if (callback != nullptr) { + callback(result, user_data); + } + return kSuccess; + }; + + // The following mocks enable channel mocking. + modifier.embedder_api().PlatformMessageCreateResponseHandle = + [](auto engine, auto data_callback, auto user_data, auto response_out) { + TestResponseHandle* response_handle = new TestResponseHandle(); + response_handle->user_data = user_data; + response_handle->callback = data_callback; + *response_out = reinterpret_cast( + response_handle); + return kSuccess; + }; + + modifier.embedder_api().PlatformMessageReleaseResponseHandle = + [](FLUTTER_API_SYMBOL(FlutterEngine) engine, + FlutterPlatformMessageResponseHandle* response) { + const TestResponseHandle* response_handle = + reinterpret_cast(response); + delete response_handle; + return kSuccess; + }; + + // The following mocks allows RunWithEntrypoint to be run, which creates a + // non-empty FlutterEngine and enables SendKeyEvent. + + modifier.embedder_api().Run = + [](size_t version, const FlutterRendererConfig* config, + const FlutterProjectArgs* args, void* user_data, + FLUTTER_API_SYMBOL(FlutterEngine) * engine_out) { + *engine_out = reinterpret_cast(1); + + return kSuccess; + }; + modifier.embedder_api().UpdateLocales = + [](auto engine, const FlutterLocale** locales, size_t locales_count) { + return kSuccess; + }; + modifier.embedder_api().SendWindowMetricsEvent = + [](auto engine, const FlutterWindowMetricsEvent* event) { + return kSuccess; + }; + modifier.embedder_api().Shutdown = [](auto engine) { return kSuccess; }; +} + +void MockMessageQueue::InjectMessageList(int count, + const Win32Message* messages) { + for (int i = 0; i < count; i += 1) { + _pending_messages.push_back(messages[i]); + } + while (!_pending_messages.empty()) { + Win32Message message = _pending_messages.front(); + _pending_messages.pop_front(); + LRESULT result = Win32SendMessage(message.hWnd, message.message, + message.wParam, message.lParam); + if (message.expected_result != kWmResultDontCheck) { + EXPECT_EQ(result, message.expected_result); + } + } +} + +BOOL MockMessageQueue::Win32PeekMessage(LPMSG lpMsg, + HWND hWnd, + UINT wMsgFilterMin, + UINT wMsgFilterMax, + UINT wRemoveMsg) { + for (auto iter = _pending_messages.begin(); iter != _pending_messages.end(); + ++iter) { + if (iter->message >= wMsgFilterMin && iter->message <= wMsgFilterMax) { + *lpMsg = MSG{ + .message = iter->message, + .wParam = iter->wParam, + .lParam = iter->lParam, + }; + if ((wRemoveMsg & PM_REMOVE) == PM_REMOVE) { + _pending_messages.erase(iter); + } + return TRUE; + } + } + return FALSE; +} + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/testing/test_keyboard.h b/shell/platform/windows/testing/test_keyboard.h new file mode 100644 index 0000000000000..e584f3b22aa2c --- /dev/null +++ b/shell/platform/windows/testing/test_keyboard.h @@ -0,0 +1,96 @@ +// 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_SHELL_PLATFORM_WINDOWS_TESTING_TEST_KEYBOARD_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_TEST_KEYBOARD_H_ + +#include + +#include +#include + +#include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/windows/testing/engine_modifier.h" +#include "flutter/shell/platform/windows/testing/wm_builders.h" + +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +::testing::AssertionResult _EventEquals(const char* expr_event, + const char* expr_expected, + const FlutterKeyEvent& event, + const FlutterKeyEvent& expected); + +// Clone string onto the heap. +// +// If #string is nullptr, returns nullptr. Otherwise, the returned pointer must +// be freed with delete[]. +char* clone_string(const char* string); + +// Creates a valid Windows LPARAM for WM_KEYDOWN and WM_CHAR from parameters +// given. +// +// While |CreateKeyEventLparam| is flexible, it's recommended to use dedicated +// functions in wm_builders.h, such as |WmKeyDownInfo|. +LPARAM CreateKeyEventLparam(USHORT scancode, + bool extended, + bool was_down, + USHORT repeat_count = 1, + bool context_code = 0, + bool transition_state = 1); + +typedef std::function MockKeyEventChannelHandler; +typedef std::function + MockKeyEventEmbedderHandler; + +void MockEmbedderApiForKeyboard(EngineModifier& modifier, + MockKeyEventChannelHandler channel_handler, + MockKeyEventEmbedderHandler embedder_handler); + +// Simulate a message queue for WM messages. +// +// Subclasses must implement |Win32SendMessage| for how dispatched messages are +// processed. +class MockMessageQueue { + public: + // Push a list of messages to the message queue, then dispatch + // them with |Win32SendMessage| one by one. + void InjectMessageList(int count, const Win32Message* messages); + + // Peak the next message in the message queue. + // + // See Win32's |PeekMessage| for documentation. + BOOL Win32PeekMessage(LPMSG lpMsg, + HWND hWnd, + UINT wMsgFilterMin, + UINT wMsgFilterMax, + UINT wRemoveMsg); + + protected: + virtual LRESULT Win32SendMessage(HWND hWnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) = 0; + + std::list _pending_messages; +}; + +} // namespace testing +} // namespace flutter + +// Expect the |_target| FlutterKeyEvent has the required properties. +#define EXPECT_EVENT_EQUALS(_target, _type, _physical, _logical, _character, \ + _synthesized) \ + EXPECT_PRED_FORMAT2(_EventEquals, _target, \ + (FlutterKeyEvent{ \ + .type = _type, \ + .physical = _physical, \ + .logical = _logical, \ + .character = _character, \ + .synthesized = _synthesized, \ + })); + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_TEST_KEYBOARD_H_ diff --git a/shell/platform/windows/testing/test_keyboard_unittests.cc b/shell/platform/windows/testing/test_keyboard_unittests.cc new file mode 100644 index 0000000000000..58f2f59a00ac6 --- /dev/null +++ b/shell/platform/windows/testing/test_keyboard_unittests.cc @@ -0,0 +1,30 @@ +// 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 + +#include "flutter/shell/platform/windows/testing/test_keyboard.h" +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +TEST(TestKeyboard, CloneString) { + const char* str1 = "123"; + char* cloned_str1 = clone_string(str1); + EXPECT_STREQ(str1, cloned_str1); + EXPECT_NE(str1, cloned_str1); + delete[] cloned_str1; + + EXPECT_EQ(clone_string(nullptr), nullptr); +}; + +TEST(TestKeyboard, CreateKeyEventLparam) { + EXPECT_EQ(CreateKeyEventLparam(0x1, true, true), 0xC1010001); + + EXPECT_EQ(CreateKeyEventLparam(0x05, false, false, 0, 1, 0), 0x20050000); +}; + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/testing/wm_builders.cc b/shell/platform/windows/testing/wm_builders.cc new file mode 100644 index 0000000000000..0c13f87149f46 --- /dev/null +++ b/shell/platform/windows/testing/wm_builders.cc @@ -0,0 +1,73 @@ +// 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/shell/platform/windows/testing/wm_builders.h" + +namespace flutter { +namespace testing { + +Win32Message WmKeyDownInfo::Build(LRESULT expected_result, HWND hWnd) { + uint32_t lParam = (repeat_count << 0) | (scan_code << 16) | (extended << 24) | + (prev_state << 30); + return Win32Message{ + .message = WM_KEYDOWN, + .wParam = key, + .lParam = lParam, + .expected_result = expected_result, + .hWnd = hWnd, + }; +} + +Win32Message WmKeyUpInfo::Build(LRESULT expected_result, HWND hWnd) { + uint32_t lParam = (1 /* repeat_count */ << 0) | (scan_code << 16) | + (extended << 24) | (1 /* prev_state */ << 30) | + (1 /* transition */ << 31); + return Win32Message{ + .message = WM_KEYUP, + .wParam = key, + .lParam = lParam, + .expected_result = expected_result, + .hWnd = hWnd, + }; +} + +Win32Message WmCharInfo::Build(LRESULT expected_result, HWND hWnd) { + uint32_t lParam = (repeat_count << 0) | (scan_code << 16) | (extended << 24) | + (context << 30) | (prev_state << 30) | (transition << 31); + return Win32Message{ + .message = WM_CHAR, + .wParam = char_code, + .lParam = lParam, + .expected_result = expected_result, + .hWnd = hWnd, + }; +} + +Win32Message WmSysKeyUpInfo::Build(LRESULT expected_result, HWND hWnd) { + uint32_t lParam = (1 /* repeat_count */ << 0) | (scan_code << 16) | + (extended << 24) | (context << 29) | + (1 /* prev_state */ << 30) | (1 /* transition */ << 31); + return Win32Message{ + .message = WM_SYSKEYUP, + .wParam = key, + .lParam = lParam, + .expected_result = expected_result, + .hWnd = hWnd, + }; +} + +Win32Message WmDeadCharInfo::Build(LRESULT expected_result, HWND hWnd) { + uint32_t lParam = (repeat_count << 0) | (scan_code << 16) | (extended << 24) | + (context << 30) | (prev_state << 30) | (transition << 31); + return Win32Message{ + .message = WM_DEADCHAR, + .wParam = char_code, + .lParam = lParam, + .expected_result = expected_result, + .hWnd = hWnd, + }; +} + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/testing/wm_builders.h b/shell/platform/windows/testing/wm_builders.h new file mode 100644 index 0000000000000..56a0b3e29858c --- /dev/null +++ b/shell/platform/windows/testing/wm_builders.h @@ -0,0 +1,165 @@ +// 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_SHELL_PLATFORM_WINDOWS_TESTING_WM_BUILDERS_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_WM_BUILDERS_H_ + +#include +#include +#include + +namespace flutter { +namespace testing { + +constexpr LRESULT kWmResultZero = 0; +constexpr LRESULT kWmResultDefault = 0xDEADC0DE; +constexpr LRESULT kWmResultDontCheck = 0xFFFF1234; + +// A struc to hold simulated events that will be delivered after the framework +// response is handled. +struct Win32Message { + UINT message; + WPARAM wParam; + LPARAM lParam; + LRESULT expected_result; + HWND hWnd; +}; + +typedef enum { + kNotExtended = 0, + kExtended = 1, +} WmFieldExtended; + +typedef enum { + kNoContext = 0, + kAltHeld = 1, +} WmFieldContext; + +typedef enum { + kWasUp = 0, + kWasDown = 1, +} WmFieldPrevState; + +typedef enum { + kBeingReleased = 0, + kBeingPressed = 1, +} WmFieldTransitionState; + +// WM_KEYDOWN messages. +// +// See https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keydown. +typedef struct { + uint32_t key; + + uint8_t scan_code; + + WmFieldExtended extended; + + WmFieldPrevState prev_state; + + // WmFieldTransitionState transition; // Always 0. + + // WmFieldContext context; // Always 0. + + uint16_t repeat_count = 1; + + Win32Message Build(LRESULT expected_result = kWmResultDontCheck, + HWND hWnd = NULL); +} WmKeyDownInfo; + +// Win32Message BuildMessage(WmKeyDownInfo info, LRESULT expected_result = +// kWmResultDontCheck, HWND hWnd = NULL); + +// WM_KEYUP messages. +// +// See https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keyup. +typedef struct { + uint32_t key; + + uint8_t scan_code; + + WmFieldExtended extended; + + // WmFieldPrevState prev_state; // Always 1. + + // WmFieldTransitionState transition; // Always 1. + + // WmFieldContext context; // Always 0. + + // uint16_t repeat_count; // Always 1. + + Win32Message Build(LRESULT expected_result = kWmResultDontCheck, + HWND hWnd = NULL); +} WmKeyUpInfo; + +// WM_CHAR messages. +// +// See https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-char. +typedef struct { + uint32_t char_code; + + uint8_t scan_code; + + WmFieldExtended extended; + + WmFieldPrevState prev_state; + + WmFieldTransitionState transition; + + WmFieldContext context; + + uint16_t repeat_count = 1; + + Win32Message Build(LRESULT expected_result = kWmResultDontCheck, + HWND hWnd = NULL); +} WmCharInfo; + +// WM_SYSKEYUP messages. +// +// See https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-syskeyup. +typedef struct { + uint32_t key; + + uint8_t scan_code; + + WmFieldExtended extended; + + // WmFieldPrevState prev_state; // Always 1. + + // WmFieldTransitionState transition; // Always 1. + + WmFieldContext context; + + // uint16_t repeat_count; // Always 1. + + Win32Message Build(LRESULT expected_result = kWmResultDontCheck, + HWND hWnd = NULL); +} WmSysKeyUpInfo; + +// WM_DEADCHAR messages. +// +// See https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-deadchar. +typedef struct { + uint32_t char_code; + + uint8_t scan_code; + + WmFieldExtended extended; + + WmFieldPrevState prev_state; + + WmFieldTransitionState transition; + + WmFieldContext context; + + uint16_t repeat_count = 1; + + Win32Message Build(LRESULT expected_result = kWmResultDontCheck, + HWND hWnd = NULL); +} WmDeadCharInfo; + +} // namespace testing +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_WM_BUILDERS_H_ diff --git a/shell/platform/windows/window_win32.cc b/shell/platform/windows/window_win32.cc index f7c8e231a3a48..d20a166bdd157 100644 --- a/shell/platform/windows/window_win32.cc +++ b/shell/platform/windows/window_win32.cc @@ -172,6 +172,30 @@ void WindowWin32::UpdateCursorRect(const Rect& rect) { text_input_manager_.UpdateCaretRect(rect); } +static uint16_t ResolveKeyCode(uint16_t original, + bool extended, + uint8_t scancode) { + switch (original) { + case VK_SHIFT: + case VK_LSHIFT: + return MapVirtualKey(scancode, MAPVK_VSC_TO_VK_EX); + case VK_MENU: + case VK_LMENU: + return extended ? VK_RMENU : VK_LMENU; + case VK_CONTROL: + case VK_LCONTROL: + return extended ? VK_RCONTROL : VK_LCONTROL; + default: + return original; + } +} + +static bool IsPrintable(uint32_t c) { + constexpr char32_t kMinPrintable = ' '; + constexpr char32_t kDelete = 0x7F; + return c >= kMinPrintable && c != kDelete; +} + LRESULT WindowWin32::HandleMessage(UINT const message, WPARAM const wparam, @@ -329,22 +353,46 @@ WindowWin32::HandleMessage(UINT const message, s_pending_high_surrogate = 0; } + const unsigned int scancode = (lparam >> 16) & 0xff; + // All key presses that generate a character should be sent from // WM_CHAR. In order to send the full key press information, the keycode // is persisted in keycode_for_char_message_ obtained from WM_KEYDOWN. - if (keycode_for_char_message_ != 0) { - const unsigned int scancode = (lparam >> 16) & 0xff; + // + // A high surrogate is always followed by a low surrogate, while a + // non-surrogate character always appears alone. Filter out high + // surrogates so that it's the low surrogate message that triggers + // the onKey, asks if the framework handles it (which can only be done + // once), and calls OnText during the redispatched messages. + if (keycode_for_char_message_ != 0 && !IS_HIGH_SURROGATE(character)) { const bool extended = ((lparam >> 24) & 0x01) == 0x01; const bool was_down = lparam & 0x40000000; + // Certain key combinations yield control characters as WM_CHAR's + // lParam. For example, 0x01 for Ctrl-A. Filter these characters. + // See + // https://docs.microsoft.com/en-us/windows/win32/learnwin32/accelerator-tables + const char32_t event_character = + (message == WM_DEADCHAR || message == WM_SYSDEADCHAR) + ? MapVirtualKey(keycode_for_char_message_, MAPVK_VK_TO_CHAR) + : IsPrintable(code_point) ? code_point + : 0; bool handled = OnKey(keycode_for_char_message_, scancode, WM_KEYDOWN, - code_point, extended, was_down); + event_character, extended, was_down); keycode_for_char_message_ = 0; if (handled) { // If the OnKey handler handles the message, then return so we don't // pass it to OnText, because handling the message indicates that - // OnKey either just sent it to the framework to be processed, or the - // framework handled the key in its response, so it shouldn't also be - // added as text. + // OnKey either just sent it to the framework to be processed. + // + // This message will be redispatched if not handled by the framework, + // during which the OnText (below) might be reached. However, if the + // original message was preceded by dead chars (such as ^ and e + // yielding ê), then since the redispatched message is no longer + // preceded by the dead char, the text will be wrong. Therefore we + // record the text here for the redispached event to use. + if (message == WM_CHAR) { + text_for_scancode_on_redispatch_[scancode] = text; + } return 0; } } @@ -357,10 +405,15 @@ WindowWin32::HandleMessage(UINT const message, // - ASCII control characters, which are sent as WM_CHAR events for all // control key shortcuts. if (message == WM_CHAR && s_pending_high_surrogate == 0 && - character >= u' ') { + IsPrintable(character)) { + auto found_text_iter = text_for_scancode_on_redispatch_.find(scancode); + if (found_text_iter != text_for_scancode_on_redispatch_.end()) { + text = found_text_iter->second; + text_for_scancode_on_redispatch_.erase(found_text_iter); + } OnText(text); } - break; + return 0; } case WM_KEYDOWN: case WM_SYSKEYDOWN: @@ -377,18 +430,20 @@ WindowWin32::HandleMessage(UINT const message, // to properly produce key down events even though `MapVirtualKey` returns // a valid character. See https://github.com/flutter/flutter/issues/85587. unsigned int character = MapVirtualKey(wparam, MAPVK_VK_TO_CHAR); - if (character > 0 && is_keydown_message && GetKeyState(VK_CONTROL) >= 0 && - GetKeyState(VK_LWIN) >= 0 && GetKeyState(VK_RWIN) >= 0) { + UINT next_key_message = PeekNextMessageType(WM_KEYFIRST, WM_KEYLAST); + bool has_wm_char = + (next_key_message == WM_DEADCHAR || + next_key_message == WM_SYSDEADCHAR || next_key_message == WM_CHAR || + next_key_message == WM_SYSCHAR); + if (character > 0 && is_keydown_message && has_wm_char) { keycode_for_char_message_ = wparam; - break; + return 0; } unsigned int keyCode(wparam); - const unsigned int scancode = (lparam >> 16) & 0xff; + const uint8_t scancode = (lparam >> 16) & 0xff; const bool extended = ((lparam >> 24) & 0x01) == 0x01; // If the key is a modifier, get its side. - if (keyCode == VK_SHIFT || keyCode == VK_MENU || keyCode == VK_CONTROL) { - keyCode = MapVirtualKey(scancode, MAPVK_VSC_TO_VK_EX); - } + keyCode = ResolveKeyCode(keyCode, extended, scancode); const int action = is_keydown_message ? WM_KEYDOWN : WM_KEYUP; const bool was_down = lparam & 0x40000000; if (OnKey(keyCode, scancode, action, 0, extended, was_down)) { @@ -397,7 +452,7 @@ WindowWin32::HandleMessage(UINT const message, break; } - return DefWindowProc(window_handle_, message, wparam, result_lparam); + return Win32DefWindowProc(window_handle_, message, wparam, result_lparam); } UINT WindowWin32::GetCurrentDPI() { @@ -431,16 +486,34 @@ void WindowWin32::HandleResize(UINT width, UINT height) { OnResize(width, height); } +UINT WindowWin32::PeekNextMessageType(UINT wMsgFilterMin, UINT wMsgFilterMax) { + MSG next_message; + BOOL has_msg = Win32PeekMessage(&next_message, window_handle_, wMsgFilterMin, + wMsgFilterMax, PM_NOREMOVE); + if (!has_msg) { + return 0; + } + return next_message.message; +} + WindowWin32* WindowWin32::GetThisFromHandle(HWND const window) noexcept { return reinterpret_cast( GetWindowLongPtr(window, GWLP_USERDATA)); } -LRESULT WindowWin32::DefaultWindowProc(HWND hWnd, - UINT Msg, - WPARAM wParam, - LPARAM lParam) { +LRESULT WindowWin32::Win32DefWindowProc(HWND hWnd, + UINT Msg, + WPARAM wParam, + LPARAM lParam) { return DefWindowProc(hWnd, Msg, wParam, lParam); } +BOOL WindowWin32::Win32PeekMessage(LPMSG lpMsg, + HWND hWnd, + UINT wMsgFilterMin, + UINT wMsgFilterMax, + UINT wRemoveMsg) { + return PeekMessage(lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax, wRemoveMsg); +} + } // namespace flutter diff --git a/shell/platform/windows/window_win32.h b/shell/platform/windows/window_win32.h index 37d8d9aa18719..5fcedeaf0d622 100644 --- a/shell/platform/windows/window_win32.h +++ b/shell/platform/windows/window_win32.h @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -159,7 +160,23 @@ class WindowWin32 { UINT GetCurrentHeight(); protected: - LRESULT DefaultWindowProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); + // Win32's DefWindowProc. + // + // Used as the fallback behavior of HandleMessage. Exposed for dependency + // injection. + virtual LRESULT Win32DefWindowProc(HWND hWnd, + UINT Msg, + WPARAM wParam, + LPARAM lParam); + + // Win32's PeekMessage. + // + // Used to process key messages. Exposed for dependency injection. + virtual BOOL Win32PeekMessage(LPMSG lpMsg, + HWND hWnd, + UINT wMsgFilterMin, + UINT wMsgFilterMax, + UINT wRemoveMsg); private: // Release OS resources associated with window. @@ -171,8 +188,19 @@ class WindowWin32 { // Stores new width and height and calls |OnResize| to notify inheritors void HandleResize(UINT width, UINT height); + // Returns the type of the next WM message. + // + // The parameters limits the range of interested messages. See Win32's + // |PeekMessage| for information. + // + // If there's no message, returns 0. + // + // The behavior can be mocked by replacing |Win32PeekMessage|. + UINT PeekNextMessageType(UINT wMsgFilterMin, UINT wMsgFilterMax); + // Retrieves a class instance pointer for |window| static WindowWin32* GetThisFromHandle(HWND const window) noexcept; + int current_dpi_ = 0; int current_width_ = 0; int current_height_ = 0; @@ -194,6 +222,8 @@ class WindowWin32 { // message. int keycode_for_char_message_ = 0; + std::map text_for_scancode_on_redispatch_; + // Manages IME state. TextInputManagerWin32 text_input_manager_; }; diff --git a/shell/platform/windows/window_win32_unittests.cc b/shell/platform/windows/window_win32_unittests.cc index ce54155cd7a96..58f9e027a87e7 100644 --- a/shell/platform/windows/window_win32_unittests.cc +++ b/shell/platform/windows/window_win32_unittests.cc @@ -9,22 +9,6 @@ using testing::_; namespace flutter { namespace testing { -namespace { - -// Creates a valid Windows LPARAM for WM_KEYDOWN and WM_KEYUP from parameters -// given. -static LPARAM CreateKeyEventLparam(USHORT scancode, - bool extended = false, - bool was_down = 1, - USHORT repeat_count = 1, - bool context_code = 0, - bool transition_state = 1) { - return ((LPARAM(transition_state) << 31) | (LPARAM(was_down) << 30) | - (LPARAM(context_code) << 29) | (LPARAM(extended ? 0x1 : 0x0) << 24) | - (LPARAM(scancode) << 16) | LPARAM(repeat_count)); -} - -} // namespace TEST(MockWin32Window, CreateDestroy) { MockWin32Window window; @@ -58,7 +42,7 @@ TEST(MockWin32Window, HorizontalScroll) { TEST(MockWin32Window, KeyDown) { MockWin32Window window; EXPECT_CALL(window, OnKey(_, _, _, _, _, _)).Times(1); - LPARAM lparam = CreateKeyEventLparam(42); + LPARAM lparam = CreateKeyEventLparam(42, false, false); // send a "Shift" key down event. window.InjectWindowMessage(WM_KEYDOWN, 16, lparam); } @@ -66,22 +50,20 @@ TEST(MockWin32Window, KeyDown) { TEST(MockWin32Window, KeyUp) { MockWin32Window window; EXPECT_CALL(window, OnKey(_, _, _, _, _, _)).Times(1); - LPARAM lparam = CreateKeyEventLparam(42); + LPARAM lparam = CreateKeyEventLparam(42, false, true); // send a "Shift" key up event. window.InjectWindowMessage(WM_KEYUP, 16, lparam); } TEST(MockWin32Window, KeyDownPrintable) { MockWin32Window window; - LPARAM lparam = CreateKeyEventLparam(30); - // OnKey shouldn't be called until the WM_CHAR message. - EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 65, false, true)).Times(0); - // send a "A" key down event. - window.InjectWindowMessage(WM_KEYDOWN, 65, lparam); + LPARAM lparam = CreateKeyEventLparam(30, false, false); - EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 65, false, true)).Times(1); + EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 0, false, false)).Times(1); EXPECT_CALL(window, OnText(_)).Times(1); - window.InjectWindowMessage(WM_CHAR, 65, lparam); + Win32Message messages[] = {{WM_KEYDOWN, 65, lparam, kWmResultDontCheck}, + {WM_CHAR, 65, lparam, kWmResultDontCheck}}; + window.InjectMessageList(2, messages); } TEST(MockWin32Window, KeyDownWithCtrl) { @@ -93,11 +75,11 @@ TEST(MockWin32Window, KeyDownWithCtrl) { keyboard_state[VK_CONTROL] = -1; SetKeyboardState(keyboard_state); - LPARAM lparam = CreateKeyEventLparam(30); + LPARAM lparam = CreateKeyEventLparam(30, false, false); // Expect OnKey, but not OnText, because Control + Key is not followed by // WM_CHAR - EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 0, false, true)).Times(1); + EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 0, false, false)).Times(1); EXPECT_CALL(window, OnText(_)).Times(0); window.InjectWindowMessage(WM_KEYDOWN, 65, lparam); @@ -115,16 +97,15 @@ TEST(MockWin32Window, KeyDownWithCtrlToggled) { keyboard_state[VK_CONTROL] = 1; SetKeyboardState(keyboard_state); - LPARAM lparam = CreateKeyEventLparam(30); + LPARAM lparam = CreateKeyEventLparam(30, false, false); - // OnKey shouldn't be called until the WM_CHAR message. - EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 65, false, true)).Times(0); - // send a "A" key down event. - window.InjectWindowMessage(WM_KEYDOWN, 65, lparam); - - EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 65, false, true)).Times(1); + EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 0, false, false)).Times(1); EXPECT_CALL(window, OnText(_)).Times(1); - window.InjectWindowMessage(WM_CHAR, 65, lparam); + + // send a "A" key down event. + Win32Message messages[] = {{WM_KEYDOWN, 65, lparam, kWmResultDontCheck}, + {WM_CHAR, 65, lparam, kWmResultDontCheck}}; + window.InjectMessageList(2, messages); memset(keyboard_state, 0, 256); SetKeyboardState(keyboard_state);