[Windows] Begin decoupling text input plugin from the view (flutter/engine#47833)

Currently the text input plugin is strongly tied to a single view. This change makes the text input plugin tied to the engine in preparation for multi-view world.

Part of https://github.com/flutter/flutter/issues/115611

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
This commit is contained in:
Loïc Sharma 2023-11-30 13:49:14 -08:00 committed by GitHub
parent 0b04bedb47
commit f5cacea14a
10 changed files with 248 additions and 121 deletions

View File

@ -7166,7 +7166,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
@ -9990,7 +9989,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

View File

@ -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<MockWindowBindingHandler>();
@ -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());

View File

@ -666,7 +666,7 @@ FlutterWindowsEngine::CreateKeyboardKeyHandler(
std::unique_ptr<TextInputPlugin> FlutterWindowsEngine::CreateTextInputPlugin(
BinaryMessenger* messenger) {
return std::make_unique<TextInputPlugin>(messenger, view_);
return std::make_unique<TextInputPlugin>(messenger, this);
}
bool FlutterWindowsEngine::RegisterExternalTexture(int64_t texture_id) {

View File

@ -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();

View File

@ -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<NiceMock<MockWindowBindingHandler>>();
@ -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;

View File

@ -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 <windows.h>
#include <cstdint>
#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<flutter::MethodChannel<rapidjson::Document>>(
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;

View File

@ -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<flutter::MethodChannel<rapidjson::Document>> 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

View File

@ -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_

View File

@ -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<rapidjson::Document> EncodedEditingState(
return arguments;
}
class MockTextInputPluginDelegate : public TextInputPluginDelegate {
class MockFlutterWindowsView : public FlutterWindowsView {
public:
MockTextInputPluginDelegate() {}
virtual ~MockTextInputPluginDelegate() = default;
MockFlutterWindowsView(std::unique_ptr<WindowBindingHandler> 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<MockWindowBindingHandler>();
window_ = window.get();
EXPECT_CALL(*window_, SetView).Times(1);
EXPECT_CALL(*window, GetRenderTarget).WillRepeatedly(Return(nullptr));
engine_ = builder.Build();
view_ = std::make_unique<MockFlutterWindowsView>(std::move(window));
engine_->SetView(view_.get());
}
private:
std::unique_ptr<FlutterWindowsEngine> engine_;
std::unique_ptr<MockFlutterWindowsView> 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,16 +181,17 @@ 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(delegate, OnResetImeComposing());
EXPECT_CALL(*view(), OnResetImeComposing());
auto& codec = JsonMethodCodec::GetInstance();
auto message = codec.EncodeMethodCall({"TextInput.clearClient", nullptr});
@ -152,9 +199,37 @@ TEST(TextInputPluginTest, ClearClientResetsComposing) {
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<const char*>(reply_bytes), reply_size);
};
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<std::string> 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<std::vector<uint8_t>> 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::Document>(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::Document>(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<const char*>(reply_bytes), reply_size);
};
TextInputPlugin handler(&messenger, engine());
auto& codec = JsonMethodCodec::GetInstance();
auto arguments =
std::make_unique<rapidjson::Document>(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

View File

@ -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.