diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 22d3e68741eb6..d6ba42412eeee 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -7160,7 +7160,6 @@ ORIGIN: ../../../flutter/shell/platform/windows/text_input_manager.cc + ../../.. ORIGIN: ../../../flutter/shell/platform/windows/text_input_manager.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/text_input_plugin.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/text_input_plugin.h + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/shell/platform/windows/text_input_plugin_delegate.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/window_binding_handler.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/window_binding_handler_delegate.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/window_proc_delegate_manager.cc + ../../../flutter/LICENSE @@ -9978,7 +9977,6 @@ FILE: ../../../flutter/shell/platform/windows/text_input_manager.cc FILE: ../../../flutter/shell/platform/windows/text_input_manager.h FILE: ../../../flutter/shell/platform/windows/text_input_plugin.cc FILE: ../../../flutter/shell/platform/windows/text_input_plugin.h -FILE: ../../../flutter/shell/platform/windows/text_input_plugin_delegate.h FILE: ../../../flutter/shell/platform/windows/window_binding_handler.h FILE: ../../../flutter/shell/platform/windows/window_binding_handler_delegate.h FILE: ../../../flutter/shell/platform/windows/window_proc_delegate_manager.cc diff --git a/shell/platform/windows/cursor_handler_unittests.cc b/shell/platform/windows/cursor_handler_unittests.cc index d3f4ed5928af0..fac458beefcb8 100644 --- a/shell/platform/windows/cursor_handler_unittests.cc +++ b/shell/platform/windows/cursor_handler_unittests.cc @@ -63,13 +63,13 @@ class CursorHandlerTest : public WindowsTest { FlutterWindowsView* view() { return view_.get(); } MockWindowBindingHandler* window() { return window_; } - void use_headless_engine() { + void UseHeadlessEngine() { FlutterWindowsEngineBuilder builder{GetContext()}; engine_ = builder.Build(); } - void use_engine_with_view() { + void UseEngineWithView() { FlutterWindowsEngineBuilder builder{GetContext()}; auto window = std::make_unique(); @@ -93,7 +93,7 @@ class CursorHandlerTest : public WindowsTest { }; TEST_F(CursorHandlerTest, ActivateSystemCursor) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; CursorHandler cursor_handler(&messenger, engine()); @@ -119,7 +119,7 @@ TEST_F(CursorHandlerTest, ActivateSystemCursor) { } TEST_F(CursorHandlerTest, ActivateSystemCursorRequiresView) { - use_headless_engine(); + UseHeadlessEngine(); TestBinaryMessenger messenger; CursorHandler cursor_handler(&messenger, engine()); @@ -146,7 +146,7 @@ TEST_F(CursorHandlerTest, ActivateSystemCursorRequiresView) { } TEST_F(CursorHandlerTest, CreateCustomCursor) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; CursorHandler cursor_handler(&messenger, engine()); @@ -177,7 +177,7 @@ TEST_F(CursorHandlerTest, CreateCustomCursor) { } TEST_F(CursorHandlerTest, SetCustomCursor) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; CursorHandler cursor_handler(&messenger, engine()); @@ -217,7 +217,7 @@ TEST_F(CursorHandlerTest, SetCustomCursor) { } TEST_F(CursorHandlerTest, SetCustomCursorRequiresView) { - use_headless_engine(); + UseHeadlessEngine(); TestBinaryMessenger messenger; CursorHandler cursor_handler(&messenger, engine()); @@ -258,7 +258,7 @@ TEST_F(CursorHandlerTest, SetCustomCursorRequiresView) { } TEST_F(CursorHandlerTest, SetNonexistentCustomCursor) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; CursorHandler cursor_handler(&messenger, engine()); @@ -287,7 +287,7 @@ TEST_F(CursorHandlerTest, SetNonexistentCustomCursor) { } TEST_F(CursorHandlerTest, DeleteCustomCursor) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; CursorHandler cursor_handler(&messenger, engine()); @@ -325,7 +325,7 @@ TEST_F(CursorHandlerTest, DeleteCustomCursor) { } TEST_F(CursorHandlerTest, DeleteNonexistentCustomCursor) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; CursorHandler cursor_handler(&messenger, engine()); diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc index 348ea7b773ecd..fa43c05749017 100644 --- a/shell/platform/windows/flutter_windows_engine.cc +++ b/shell/platform/windows/flutter_windows_engine.cc @@ -666,7 +666,7 @@ FlutterWindowsEngine::CreateKeyboardKeyHandler( std::unique_ptr FlutterWindowsEngine::CreateTextInputPlugin( BinaryMessenger* messenger) { - return std::make_unique(messenger, view_); + return std::make_unique(messenger, this); } bool FlutterWindowsEngine::RegisterExternalTexture(int64_t texture_id) { diff --git a/shell/platform/windows/flutter_windows_view.h b/shell/platform/windows/flutter_windows_view.h index a8871c9f5a10a..3f11677902576 100644 --- a/shell/platform/windows/flutter_windows_view.h +++ b/shell/platform/windows/flutter_windows_view.h @@ -20,7 +20,6 @@ #include "flutter/shell/platform/windows/angle_surface_manager.h" #include "flutter/shell/platform/windows/flutter_windows_engine.h" #include "flutter/shell/platform/windows/public/flutter_windows.h" -#include "flutter/shell/platform/windows/text_input_plugin_delegate.h" #include "flutter/shell/platform/windows/window_binding_handler.h" #include "flutter/shell/platform/windows/window_binding_handler_delegate.h" #include "flutter/shell/platform/windows/window_state.h" @@ -30,10 +29,9 @@ namespace flutter { // ID for the window frame buffer. inline constexpr uint32_t kWindowFrameBufferID = 0; -// An OS-windowing neutral abstration for flutter -// view that works with win32 hwnds and Windows::UI::Composition visuals. -class FlutterWindowsView : public WindowBindingHandlerDelegate, - public TextInputPluginDelegate { +// An OS-windowing neutral abstration for a Flutter view that works +// with win32 HWNDs. +class FlutterWindowsView : public WindowBindingHandlerDelegate { public: // Creates a FlutterWindowsView with the given implementor of // WindowBindingHandler. @@ -186,11 +184,12 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, // |WindowBindingHandlerDelegate| virtual gfx::NativeViewAccessible GetNativeViewAccessible() override; - // |TextInputPluginDelegate| - void OnCursorRectUpdated(const Rect& rect) override; + // Notifies the delegate of the updated the cursor rect in Flutter root view + // coordinates. + virtual void OnCursorRectUpdated(const Rect& rect); - // |TextInputPluginDelegate| - void OnResetImeComposing() override; + // Notifies the delegate that the system IME composing state should be reset. + virtual void OnResetImeComposing(); // Called when a WM_ONCOMPOSITIONCHANGED message is received. void OnDwmCompositionChanged(); diff --git a/shell/platform/windows/platform_handler_unittests.cc b/shell/platform/windows/platform_handler_unittests.cc index d900cc48a30fa..35cb70368c1ec 100644 --- a/shell/platform/windows/platform_handler_unittests.cc +++ b/shell/platform/windows/platform_handler_unittests.cc @@ -142,13 +142,13 @@ class PlatformHandlerTest : public WindowsTest { protected: FlutterWindowsEngine* engine() { return engine_.get(); } - void use_headless_engine() { + void UseHeadlessEngine() { FlutterWindowsEngineBuilder builder{GetContext()}; engine_ = builder.Build(); } - void use_engine_with_view() { + void UseEngineWithView() { FlutterWindowsEngineBuilder builder{GetContext()}; auto window = std::make_unique>(); @@ -166,7 +166,7 @@ class PlatformHandlerTest : public WindowsTest { }; TEST_F(PlatformHandlerTest, GetClipboardData) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine(), []() { @@ -190,7 +190,7 @@ TEST_F(PlatformHandlerTest, GetClipboardData) { } TEST_F(PlatformHandlerTest, GetClipboardDataRejectsUnknownContentType) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine()); @@ -203,7 +203,7 @@ TEST_F(PlatformHandlerTest, GetClipboardDataRejectsUnknownContentType) { } TEST_F(PlatformHandlerTest, GetClipboardDataRequiresView) { - use_headless_engine(); + UseHeadlessEngine(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine()); @@ -217,7 +217,7 @@ TEST_F(PlatformHandlerTest, GetClipboardDataRequiresView) { } TEST_F(PlatformHandlerTest, GetClipboardDataReportsOpenFailure) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine(), []() { @@ -237,7 +237,7 @@ TEST_F(PlatformHandlerTest, GetClipboardDataReportsOpenFailure) { } TEST_F(PlatformHandlerTest, GetClipboardDataReportsGetDataFailure) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine(), []() { @@ -261,7 +261,7 @@ TEST_F(PlatformHandlerTest, GetClipboardDataReportsGetDataFailure) { } TEST_F(PlatformHandlerTest, ClipboardHasStrings) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine(), []() { @@ -282,7 +282,7 @@ TEST_F(PlatformHandlerTest, ClipboardHasStrings) { } TEST_F(PlatformHandlerTest, ClipboardHasStringsReturnsFalse) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine(), []() { @@ -303,7 +303,7 @@ TEST_F(PlatformHandlerTest, ClipboardHasStringsReturnsFalse) { } TEST_F(PlatformHandlerTest, ClipboardHasStringsRejectsUnknownContentType) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine()); @@ -315,7 +315,7 @@ TEST_F(PlatformHandlerTest, ClipboardHasStringsRejectsUnknownContentType) { } TEST_F(PlatformHandlerTest, ClipboardHasStringsRequiresView) { - use_headless_engine(); + UseHeadlessEngine(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine()); @@ -330,7 +330,7 @@ TEST_F(PlatformHandlerTest, ClipboardHasStringsRequiresView) { // Regression test for https://github.com/flutter/flutter/issues/95817. TEST_F(PlatformHandlerTest, ClipboardHasStringsIgnoresPermissionErrors) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine(), []() { @@ -350,7 +350,7 @@ TEST_F(PlatformHandlerTest, ClipboardHasStringsIgnoresPermissionErrors) { } TEST_F(PlatformHandlerTest, ClipboardHasStringsReportsErrors) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine(), []() { @@ -370,7 +370,7 @@ TEST_F(PlatformHandlerTest, ClipboardHasStringsReportsErrors) { } TEST_F(PlatformHandlerTest, ClipboardSetData) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine(), []() { @@ -397,7 +397,7 @@ TEST_F(PlatformHandlerTest, ClipboardSetData) { // Regression test for: https://github.com/flutter/flutter/issues/121976 TEST_F(PlatformHandlerTest, ClipboardSetDataTextMustBeString) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine()); @@ -409,7 +409,7 @@ TEST_F(PlatformHandlerTest, ClipboardSetDataTextMustBeString) { } TEST_F(PlatformHandlerTest, ClipboardSetDataUnknownType) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine()); @@ -421,7 +421,7 @@ TEST_F(PlatformHandlerTest, ClipboardSetDataUnknownType) { } TEST_F(PlatformHandlerTest, ClipboardSetDataRequiresView) { - use_headless_engine(); + UseHeadlessEngine(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine()); @@ -435,7 +435,7 @@ TEST_F(PlatformHandlerTest, ClipboardSetDataRequiresView) { } TEST_F(PlatformHandlerTest, ClipboardSetDataReportsOpenFailure) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine(), []() { @@ -455,7 +455,7 @@ TEST_F(PlatformHandlerTest, ClipboardSetDataReportsOpenFailure) { } TEST_F(PlatformHandlerTest, ClipboardSetDataReportsSetDataFailure) { - use_engine_with_view(); + UseEngineWithView(); TestBinaryMessenger messenger; PlatformHandler platform_handler(&messenger, engine(), []() { @@ -478,7 +478,7 @@ TEST_F(PlatformHandlerTest, ClipboardSetDataReportsSetDataFailure) { } TEST_F(PlatformHandlerTest, PlaySystemSound) { - use_headless_engine(); + UseHeadlessEngine(); TestBinaryMessenger messenger; MockPlatformHandler platform_handler(&messenger, engine()); @@ -496,7 +496,7 @@ TEST_F(PlatformHandlerTest, PlaySystemSound) { } TEST_F(PlatformHandlerTest, SystemExitApplicationRequired) { - use_headless_engine(); + UseHeadlessEngine(); UINT exit_code = 0; TestBinaryMessenger messenger([](const std::string& channel, @@ -518,7 +518,7 @@ TEST_F(PlatformHandlerTest, SystemExitApplicationRequired) { } TEST_F(PlatformHandlerTest, SystemExitApplicationCancelableCancel) { - use_headless_engine(); + UseHeadlessEngine(); bool called_cancel = false; TestBinaryMessenger messenger( @@ -539,7 +539,7 @@ TEST_F(PlatformHandlerTest, SystemExitApplicationCancelableCancel) { } TEST_F(PlatformHandlerTest, SystemExitApplicationCancelableExit) { - use_headless_engine(); + UseHeadlessEngine(); bool called_cancel = false; UINT exit_code = 0; diff --git a/shell/platform/windows/text_input_plugin.cc b/shell/platform/windows/text_input_plugin.cc index 08c0007a4d083..9cf1229b5b8f5 100644 --- a/shell/platform/windows/text_input_plugin.cc +++ b/shell/platform/windows/text_input_plugin.cc @@ -3,15 +3,16 @@ // found in the LICENSE file. #include "flutter/shell/platform/windows/text_input_plugin.h" -#include "flutter/fml/string_conversion.h" -#include "flutter/shell/platform/common/text_editing_delta.h" -#include "flutter/shell/platform/windows/text_input_plugin_delegate.h" #include #include +#include "flutter/fml/string_conversion.h" #include "flutter/shell/platform/common/json_method_codec.h" +#include "flutter/shell/platform/common/text_editing_delta.h" +#include "flutter/shell/platform/windows/flutter_windows_engine.h" +#include "flutter/shell/platform/windows/flutter_windows_view.h" static constexpr char kSetEditingStateMethod[] = "TextInput.setEditingState"; static constexpr char kClearClientMethod[] = "TextInput.clearClient"; @@ -104,12 +105,12 @@ void TextInputPlugin::KeyboardHook(int key, } TextInputPlugin::TextInputPlugin(flutter::BinaryMessenger* messenger, - TextInputPluginDelegate* delegate) + FlutterWindowsEngine* engine) : channel_(std::make_unique>( messenger, kChannelName, &flutter::JsonMethodCodec::GetInstance())), - delegate_(delegate), + engine_(engine), active_model_(nullptr) { channel_->SetMethodCallHandler( [this]( @@ -217,12 +218,18 @@ void TextInputPlugin::HandleMethodCall( if (method.compare(kShowMethod) == 0 || method.compare(kHideMethod) == 0) { // These methods are no-ops. } else if (method.compare(kClearClientMethod) == 0) { + FlutterWindowsView* view = engine_->view(); + if (view == nullptr) { + result->Error(kInternalConsistencyError, + "Text input is not available in Windows headless mode"); + return; + } if (active_model_ != nullptr && active_model_->composing()) { active_model_->CommitComposing(); active_model_->EndComposing(); SendStateUpdate(*active_model_); } - delegate_->OnResetImeComposing(); + view->OnResetImeComposing(); active_model_ = nullptr; } else if (method.compare(kSetClientMethod) == 0) { if (!method_call.arguments() || method_call.arguments()->IsNull()) { @@ -321,6 +328,12 @@ void TextInputPlugin::HandleMethodCall( TextRange(composing_base, composing_extent), cursor_offset); } } else if (method.compare(kSetMarkedTextRect) == 0) { + FlutterWindowsView* view = engine_->view(); + if (view == nullptr) { + result->Error(kInternalConsistencyError, + "Text input is not available in Windows headless mode"); + return; + } if (!method_call.arguments() || method_call.arguments()->IsNull()) { result->Error(kBadArgumentError, "Method invoked without args"); return; @@ -342,8 +355,14 @@ void TextInputPlugin::HandleMethodCall( {width->value.GetDouble(), height->value.GetDouble()}}; Rect transformed_rect = GetCursorRect(); - delegate_->OnCursorRectUpdated(transformed_rect); + view->OnCursorRectUpdated(transformed_rect); } else if (method.compare(kSetEditableSizeAndTransform) == 0) { + FlutterWindowsView* view = engine_->view(); + if (view == nullptr) { + result->Error(kInternalConsistencyError, + "Text input is not available in Windows headless mode"); + return; + } if (!method_call.arguments() || method_call.arguments()->IsNull()) { result->Error(kBadArgumentError, "Method invoked without args"); return; @@ -367,7 +386,7 @@ void TextInputPlugin::HandleMethodCall( ++i; } Rect transformed_rect = GetCursorRect(); - delegate_->OnCursorRectUpdated(transformed_rect); + view->OnCursorRectUpdated(transformed_rect); } else { result->NotImplemented(); return; diff --git a/shell/platform/windows/text_input_plugin.h b/shell/platform/windows/text_input_plugin.h index a5ee04b08baeb..a452e293d6808 100644 --- a/shell/platform/windows/text_input_plugin.h +++ b/shell/platform/windows/text_input_plugin.h @@ -20,18 +20,19 @@ namespace flutter { -class TextInputPluginDelegate; +class FlutterWindowsEngine; // Implements a text input plugin. // // Specifically handles window events within windows. class TextInputPlugin { public: - explicit TextInputPlugin(flutter::BinaryMessenger* messenger, - TextInputPluginDelegate* delegate); + TextInputPlugin(flutter::BinaryMessenger* messenger, + FlutterWindowsEngine* engine); virtual ~TextInputPlugin(); + // Called when the Flutter engine receives a raw keyboard message. virtual void KeyboardHook(int key, int scancode, int action, @@ -39,14 +40,33 @@ class TextInputPlugin { bool extended, bool was_down); + // Called when the Flutter engine receives a keyboard character. virtual void TextHook(const std::u16string& text); + // Called on an IME compose begin event. + // + // Triggered when the user begins editing composing text using a multi-step + // input method such as in CJK text input. virtual void ComposeBeginHook(); + // Called on an IME compose commit event. + // + // Triggered when the user triggers a commit of the current composing text + // while using a multi-step input method such as in CJK text input. Composing + // continues with the next keypress. virtual void ComposeCommitHook(); + // Called on an IME compose end event. + // + // Triggered when the composing ends, for example when the user presses + // ESC or when the user triggers a commit of the composing text while using a + // multi-step input method such as in CJK text input. virtual void ComposeEndHook(); + // Called on an IME composing region change event. + // + // Triggered when the user edits the composing text while using a multi-step + // input method such as in CJK text input. virtual void ComposeChangeHook(const std::u16string& text, int cursor_pos); private: @@ -72,8 +92,8 @@ class TextInputPlugin { // The MethodChannel used for communication with the Flutter engine. std::unique_ptr> channel_; - // The associated |TextInputPluginDelegate|. - TextInputPluginDelegate* delegate_; + // The associated |FlutterWindowsEngine|. + FlutterWindowsEngine* engine_; // The active client id. int client_id_; @@ -85,7 +105,7 @@ class TextInputPlugin { // as TextEditingDeltas or as one TextEditingValue. // For more information on the delta model, see: // https://master-api.flutter.dev/flutter/services/TextInputConfiguration/enableDeltaModel.html - bool enable_delta_model; + bool enable_delta_model = false; // Keyboard type of the client. See available options: // https://api.flutter.dev/flutter/services/TextInputType-class.html diff --git a/shell/platform/windows/text_input_plugin_delegate.h b/shell/platform/windows/text_input_plugin_delegate.h deleted file mode 100644 index 0d41ff4bdc3a0..0000000000000 --- a/shell/platform/windows/text_input_plugin_delegate.h +++ /dev/null @@ -1,25 +0,0 @@ -// 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_TEXT_INPUT_PLUGIN_DELEGATE_H_ -#define FLUTTER_SHELL_PLATFORM_WINDOWS_TEXT_INPUT_PLUGIN_DELEGATE_H_ - -#include "flutter/shell/platform/common/geometry.h" -#include "flutter/shell/platform/embedder/embedder.h" - -namespace flutter { - -class TextInputPluginDelegate { - public: - // Notifies the delegate of the updated the cursor rect in Flutter root view - // coordinates. - virtual void OnCursorRectUpdated(const Rect& rect) = 0; - - // Notifies the delegate that the system IME composing state should be reset. - virtual void OnResetImeComposing() = 0; -}; - -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_TEXT_INPUT_PLUGIN_DELEGATE_H_ diff --git a/shell/platform/windows/text_input_plugin_unittest.cc b/shell/platform/windows/text_input_plugin_unittest.cc index a99e0149aa286..73d4cf1469f68 100644 --- a/shell/platform/windows/text_input_plugin_unittest.cc +++ b/shell/platform/windows/text_input_plugin_unittest.cc @@ -10,8 +10,11 @@ #include "flutter/fml/macros.h" #include "flutter/shell/platform/common/json_message_codec.h" #include "flutter/shell/platform/common/json_method_codec.h" +#include "flutter/shell/platform/windows/flutter_windows_view.h" +#include "flutter/shell/platform/windows/testing/flutter_windows_engine_builder.h" +#include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h" #include "flutter/shell/platform/windows/testing/test_binary_messenger.h" -#include "flutter/shell/platform/windows/text_input_plugin_delegate.h" +#include "flutter/shell/platform/windows/testing/windows_test.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -19,6 +22,8 @@ namespace flutter { namespace testing { namespace { +using ::testing::Return; + static constexpr char kScanCodeKey[] = "scanCode"; static constexpr int kHandledScanCode = 20; static constexpr int kUnhandledScanCode = 21; @@ -97,21 +102,63 @@ static std::unique_ptr EncodedEditingState( return arguments; } -class MockTextInputPluginDelegate : public TextInputPluginDelegate { +class MockFlutterWindowsView : public FlutterWindowsView { public: - MockTextInputPluginDelegate() {} - virtual ~MockTextInputPluginDelegate() = default; + MockFlutterWindowsView(std::unique_ptr window) + : FlutterWindowsView(std::move(window)) {} + virtual ~MockFlutterWindowsView() = default; MOCK_METHOD(void, OnCursorRectUpdated, (const Rect&), (override)); MOCK_METHOD(void, OnResetImeComposing, (), (override)); private: - FML_DISALLOW_COPY_AND_ASSIGN(MockTextInputPluginDelegate); + FML_DISALLOW_COPY_AND_ASSIGN(MockFlutterWindowsView); }; } // namespace -TEST(TextInputPluginTest, TextMethodsWorksWithEmptyModel) { +class TextInputPluginTest : public WindowsTest { + public: + TextInputPluginTest() = default; + virtual ~TextInputPluginTest() = default; + + protected: + FlutterWindowsEngine* engine() { return engine_.get(); } + MockFlutterWindowsView* view() { return view_.get(); } + MockWindowBindingHandler* window() { return window_; } + + void UseHeadlessEngine() { + FlutterWindowsEngineBuilder builder{GetContext()}; + + engine_ = builder.Build(); + } + + void UseEngineWithView() { + FlutterWindowsEngineBuilder builder{GetContext()}; + + auto window = std::make_unique(); + + window_ = window.get(); + EXPECT_CALL(*window_, SetView).Times(1); + EXPECT_CALL(*window, GetRenderTarget).WillRepeatedly(Return(nullptr)); + + engine_ = builder.Build(); + view_ = std::make_unique(std::move(window)); + + engine_->SetView(view_.get()); + } + + private: + std::unique_ptr engine_; + std::unique_ptr view_; + MockWindowBindingHandler* window_; + + FML_DISALLOW_COPY_AND_ASSIGN(TextInputPluginTest); +}; + +TEST_F(TextInputPluginTest, TextMethodsWorksWithEmptyModel) { + UseEngineWithView(); + auto handled_message = CreateResponse(true); auto unhandled_message = CreateResponse(false); int received_scancode = 0; @@ -120,10 +167,9 @@ TEST(TextInputPluginTest, TextMethodsWorksWithEmptyModel) { [&received_scancode, &handled_message, &unhandled_message]( const std::string& channel, const uint8_t* message, size_t message_size, BinaryReply reply) {}); - MockTextInputPluginDelegate delegate; int redispatch_scancode = 0; - TextInputPlugin handler(&messenger, &delegate); + TextInputPlugin handler(&messenger, engine()); handler.KeyboardHook(VK_RETURN, 100, WM_KEYDOWN, '\n', false, false); handler.ComposeBeginHook(); @@ -135,26 +181,55 @@ TEST(TextInputPluginTest, TextMethodsWorksWithEmptyModel) { // Passes if it did not crash } -TEST(TextInputPluginTest, ClearClientResetsComposing) { +TEST_F(TextInputPluginTest, ClearClientResetsComposing) { + UseEngineWithView(); + TestBinaryMessenger messenger([](const std::string& channel, const uint8_t* message, size_t message_size, BinaryReply reply) {}); BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {}; - MockTextInputPluginDelegate delegate; - TextInputPlugin handler(&messenger, &delegate); + TextInputPlugin handler(&messenger, engine()); + + EXPECT_CALL(*view(), OnResetImeComposing()); + + auto& codec = JsonMethodCodec::GetInstance(); + auto message = codec.EncodeMethodCall({"TextInput.clearClient", nullptr}); + messenger.SimulateEngineMessage(kChannelName, message->data(), + message->size(), reply_handler); +} + +// Verify that clear client fails if in headless mode. +TEST_F(TextInputPluginTest, ClearClientRequiresView) { + UseHeadlessEngine(); + + TestBinaryMessenger messenger([](const std::string& channel, + const uint8_t* message, size_t message_size, + BinaryReply reply) {}); + + std::string reply; + BinaryReply reply_handler = [&reply](const uint8_t* reply_bytes, + size_t reply_size) { + reply = std::string(reinterpret_cast(reply_bytes), reply_size); + }; - EXPECT_CALL(delegate, OnResetImeComposing()); + TextInputPlugin handler(&messenger, engine()); auto& codec = JsonMethodCodec::GetInstance(); auto message = codec.EncodeMethodCall({"TextInput.clearClient", nullptr}); messenger.SimulateEngineMessage(kChannelName, message->data(), message->size(), reply_handler); + + EXPECT_EQ(reply, + "[\"Internal Consistency Error\",\"Text input is not available in " + "Windows headless mode\",null]"); } // Verify that the embedder sends state update messages to the framework during // IME composing. -TEST(TextInputPluginTest, VerifyComposingSendStateUpdate) { +TEST_F(TextInputPluginTest, VerifyComposingSendStateUpdate) { + UseEngineWithView(); + bool sent_message = false; TestBinaryMessenger messenger( [&sent_message](const std::string& channel, const uint8_t* message, @@ -162,8 +237,7 @@ TEST(TextInputPluginTest, VerifyComposingSendStateUpdate) { BinaryReply reply) { sent_message = true; }); BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {}; - MockTextInputPluginDelegate delegate; - TextInputPlugin handler(&messenger, &delegate); + TextInputPlugin handler(&messenger, engine()); auto& codec = JsonMethodCodec::GetInstance(); @@ -209,7 +283,9 @@ TEST(TextInputPluginTest, VerifyComposingSendStateUpdate) { EXPECT_TRUE(sent_message); } -TEST(TextInputPluginTest, VerifyInputActionNewlineInsertNewLine) { +TEST_F(TextInputPluginTest, VerifyInputActionNewlineInsertNewLine) { + UseEngineWithView(); + // Store messages as std::string for convenience. std::vector messages; @@ -222,8 +298,7 @@ TEST(TextInputPluginTest, VerifyInputActionNewlineInsertNewLine) { }); BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {}; - MockTextInputPluginDelegate delegate; - TextInputPlugin handler(&messenger, &delegate); + TextInputPlugin handler(&messenger, engine()); auto& codec = JsonMethodCodec::GetInstance(); @@ -266,7 +341,9 @@ TEST(TextInputPluginTest, VerifyInputActionNewlineInsertNewLine) { } // Regression test for https://github.com/flutter/flutter/issues/125879. -TEST(TextInputPluginTest, VerifyInputActionSendDoesNotInsertNewLine) { +TEST_F(TextInputPluginTest, VerifyInputActionSendDoesNotInsertNewLine) { + UseEngineWithView(); + std::vector> messages; TestBinaryMessenger messenger( @@ -279,8 +356,7 @@ TEST(TextInputPluginTest, VerifyInputActionSendDoesNotInsertNewLine) { }); BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {}; - MockTextInputPluginDelegate delegate; - TextInputPlugin handler(&messenger, &delegate); + TextInputPlugin handler(&messenger, engine()); auto& codec = JsonMethodCodec::GetInstance(); @@ -312,7 +388,9 @@ TEST(TextInputPluginTest, VerifyInputActionSendDoesNotInsertNewLine) { messages.front().begin())); } -TEST(TextInputPluginTest, TextEditingWorksWithDeltaModel) { +TEST_F(TextInputPluginTest, TextEditingWorksWithDeltaModel) { + UseEngineWithView(); + auto handled_message = CreateResponse(true); auto unhandled_message = CreateResponse(false); int received_scancode = 0; @@ -321,10 +399,9 @@ TEST(TextInputPluginTest, TextEditingWorksWithDeltaModel) { [&received_scancode, &handled_message, &unhandled_message]( const std::string& channel, const uint8_t* message, size_t message_size, BinaryReply reply) {}); - MockTextInputPluginDelegate delegate; int redispatch_scancode = 0; - TextInputPlugin handler(&messenger, &delegate); + TextInputPlugin handler(&messenger, engine()); auto args = std::make_unique(rapidjson::kArrayType); auto& allocator = args->GetAllocator(); @@ -369,7 +446,9 @@ TEST(TextInputPluginTest, TextEditingWorksWithDeltaModel) { } // Regression test for https://github.com/flutter/flutter/issues/123749 -TEST(TextInputPluginTest, CompositionCursorPos) { +TEST_F(TextInputPluginTest, CompositionCursorPos) { + UseEngineWithView(); + int selection_base = -1; TestBinaryMessenger messenger([&](const std::string& channel, const uint8_t* message, size_t size, @@ -389,9 +468,8 @@ TEST(TextInputPluginTest, CompositionCursorPos) { EXPECT_EQ(extent->value.GetInt(), selection_base); } }); - MockTextInputPluginDelegate delegate; - TextInputPlugin plugin(&messenger, &delegate); + TextInputPlugin plugin(&messenger, engine()); auto args = std::make_unique(rapidjson::kArrayType); auto& allocator = args->GetAllocator(); @@ -427,7 +505,9 @@ TEST(TextInputPluginTest, CompositionCursorPos) { EXPECT_EQ(selection_base, 5); } -TEST(TextInputPluginTest, TransformCursorRect) { +TEST_F(TextInputPluginTest, TransformCursorRect) { + UseEngineWithView(); + // A position of `EditableText`. double view_x = 100; double view_y = 200; @@ -450,12 +530,11 @@ TEST(TextInputPluginTest, TransformCursorRect) { BinaryReply reply) {}); BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {}; - MockTextInputPluginDelegate delegate; - TextInputPlugin handler(&messenger, &delegate); + TextInputPlugin handler(&messenger, engine()); auto& codec = JsonMethodCodec::GetInstance(); - EXPECT_CALL(delegate, OnCursorRectUpdated(Rect{{view_x, view_y}, {0, 0}})); + EXPECT_CALL(*view(), OnCursorRectUpdated(Rect{{view_x, view_y}, {0, 0}})); { auto arguments = @@ -476,7 +555,7 @@ TEST(TextInputPluginTest, TransformCursorRect) { message->size(), reply_handler); } - EXPECT_CALL(delegate, + EXPECT_CALL(*view(), OnCursorRectUpdated(Rect{{view_x + ime_x, view_y + ime_y}, {ime_width, ime_height}})); @@ -497,5 +576,41 @@ TEST(TextInputPluginTest, TransformCursorRect) { } } +TEST_F(TextInputPluginTest, SetMarkedTextRectRequiresView) { + UseHeadlessEngine(); + + TestBinaryMessenger messenger([](const std::string& channel, + const uint8_t* message, size_t message_size, + BinaryReply reply) {}); + + std::string reply; + BinaryReply reply_handler = [&reply](const uint8_t* reply_bytes, + size_t reply_size) { + reply = std::string(reinterpret_cast(reply_bytes), reply_size); + }; + + TextInputPlugin handler(&messenger, engine()); + + auto& codec = JsonMethodCodec::GetInstance(); + + auto arguments = + std::make_unique(rapidjson::kObjectType); + auto& allocator = arguments->GetAllocator(); + + arguments->AddMember("x", 0, allocator); + arguments->AddMember("y", 0, allocator); + arguments->AddMember("width", 0, allocator); + arguments->AddMember("height", 0, allocator); + + auto message = codec.EncodeMethodCall( + {"TextInput.setMarkedTextRect", std::move(arguments)}); + messenger.SimulateEngineMessage(kChannelName, message->data(), + message->size(), reply_handler); + + EXPECT_EQ(reply, + "[\"Internal Consistency Error\",\"Text input is not available in " + "Windows headless mode\",null]"); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/windows/window_binding_handler_delegate.h b/shell/platform/windows/window_binding_handler_delegate.h index a5647662dcb6f..265ca8093b395 100644 --- a/shell/platform/windows/window_binding_handler_delegate.h +++ b/shell/platform/windows/window_binding_handler_delegate.h @@ -98,15 +98,16 @@ class WindowBindingHandlerDelegate { // Notifies the delegate that IME composing region have been committed. // - // Triggered when the user commits the current composing text while using a - // multi-step input method such as in CJK text input. Composing continues with - // the next keypress. + // Triggered when the user triggers a commit of the current composing text + // while using a multi-step input method such as in CJK text input. Composing + // continues with the next keypress. virtual void OnComposeCommit() = 0; // Notifies the delegate that IME composing mode has ended. // - // Triggered when the user commits the composing text while using a multi-step - // input method such as in CJK text input. + // Triggered when the composing ends, for example when the user presses + // ESC or when the user triggers a commit of the composing text while using a + // multi-step input method such as in CJK text input. virtual void OnComposeEnd() = 0; // Notifies the delegate that IME composing region contents have changed.