[fuchsia] Support multi-line text editing (flutter/engine#34410)

Multi-line text editing and actions other than DONE were
never implemented in Flutter on Fuchsia.  This change
implements the feature, by plumbing the desired action
through to Fuchsia proper, and back, as Fuchsia's text
editing API expects.

Tested: the new behavior was verified by Fuchsia-side
integration tests.

Issue: https://github.com/flutter/flutter/issues/106905
This commit is contained in:
Filip Filmar 2022-07-28 15:33:11 -05:00 committed by GitHub
parent c5e76f4711
commit e4a2f48892
7 changed files with 919 additions and 334 deletions

View File

@ -2134,6 +2134,9 @@ FILE: ../../../flutter/shell/platform/fuchsia/flutter/surface.h
FILE: ../../../flutter/shell/platform/fuchsia/flutter/surface_producer.h
FILE: ../../../flutter/shell/platform/fuchsia/flutter/task_runner_adapter.cc
FILE: ../../../flutter/shell/platform/fuchsia/flutter/task_runner_adapter.h
FILE: ../../../flutter/shell/platform/fuchsia/flutter/text_delegate.cc
FILE: ../../../flutter/shell/platform/fuchsia/flutter/text_delegate.h
FILE: ../../../flutter/shell/platform/fuchsia/flutter/text_delegate_unittests.cc
FILE: ../../../flutter/shell/platform/fuchsia/flutter/unique_fdio_ns.h
FILE: ../../../flutter/shell/platform/fuchsia/flutter/vsync_waiter.cc
FILE: ../../../flutter/shell/platform/fuchsia/flutter/vsync_waiter.h

View File

@ -106,6 +106,8 @@ template("runner_sources") {
"surface_producer.h",
"task_runner_adapter.cc",
"task_runner_adapter.h",
"text_delegate.cc",
"text_delegate.h",
"unique_fdio_ns.h",
"vsync_waiter.cc",
"vsync_waiter.h",
@ -487,6 +489,7 @@ if (enable_unittests) {
"tests/gfx_session_connection_unittests.cc",
"tests/pointer_event_utility.cc",
"tests/pointer_event_utility.h",
"text_delegate_unittests.cc",
"vsync_waiter_unittest.cc",
]

View File

@ -25,32 +25,16 @@
#include "logging.h"
#include "runtime/dart/utils/inlines.h"
#include "text_delegate.h"
#include "vsync_waiter.h"
namespace flutter_runner {
static constexpr char kFlutterPlatformChannel[] = "flutter/platform";
static constexpr char kTextInputChannel[] = "flutter/textinput";
static constexpr char kKeyEventChannel[] = "flutter/keyevent";
static constexpr char kAccessibilityChannel[] = "flutter/accessibility";
static constexpr char kFlutterPlatformViewsChannel[] = "flutter/platform_views";
static constexpr char kFuchsiaShaderWarmupChannel[] = "fuchsia/shader_warmup";
// FL(77): Terminate engine if Fuchsia system FIDL connections have error.
template <class T>
void SetInterfaceErrorHandler(fidl::InterfacePtr<T>& interface,
std::string name) {
interface.set_error_handler([name](zx_status_t status) {
FML_LOG(ERROR) << "Interface error on: " << name << ", status: " << status;
});
}
template <class T>
void SetInterfaceErrorHandler(fidl::Binding<T>& binding, std::string name) {
binding.set_error_handler([name](zx_status_t status) {
FML_LOG(ERROR) << "Binding error on: " << name << ", status: " << status;
});
}
PlatformView::PlatformView(
bool is_flatland,
flutter::PlatformView::Delegate& delegate,
@ -81,10 +65,6 @@ PlatformView::PlatformView(
pointer_delegate_(
std::make_shared<PointerDelegate>(std::move(touch_source),
std::move(mouse_source))),
ime_client_(this),
text_sync_service_(ime_service.Bind()),
keyboard_listener_binding_(this),
keyboard_(keyboard.Bind()),
wireframe_enabled_callback_(std::move(wireframe_enabled_callback)),
on_update_view_callback_(std::move(on_update_view_callback)),
on_create_surface_callback_(std::move(on_create_surface_callback)),
@ -96,19 +76,21 @@ PlatformView::PlatformView(
await_vsync_for_secondary_callback_callback_(
await_vsync_for_secondary_callback_callback),
weak_factory_(this) {
// Register all error handlers.
SetInterfaceErrorHandler(ime_, "Input Method Editor");
SetInterfaceErrorHandler(ime_client_, "IME Client");
SetInterfaceErrorHandler(text_sync_service_, "Text Sync Service");
SetInterfaceErrorHandler(keyboard_listener_binding_, "Keyboard Listener");
SetInterfaceErrorHandler(keyboard_, "Keyboard");
fuchsia::ui::views::ViewRef view_ref_clone;
fidl::Clone(view_ref, &view_ref_clone);
// Configure keyboard listener.
keyboard_->AddListener(std::move(view_ref),
keyboard_listener_binding_.NewBinding(), [] {});
text_delegate_ =
std::make_unique<TextDelegate>(
std::move(view_ref), std::move(ime_service), std::move(keyboard),
[weak = weak_factory_.GetWeakPtr()](
std::unique_ptr<flutter::PlatformMessage> message) {
if (!weak) {
FML_LOG(WARNING)
<< "PlatformView use-after-free attempted. Ignoring.";
}
weak->delegate_.OnPlatformViewDispatchPlatformMessage(
std::move(message));
});
// Begin watching for focus changes.
focus_delegate_->WatchLoop([weak = weak_factory_.GetWeakPtr()](bool focused) {
@ -119,10 +101,10 @@ PlatformView::PlatformView(
// Ensure last_text_state_ is set to make sure Flutter actually wants
// an IME.
if (focused && weak->last_text_state_) {
weak->ActivateIme();
if (focused && weak->text_delegate_->HasTextState()) {
weak->text_delegate_->ActivateIme();
} else if (!focused) {
weak->DeactivateIme();
weak->text_delegate_->DeactivateIme();
}
});
@ -171,8 +153,8 @@ void PlatformView::RegisterPlatformMessageHandlers() {
std::bind(&PlatformView::HandleFlutterPlatformChannelPlatformMessage,
this, std::placeholders::_1);
platform_message_handlers_[kTextInputChannel] =
std::bind(&PlatformView::HandleFlutterTextInputChannelPlatformMessage,
this, std::placeholders::_1);
std::bind(&TextDelegate::HandleFlutterTextInputChannelPlatformMessage,
text_delegate_.get(), std::placeholders::_1);
platform_message_handlers_[kAccessibilityChannel] =
std::bind(&PlatformView::HandleAccessibilityChannelPlatformMessage, this,
std::placeholders::_1);
@ -184,84 +166,6 @@ void PlatformView::RegisterPlatformMessageHandlers() {
on_shader_warmup_, std::placeholders::_1);
}
// |fuchsia::ui::input::InputMethodEditorClient|
void PlatformView::DidUpdateState(
fuchsia::ui::input::TextInputState state,
std::unique_ptr<fuchsia::ui::input::InputEvent> input_event) {
rapidjson::Document document;
auto& allocator = document.GetAllocator();
rapidjson::Value encoded_state(rapidjson::kObjectType);
encoded_state.AddMember("text", state.text, allocator);
encoded_state.AddMember("selectionBase", state.selection.base, allocator);
encoded_state.AddMember("selectionExtent", state.selection.extent, allocator);
switch (state.selection.affinity) {
case fuchsia::ui::input::TextAffinity::UPSTREAM:
encoded_state.AddMember("selectionAffinity",
rapidjson::Value("TextAffinity.upstream"),
allocator);
break;
case fuchsia::ui::input::TextAffinity::DOWNSTREAM:
encoded_state.AddMember("selectionAffinity",
rapidjson::Value("TextAffinity.downstream"),
allocator);
break;
}
encoded_state.AddMember("selectionIsDirectional", true, allocator);
encoded_state.AddMember("composingBase", state.composing.start, allocator);
encoded_state.AddMember("composingExtent", state.composing.end, allocator);
rapidjson::Value args(rapidjson::kArrayType);
args.PushBack(current_text_input_client_, allocator);
args.PushBack(encoded_state, allocator);
document.SetObject();
document.AddMember("method",
rapidjson::Value("TextInputClient.updateEditingState"),
allocator);
document.AddMember("args", args, allocator);
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
document.Accept(writer);
const uint8_t* data = reinterpret_cast<const uint8_t*>(buffer.GetString());
DispatchPlatformMessage(std::make_unique<flutter::PlatformMessage>(
kTextInputChannel, // channel
fml::MallocMapping::Copy(data, buffer.GetSize()), // message
nullptr) // response
);
last_text_state_ =
std::make_unique<fuchsia::ui::input::TextInputState>(state);
}
// |fuchsia::ui::input::InputMethodEditorClient|
void PlatformView::OnAction(fuchsia::ui::input::InputMethodAction action) {
rapidjson::Document document;
auto& allocator = document.GetAllocator();
rapidjson::Value args(rapidjson::kArrayType);
args.PushBack(current_text_input_client_, allocator);
// Done is currently the only text input action defined by Flutter.
args.PushBack("TextInputAction.done", allocator);
document.SetObject();
document.AddMember(
"method", rapidjson::Value("TextInputClient.performAction"), allocator);
document.AddMember("args", args, allocator);
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
document.Accept(writer);
const uint8_t* data = reinterpret_cast<const uint8_t*>(buffer.GetString());
DispatchPlatformMessage(std::make_unique<flutter::PlatformMessage>(
kTextInputChannel, // channel
fml::MallocMapping::Copy(data, buffer.GetSize()), // message
nullptr) // response
);
}
static flutter::PointerData::Change GetChangeFromPointerEventPhase(
fuchsia::ui::input::PointerEventPhase phase) {
switch (phase) {
@ -405,77 +309,6 @@ bool PlatformView::OnHandlePointerEvent(
return true;
}
// |fuchsia::ui:input3::KeyboardListener|
void PlatformView::OnKeyEvent(
fuchsia::ui::input3::KeyEvent key_event,
fuchsia::ui::input3::KeyboardListener::OnKeyEventCallback callback) {
const char* type = nullptr;
switch (key_event.type()) {
case fuchsia::ui::input3::KeyEventType::PRESSED:
type = "keydown";
break;
case fuchsia::ui::input3::KeyEventType::RELEASED:
type = "keyup";
break;
case fuchsia::ui::input3::KeyEventType::SYNC:
// What, if anything, should happen here?
case fuchsia::ui::input3::KeyEventType::CANCEL:
// What, if anything, should happen here?
default:
break;
}
if (type == nullptr) {
FML_LOG(ERROR) << "Unknown key event phase.";
callback(fuchsia::ui::input3::KeyEventStatus::NOT_HANDLED);
return;
}
keyboard_translator_.ConsumeEvent(std::move(key_event));
rapidjson::Document document;
auto& allocator = document.GetAllocator();
document.SetObject();
document.AddMember("type", rapidjson::Value(type, strlen(type)), allocator);
document.AddMember("keymap", rapidjson::Value("fuchsia"), allocator);
document.AddMember("hidUsage", keyboard_translator_.LastHIDUsage(),
allocator);
document.AddMember("codePoint", keyboard_translator_.LastCodePoint(),
allocator);
document.AddMember("modifiers", keyboard_translator_.Modifiers(), allocator);
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
document.Accept(writer);
const uint8_t* data = reinterpret_cast<const uint8_t*>(buffer.GetString());
DispatchPlatformMessage(std::make_unique<flutter::PlatformMessage>(
kKeyEventChannel, // channel
fml::MallocMapping::Copy(data, buffer.GetSize()), // data
nullptr) // response
);
callback(fuchsia::ui::input3::KeyEventStatus::HANDLED);
}
void PlatformView::ActivateIme() {
DEBUG_CHECK(last_text_state_ != nullptr, LOG_TAG, "");
text_sync_service_->GetInputMethodEditor(
fuchsia::ui::input::KeyboardType::TEXT, // keyboard type
fuchsia::ui::input::InputMethodAction::DONE, // input method action
*last_text_state_, // initial state
ime_client_.NewBinding(), // client
ime_.NewRequest() // editor
);
}
void PlatformView::DeactivateIme() {
if (ime_) {
text_sync_service_->HideKeyboard();
ime_ = nullptr;
}
if (ime_client_.is_bound()) {
ime_client_.Unbind();
}
}
// |flutter::PlatformView|
std::unique_ptr<flutter::VsyncWaiter> PlatformView::CreateVSyncWaiter() {
return std::make_unique<flutter_runner::VsyncWaiter>(
@ -585,110 +418,6 @@ bool PlatformView::HandleFlutterPlatformChannelPlatformMessage(
return false;
}
// Channel handler for kTextInputChannel
bool PlatformView::HandleFlutterTextInputChannelPlatformMessage(
std::unique_ptr<flutter::PlatformMessage> message) {
FML_DCHECK(message->channel() == kTextInputChannel);
const auto& data = message->data();
rapidjson::Document document;
document.Parse(reinterpret_cast<const char*>(data.GetMapping()),
data.GetSize());
if (document.HasParseError() || !document.IsObject()) {
return false;
}
auto root = document.GetObject();
auto method = root.FindMember("method");
if (method == root.MemberEnd() || !method->value.IsString()) {
return false;
}
if (method->value == "TextInput.show") {
if (ime_) {
text_sync_service_->ShowKeyboard();
}
} else if (method->value == "TextInput.hide") {
if (ime_) {
text_sync_service_->HideKeyboard();
}
} else if (method->value == "TextInput.setClient") {
current_text_input_client_ = 0;
DeactivateIme();
auto args = root.FindMember("args");
if (args == root.MemberEnd() || !args->value.IsArray() ||
args->value.Size() != 2)
return false;
const auto& configuration = args->value[1];
if (!configuration.IsObject()) {
return false;
}
// TODO(abarth): Read the keyboard type from the configuration.
current_text_input_client_ = args->value[0].GetInt();
auto initial_text_input_state = fuchsia::ui::input::TextInputState{};
initial_text_input_state.text = "";
last_text_state_ = std::make_unique<fuchsia::ui::input::TextInputState>(
initial_text_input_state);
ActivateIme();
} else if (method->value == "TextInput.setEditingState") {
if (ime_) {
auto args_it = root.FindMember("args");
if (args_it == root.MemberEnd() || !args_it->value.IsObject()) {
return false;
}
const auto& args = args_it->value;
fuchsia::ui::input::TextInputState state;
state.text = "";
// TODO(abarth): Deserialize state.
auto text = args.FindMember("text");
if (text != args.MemberEnd() && text->value.IsString())
state.text = text->value.GetString();
auto selection_base = args.FindMember("selectionBase");
if (selection_base != args.MemberEnd() && selection_base->value.IsInt())
state.selection.base = selection_base->value.GetInt();
auto selection_extent = args.FindMember("selectionExtent");
if (selection_extent != args.MemberEnd() &&
selection_extent->value.IsInt())
state.selection.extent = selection_extent->value.GetInt();
auto selection_affinity = args.FindMember("selectionAffinity");
if (selection_affinity != args.MemberEnd() &&
selection_affinity->value.IsString() &&
selection_affinity->value == "TextAffinity.upstream")
state.selection.affinity = fuchsia::ui::input::TextAffinity::UPSTREAM;
else
state.selection.affinity = fuchsia::ui::input::TextAffinity::DOWNSTREAM;
// We ignore selectionIsDirectional because that concept doesn't exist on
// Fuchsia.
auto composing_base = args.FindMember("composingBase");
if (composing_base != args.MemberEnd() && composing_base->value.IsInt())
state.composing.start = composing_base->value.GetInt();
auto composing_extent = args.FindMember("composingExtent");
if (composing_extent != args.MemberEnd() &&
composing_extent->value.IsInt())
state.composing.end = composing_extent->value.GetInt();
ime_->SetState(std::move(state));
}
} else if (method->value == "TextInput.clearClient") {
current_text_input_client_ = 0;
last_text_state_ = nullptr;
DeactivateIme();
} else if (method->value == "TextInput.setCaretRect" ||
method->value == "TextInput.setEditableSizeAndTransform" ||
method->value == "TextInput.setMarkedTextRect" ||
method->value == "TextInput.setStyle") {
// We don't have these methods implemented and they get
// sent a lot during text input, so we create an empty case for them
// here to avoid "Unknown flutter/textinput method TextInput.*"
// log spam.
//
// TODO(fxb/101619): We should implement these.
} else {
FML_LOG(ERROR) << "Unknown " << message->channel() << " method "
<< method->value.GetString();
}
// Complete with an empty response.
return false;
}
bool PlatformView::HandleFlutterPlatformViewsChannelPlatformMessage(
std::unique_ptr<flutter::PlatformMessage> message) {
FML_DCHECK(message->channel() == kFlutterPlatformViewsChannel);

View File

@ -34,6 +34,7 @@
#include "focus_delegate.h"
#include "pointer_delegate.h"
#include "pointer_injector_delegate.h"
#include "text_delegate.h"
namespace flutter_runner {
@ -60,9 +61,7 @@ using OnShaderWarmup = std::function<void(const std::vector<std::string>&,
// in HandlePlatformMessage. This communication is bidirectional. Platform
// messages are notably responsible for communication related to input and
// external views / windowing.
class PlatformView : public flutter::PlatformView,
private fuchsia::ui::input3::KeyboardListener,
private fuchsia::ui::input::InputMethodEditorClient {
class PlatformView : public flutter::PlatformView {
public:
PlatformView(
bool is_flatland,
@ -99,34 +98,10 @@ class PlatformView : public flutter::PlatformView,
protected:
void RegisterPlatformMessageHandlers();
// |fuchsia.ui.input3.KeyboardListener|
// Called by the embedder every time there is a key event to process.
void OnKeyEvent(fuchsia::ui::input3::KeyEvent key_event,
fuchsia::ui::input3::KeyboardListener::OnKeyEventCallback
callback) override;
// |fuchsia::ui::input::InputMethodEditorClient|
void DidUpdateState(
fuchsia::ui::input::TextInputState state,
std::unique_ptr<fuchsia::ui::input::InputEvent> event) override;
// |fuchsia::ui::input::InputMethodEditorClient|
void OnAction(fuchsia::ui::input::InputMethodAction action) override;
bool OnHandlePointerEvent(const fuchsia::ui::input::PointerEvent& pointer);
bool OnHandleFocusEvent(const fuchsia::ui::input::FocusEvent& focus);
// Gets a new input method editor from the input connection. Run when both
// Scenic has focus and Flutter has requested input with setClient.
void ActivateIme();
// Detaches the input method editor connection, ending the edit session and
// closing the onscreen keyboard. Call when input is no longer desired, either
// because Scenic says we lost focus or when Flutter no longer has a text
// field focused.
void DeactivateIme();
// |flutter::PlatformView|
std::unique_ptr<flutter::VsyncWaiter> CreateVSyncWaiter() override;
@ -152,10 +127,6 @@ class PlatformView : public flutter::PlatformView,
bool HandleFlutterPlatformChannelPlatformMessage(
std::unique_ptr<flutter::PlatformMessage> message);
// Channel handler for kTextInputChannel
bool HandleFlutterTextInputChannelPlatformMessage(
std::unique_ptr<flutter::PlatformMessage> message);
// Channel handler for kPlatformViewsChannel.
bool HandleFlutterPlatformViewsChannelPlatformMessage(
std::unique_ptr<flutter::PlatformMessage> message);
@ -191,20 +162,8 @@ class PlatformView : public flutter::PlatformView,
std::shared_ptr<PointerDelegate> pointer_delegate_;
std::unique_ptr<PointerInjectorDelegate> pointer_injector_delegate_;
fidl::Binding<fuchsia::ui::input::InputMethodEditorClient> ime_client_;
fuchsia::ui::input::InputMethodEditorPtr ime_;
fuchsia::ui::input::ImeServicePtr text_sync_service_;
int current_text_input_client_ = 0;
fidl::Binding<fuchsia::ui::input3::KeyboardListener>
keyboard_listener_binding_;
fuchsia::ui::input3::KeyboardPtr keyboard_;
Keyboard keyboard_translator_;
// last_text_state_ is the last state of the text input as reported by the IME
// or initialized by Flutter. We set it to null if Flutter doesn't want any
// input, since then there is no text input state at all.
std::unique_ptr<fuchsia::ui::input::TextInputState> last_text_state_;
// Text delegate is responsible for handling keyboard input and text editing.
std::unique_ptr<TextDelegate> text_delegate_;
std::set<int> down_pointers_;
std::map<std::string /* channel */,

View File

@ -0,0 +1,449 @@
// 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 "text_delegate.h"
#include <fuchsia/ui/input/cpp/fidl.h>
#include <fuchsia/ui/input3/cpp/fidl.h>
#include <fuchsia/ui/views/cpp/fidl.h>
#include <lib/fidl/cpp/binding.h>
#include "flutter/fml/logging.h"
#include "flutter/fml/mapping.h"
#include "flutter/lib/ui/window/platform_message.h"
#include "flutter/shell/platform/fuchsia/flutter/keyboard.h"
#include "flutter/shell/platform/fuchsia/runtime/dart/utils/inlines.h"
#include "third_party/rapidjson/include/rapidjson/document.h"
#include "third_party/rapidjson/include/rapidjson/stringbuffer.h"
#include "third_party/rapidjson/include/rapidjson/writer.h"
#include "logging.h"
namespace flutter_runner {
static constexpr char kInputActionKey[] = "inputAction";
// See: https://api.flutter.dev/flutter/services/TextInputAction.html
// Only the actions relevant for Fuchsia are listed here.
static constexpr char kTextInputActionDone[] = "TextInputAction.done";
static constexpr char kTextInputActionNewline[] = "TextInputAction.newline";
static constexpr char kTextInputActionGo[] = "TextInputAction.go";
static constexpr char kTextInputActionNext[] = "TextInputAction.next";
static constexpr char kTextInputActionPrevious[] = "TextInputAction.previous";
static constexpr char kTextInputActionNone[] = "TextInputAction.none";
static constexpr char kTextInputActionSearch[] = "TextInputAction.search";
static constexpr char kTextInputActionSend[] = "TextInputAction.send";
static constexpr char kTextInputActionUnspecified[] =
"TextInputAction.unspecified";
// Converts Flutter TextInputAction to Fuchsia action enum.
static fuchsia::ui::input::InputMethodAction IntoInputMethodAction(
const std::string action_string) {
if (action_string == kTextInputActionNewline) {
return fuchsia::ui::input::InputMethodAction::NEWLINE;
} else if (action_string == kTextInputActionDone) {
return fuchsia::ui::input::InputMethodAction::DONE;
} else if (action_string == kTextInputActionGo) {
return fuchsia::ui::input::InputMethodAction::GO;
} else if (action_string == kTextInputActionNext) {
return fuchsia::ui::input::InputMethodAction::NEXT;
} else if (action_string == kTextInputActionPrevious) {
return fuchsia::ui::input::InputMethodAction::PREVIOUS;
} else if (action_string == kTextInputActionNone) {
return fuchsia::ui::input::InputMethodAction::NONE;
} else if (action_string == kTextInputActionSearch) {
return fuchsia::ui::input::InputMethodAction::SEARCH;
} else if (action_string == kTextInputActionSend) {
return fuchsia::ui::input::InputMethodAction::SEND;
} else if (action_string == kTextInputActionUnspecified) {
return fuchsia::ui::input::InputMethodAction::UNSPECIFIED;
}
// If this message comes along it means we should really add the missing 'if'
// above.
FML_VLOG(1) << "unexpected action_string: " << action_string;
// Substituting DONE for an unexpected action string will probably be OK.
return fuchsia::ui::input::InputMethodAction::DONE;
}
// Converts the Fuchsia action enum into Flutter TextInputAction.
static const std::string IntoTextInputAction(
fuchsia::ui::input::InputMethodAction action) {
if (action == fuchsia::ui::input::InputMethodAction::NEWLINE) {
return kTextInputActionNewline;
} else if (action == fuchsia::ui::input::InputMethodAction::DONE) {
return kTextInputActionDone;
} else if (action == fuchsia::ui::input::InputMethodAction::GO) {
return kTextInputActionGo;
} else if (action == fuchsia::ui::input::InputMethodAction::NEXT) {
return kTextInputActionNext;
} else if (action == fuchsia::ui::input::InputMethodAction::PREVIOUS) {
return kTextInputActionPrevious;
} else if (action == fuchsia::ui::input::InputMethodAction::NONE) {
return kTextInputActionNone;
} else if (action == fuchsia::ui::input::InputMethodAction::SEARCH) {
return kTextInputActionSearch;
} else if (action == fuchsia::ui::input::InputMethodAction::SEND) {
return kTextInputActionSend;
} else if (action == fuchsia::ui::input::InputMethodAction::UNSPECIFIED) {
return kTextInputActionUnspecified;
}
// If this message comes along it means we should really add the missing 'if'
// above.
FML_VLOG(1) << "unexpected action: " << static_cast<uint32_t>(action);
// Substituting "done" for an unexpected text input action will probably
// be OK.
return kTextInputActionDone;
}
// TODO(fxbug.dev/8868): Terminate engine if Fuchsia system FIDL connections
// have error.
template <class T>
void SetInterfaceErrorHandler(fidl::InterfacePtr<T>& interface,
std::string name) {
interface.set_error_handler([name](zx_status_t status) {
FML_LOG(ERROR) << "Interface error on: " << name << ", status: " << status;
});
}
template <class T>
void SetInterfaceErrorHandler(fidl::Binding<T>& binding, std::string name) {
binding.set_error_handler([name](zx_status_t status) {
FML_LOG(ERROR) << "Binding error on: " << name << ", status: " << status;
});
}
TextDelegate::TextDelegate(
fuchsia::ui::views::ViewRef view_ref,
fuchsia::ui::input::ImeServiceHandle ime_service,
fuchsia::ui::input3::KeyboardHandle keyboard,
std::function<void(std::unique_ptr<flutter::PlatformMessage>)>
dispatch_callback)
: dispatch_callback_(dispatch_callback),
ime_client_(this),
text_sync_service_(ime_service.Bind()),
keyboard_listener_binding_(this),
keyboard_(keyboard.Bind()) {
// Register all error handlers.
SetInterfaceErrorHandler(ime_, "Input Method Editor");
SetInterfaceErrorHandler(ime_client_, "IME Client");
SetInterfaceErrorHandler(text_sync_service_, "Text Sync Service");
SetInterfaceErrorHandler(keyboard_listener_binding_, "Keyboard Listener");
SetInterfaceErrorHandler(keyboard_, "Keyboard");
// Configure keyboard listener.
keyboard_->AddListener(std::move(view_ref),
keyboard_listener_binding_.NewBinding(), [] {});
}
void TextDelegate::ActivateIme() {
ActivateIme(requested_text_action_.value_or(
fuchsia::ui::input::InputMethodAction::DONE));
}
void TextDelegate::ActivateIme(fuchsia::ui::input::InputMethodAction action) {
DEBUG_CHECK(last_text_state_.has_value(), LOG_TAG, "");
requested_text_action_ = action;
text_sync_service_->GetInputMethodEditor(
fuchsia::ui::input::KeyboardType::TEXT, // keyboard type
action, // input method action
last_text_state_.value(), // initial state
ime_client_.NewBinding(), // client
ime_.NewRequest() // editor
);
}
void TextDelegate::DeactivateIme() {
if (ime_) {
text_sync_service_->HideKeyboard();
ime_ = nullptr;
}
if (ime_client_.is_bound()) {
ime_client_.Unbind();
}
}
// |fuchsia::ui::input::InputMethodEditorClient|
void TextDelegate::DidUpdateState(
fuchsia::ui::input::TextInputState state,
std::unique_ptr<fuchsia::ui::input::InputEvent> input_event) {
rapidjson::Document document;
auto& allocator = document.GetAllocator();
rapidjson::Value encoded_state(rapidjson::kObjectType);
encoded_state.AddMember("text", state.text, allocator);
encoded_state.AddMember("selectionBase", state.selection.base, allocator);
encoded_state.AddMember("selectionExtent", state.selection.extent, allocator);
switch (state.selection.affinity) {
case fuchsia::ui::input::TextAffinity::UPSTREAM:
encoded_state.AddMember("selectionAffinity",
rapidjson::Value("TextAffinity.upstream"),
allocator);
break;
case fuchsia::ui::input::TextAffinity::DOWNSTREAM:
encoded_state.AddMember("selectionAffinity",
rapidjson::Value("TextAffinity.downstream"),
allocator);
break;
}
encoded_state.AddMember("selectionIsDirectional", true, allocator);
encoded_state.AddMember("composingBase", state.composing.start, allocator);
encoded_state.AddMember("composingExtent", state.composing.end, allocator);
rapidjson::Value args(rapidjson::kArrayType);
args.PushBack(current_text_input_client_, allocator);
args.PushBack(encoded_state, allocator);
document.SetObject();
document.AddMember("method",
rapidjson::Value("TextInputClient.updateEditingState"),
allocator);
document.AddMember("args", args, allocator);
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
document.Accept(writer);
const uint8_t* data = reinterpret_cast<const uint8_t*>(buffer.GetString());
dispatch_callback_(std::make_unique<flutter::PlatformMessage>(
kTextInputChannel, // channel
fml::MallocMapping::Copy(data, buffer.GetSize()), // message
nullptr) // response
);
last_text_state_ = std::move(state);
}
// |fuchsia::ui::input::InputMethodEditorClient|
void TextDelegate::OnAction(fuchsia::ui::input::InputMethodAction action) {
rapidjson::Document document;
auto& allocator = document.GetAllocator();
rapidjson::Value args(rapidjson::kArrayType);
args.PushBack(current_text_input_client_, allocator);
const std::string action_string = IntoTextInputAction(action);
args.PushBack(rapidjson::Value{}.SetString(action_string.c_str(),
action_string.length()),
allocator);
document.SetObject();
document.AddMember(
"method", rapidjson::Value("TextInputClient.performAction"), allocator);
document.AddMember("args", args, allocator);
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
document.Accept(writer);
const uint8_t* data = reinterpret_cast<const uint8_t*>(buffer.GetString());
dispatch_callback_(std::make_unique<flutter::PlatformMessage>(
kTextInputChannel, // channel
fml::MallocMapping::Copy(data, buffer.GetSize()), // message
nullptr) // response
);
}
// Channel handler for kTextInputChannel
bool TextDelegate::HandleFlutterTextInputChannelPlatformMessage(
std::unique_ptr<flutter::PlatformMessage> message) {
FML_DCHECK(message->channel() == kTextInputChannel);
const auto& data = message->data();
rapidjson::Document document;
document.Parse(reinterpret_cast<const char*>(data.GetMapping()),
data.GetSize());
if (document.HasParseError() || !document.IsObject()) {
return false;
}
auto root = document.GetObject();
auto method = root.FindMember("method");
if (method == root.MemberEnd() || !method->value.IsString()) {
return false;
}
if (method->value == "TextInput.show") {
if (ime_) {
text_sync_service_->ShowKeyboard();
}
} else if (method->value == "TextInput.hide") {
if (ime_) {
text_sync_service_->HideKeyboard();
}
} else if (method->value == "TextInput.setClient") {
// Sample "setClient" message:
//
// {
// "method": "TextInput.setClient",
// "args": [
// 7,
// {
// "inputType": {
// "name": "TextInputType.multiline",
// "signed":null,
// "decimal":null
// },
// "readOnly": false,
// "obscureText": false,
// "autocorrect":true,
// "smartDashesType":"1",
// "smartQuotesType":"1",
// "enableSuggestions":true,
// "enableInteractiveSelection":true,
// "actionLabel":null,
// "inputAction":"TextInputAction.newline",
// "textCapitalization":"TextCapitalization.none",
// "keyboardAppearance":"Brightness.dark",
// "enableIMEPersonalizedLearning":true,
// "enableDeltaModel":false
// }
// ]
// }
current_text_input_client_ = 0;
DeactivateIme();
auto args = root.FindMember("args");
if (args == root.MemberEnd() || !args->value.IsArray() ||
args->value.Size() != 2)
return false;
const auto& configuration = args->value[1];
if (!configuration.IsObject()) {
return false;
}
// TODO(abarth): Read the keyboard type from the configuration.
current_text_input_client_ = args->value[0].GetInt();
auto initial_text_input_state = fuchsia::ui::input::TextInputState{};
initial_text_input_state.text = "";
last_text_state_ = std::move(initial_text_input_state);
const auto configuration_object = configuration.GetObject();
if (!configuration_object.HasMember(kInputActionKey)) {
return false;
}
const auto& action_object = configuration_object[kInputActionKey];
if (!action_object.IsString()) {
return false;
}
const auto action_string =
std::string(action_object.GetString(), action_object.GetStringLength());
ActivateIme(IntoInputMethodAction(std::move(action_string)));
} else if (method->value == "TextInput.setEditingState") {
if (ime_) {
auto args_it = root.FindMember("args");
if (args_it == root.MemberEnd() || !args_it->value.IsObject()) {
return false;
}
const auto& args = args_it->value;
fuchsia::ui::input::TextInputState state;
state.text = "";
// TODO(abarth): Deserialize state.
auto text = args.FindMember("text");
if (text != args.MemberEnd() && text->value.IsString()) {
state.text = text->value.GetString();
}
auto selection_base = args.FindMember("selectionBase");
if (selection_base != args.MemberEnd() && selection_base->value.IsInt()) {
state.selection.base = selection_base->value.GetInt();
}
auto selection_extent = args.FindMember("selectionExtent");
if (selection_extent != args.MemberEnd() &&
selection_extent->value.IsInt()) {
state.selection.extent = selection_extent->value.GetInt();
}
auto selection_affinity = args.FindMember("selectionAffinity");
if (selection_affinity != args.MemberEnd() &&
selection_affinity->value.IsString() &&
selection_affinity->value == "TextAffinity.upstream") {
state.selection.affinity = fuchsia::ui::input::TextAffinity::UPSTREAM;
} else {
state.selection.affinity = fuchsia::ui::input::TextAffinity::DOWNSTREAM;
}
// We ignore selectionIsDirectional because that concept doesn't exist on
// Fuchsia.
auto composing_base = args.FindMember("composingBase");
if (composing_base != args.MemberEnd() && composing_base->value.IsInt()) {
state.composing.start = composing_base->value.GetInt();
}
auto composing_extent = args.FindMember("composingExtent");
if (composing_extent != args.MemberEnd() &&
composing_extent->value.IsInt()) {
state.composing.end = composing_extent->value.GetInt();
}
ime_->SetState(std::move(state));
}
} else if (method->value == "TextInput.clearClient") {
current_text_input_client_ = 0;
last_text_state_ = std::nullopt;
requested_text_action_ = std::nullopt;
DeactivateIme();
} else if (method->value == "TextInput.setCaretRect" ||
method->value == "TextInput.setEditableSizeAndTransform" ||
method->value == "TextInput.setMarkedTextRect" ||
method->value == "TextInput.setStyle") {
// We don't have these methods implemented and they get
// sent a lot during text input, so we create an empty case for them
// here to avoid "Unknown flutter/textinput method TextInput.*"
// log spam.
//
// TODO(fxb/101619): We should implement these.
} else {
FML_LOG(ERROR) << "Unknown " << message->channel() << " method "
<< method->value.GetString();
}
// Complete with an empty response.
return false;
}
// |fuchsia::ui:input3::KeyboardListener|
void TextDelegate::OnKeyEvent(
fuchsia::ui::input3::KeyEvent key_event,
fuchsia::ui::input3::KeyboardListener::OnKeyEventCallback callback) {
const char* type = nullptr;
switch (key_event.type()) {
case fuchsia::ui::input3::KeyEventType::PRESSED:
type = "keydown";
break;
case fuchsia::ui::input3::KeyEventType::RELEASED:
type = "keyup";
break;
case fuchsia::ui::input3::KeyEventType::SYNC:
// SYNC means the key was pressed while focus was not on this application.
// This should possibly behave like PRESSED in the future, though it
// doesn't hurt to ignore it today.
case fuchsia::ui::input3::KeyEventType::CANCEL:
// CANCEL means the key was released while focus was not on this
// application.
// This should possibly behave like RELEASED in the future to ensure that
// a key is not repeated forever if it is pressed while focus is lost.
default:
break;
}
if (type == nullptr) {
FML_VLOG(1) << "Unknown key event phase.";
callback(fuchsia::ui::input3::KeyEventStatus::NOT_HANDLED);
return;
}
keyboard_translator_.ConsumeEvent(std::move(key_event));
rapidjson::Document document;
auto& allocator = document.GetAllocator();
document.SetObject();
document.AddMember("type", rapidjson::Value(type, strlen(type)), allocator);
document.AddMember("keymap", rapidjson::Value("fuchsia"), allocator);
document.AddMember("hidUsage", keyboard_translator_.LastHIDUsage(),
allocator);
document.AddMember("codePoint", keyboard_translator_.LastCodePoint(),
allocator);
document.AddMember("modifiers", keyboard_translator_.Modifiers(), allocator);
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
document.Accept(writer);
const uint8_t* data = reinterpret_cast<const uint8_t*>(buffer.GetString());
dispatch_callback_(std::make_unique<flutter::PlatformMessage>(
kKeyEventChannel, // channel
fml::MallocMapping::Copy(data, buffer.GetSize()), // data
nullptr) // response
);
callback(fuchsia::ui::input3::KeyEventStatus::HANDLED);
}
} // namespace flutter_runner

View File

@ -0,0 +1,154 @@
// 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.
/// Text editing functionality delegated from |PlatformView|.
/// See |TextDelegate| for details.
#ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_TEXT_DELEGATE_H_
#define FLUTTER_SHELL_PLATFORM_FUCHSIA_TEXT_DELEGATE_H_
#include <memory>
#include <fuchsia/ui/input/cpp/fidl.h>
#include <fuchsia/ui/input3/cpp/fidl.h>
#include <fuchsia/ui/views/cpp/fidl.h>
#include <lib/fidl/cpp/binding.h>
#include "flutter/lib/ui/window/platform_message.h"
#include "flutter/shell/common/platform_view.h"
#include "flutter/shell/platform/fuchsia/flutter/keyboard.h"
#include "logging.h"
namespace flutter_runner {
/// The channel name used for text editing platofrm messages.
constexpr char kTextInputChannel[] = "flutter/textinput";
/// The channel name used for key event platform messages.
constexpr char kKeyEventChannel[] = "flutter/keyevent";
/// TextDelegate handles keyboard inpout and text editing.
///
/// It mediates between Fuchsia's input and Flutter's platform messages. When it
/// is initialized, it contacts `fuchsia.ui.input.Keyboard` to register itself
/// as listener of key events.
///
/// Whenever a text editing request comes from the
/// Flutter app, it will activate Fuchsia's input method editor, and will send
/// text edit actions coming from the Fuchsia platform over to the Flutter app,
/// by converting FIDL messages (`fuchsia.ui.input.InputMethodEditorClient`
/// calls) to appropriate text editing Flutter platform messages.
///
/// For details refer to:
/// * Flutter side:
/// https://api.flutter.dev/javadoc/io/flutter/embedding/engine/systemchannels/TextInputChannel.html
/// * Fuchsia side: https://fuchsia.dev/reference/fidl/fuchsia.ui.input
class TextDelegate : public fuchsia::ui::input3::KeyboardListener,
public fuchsia::ui::input::InputMethodEditorClient {
public:
/// Creates a new TextDelegate.
///
/// Args:
/// view_ref: the reference to the app's view. Required for registration
/// with Fuchsia.
/// ime_service: a handle to Fuchsia's input method service.
/// keyboard: the keyboard listener, gets notified of key presses and
/// releases.
/// dispatch_callback: a function used to send a Flutter platform message.
TextDelegate(fuchsia::ui::views::ViewRef view_ref,
fuchsia::ui::input::ImeServiceHandle ime_service,
fuchsia::ui::input3::KeyboardHandle keyboard,
std::function<void(std::unique_ptr<flutter::PlatformMessage>)>
dispatch_callback);
/// |fuchsia.ui.input3.KeyboardListener|
/// Called by the embedder every time there is a key event to process.
void OnKeyEvent(fuchsia::ui::input3::KeyEvent key_event,
fuchsia::ui::input3::KeyboardListener::OnKeyEventCallback
callback) override;
/// |fuchsia::ui::input::InputMethodEditorClient|
/// Called by the embedder every time the edit state is updated.
void DidUpdateState(
fuchsia::ui::input::TextInputState state,
std::unique_ptr<fuchsia::ui::input::InputEvent> event) override;
/// |fuchsia::ui::input::InputMethodEditorClient|
/// Called by the embedder when the action key is pressed, and the requested
/// action is supplied to Flutter.
void OnAction(fuchsia::ui::input::InputMethodAction action) override;
/// Gets a new input method editor from the input connection. Run when both
/// Scenic has focus and Flutter has requested input with setClient.
void ActivateIme();
/// Detaches the input method editor connection, ending the edit session and
/// closing the onscreen keyboard. Call when input is no longer desired,
/// either because Scenic says we lost focus or when Flutter no longer has a
/// text field focused.
void DeactivateIme();
/// Channel handler for kTextInputChannel
bool HandleFlutterTextInputChannelPlatformMessage(
std::unique_ptr<flutter::PlatformMessage> message);
/// Returns true if there is a text state (i.e. if some text editing is in
/// progress).
bool HasTextState() { return last_text_state_.has_value(); }
private:
// Activates the input method editor, assigning |action| to the "enter" key.
// This action will be reported by |OnAction| above when the "enter" key is
// pressed. Note that in the case of multi-line text editors, |OnAction| will
// never be called: instead, the text editor will insert a newline into the
// edited text.
void ActivateIme(fuchsia::ui::input::InputMethodAction action);
// Converts Fuchsia platform key codes into Flutter key codes.
Keyboard keyboard_translator_;
// A callback for sending a single Flutter platform message.
std::function<void(std::unique_ptr<flutter::PlatformMessage>)>
dispatch_callback_;
// TextDelegate server-side binding. Methods called when the text edit state
// is updated.
fidl::Binding<fuchsia::ui::input::InputMethodEditorClient> ime_client_;
// An interface for interacting with a text input control.
fuchsia::ui::input::InputMethodEditorPtr ime_;
// An interface for requesting the InputMethodEditor.
fuchsia::ui::input::ImeServicePtr text_sync_service_;
// The locally-unique identifier of the text input currently in use. Flutter
// usually uses only one at a time.
int current_text_input_client_ = 0;
// TextDelegate server side binding. Methods called when a key is pressed.
fidl::Binding<fuchsia::ui::input3::KeyboardListener>
keyboard_listener_binding_;
// The client-side stub for calling the Keyboard protocol.
fuchsia::ui::input3::KeyboardPtr keyboard_;
// last_text_state_ is the last state of the text input as reported by the IME
// or initialized by Flutter. We set it to null if Flutter doesn't want any
// input, since then there is no text input state at all.
// If nullptr, then no editing is in progress.
std::optional<fuchsia::ui::input::TextInputState> last_text_state_;
// The action that Flutter expects to happen when the user presses the "enter"
// key. For example, it could be `InputMethodAction::DONE` which would cause
// text editing to stop and the current text to be accepted.
// If set to std::nullopt, then no editing is in progress.
std::optional<fuchsia::ui::input::InputMethodAction> requested_text_action_;
FML_DISALLOW_COPY_AND_ASSIGN(TextDelegate);
};
} // namespace flutter_runner
#endif // FLUTTER_SHELL_PLATFORM_FUCHSIA_TEXT_DELEGATE_H_

View File

@ -0,0 +1,288 @@
// 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 "text_delegate.h"
#include <fuchsia/ui/input/cpp/fidl.h>
#include <fuchsia/ui/input3/cpp/fidl.h>
#include <fuchsia/ui/views/cpp/fidl.h>
#include <gtest/gtest.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/fidl/cpp/binding.h>
#include <lib/fidl/cpp/binding_set.h>
#include <lib/ui/scenic/cpp/view_ref_pair.h>
#include "tests/fakes/platform_message.h"
#include "flutter/lib/ui/window/platform_message.h"
#include <memory>
namespace flutter_runner::testing {
// Convert a |PlatformMessage| to string for ease of testing.
static std::string MessageToString(PlatformMessage& message) {
const char* data = reinterpret_cast<const char*>(message.data().GetMapping());
return std::string(data, message.data().GetSize());
}
// Fake |KeyboardService| implementation. Only responsibility is to remember
// what it was called with.
class FakeKeyboardService : public fuchsia::ui::input3::Keyboard {
public:
// |fuchsia.ui.input3/Keyboard.AddListener|
virtual void AddListener(
fuchsia::ui::views::ViewRef,
fidl::InterfaceHandle<fuchsia::ui::input3::KeyboardListener> listener,
fuchsia::ui::input3::Keyboard::AddListenerCallback callback) {
listener_ = listener.Bind();
callback();
}
fidl::InterfacePtr<fuchsia::ui::input3::KeyboardListener> listener_;
};
// Fake ImeService implementation. Only responsibility is to remember what
// it was called with.
class FakeImeService : public fuchsia::ui::input::ImeService {
public:
virtual void GetInputMethodEditor(
fuchsia::ui::input::KeyboardType keyboard_type,
fuchsia::ui::input::InputMethodAction action,
fuchsia::ui::input::TextInputState input_state,
fidl::InterfaceHandle<fuchsia::ui::input::InputMethodEditorClient> client,
fidl::InterfaceRequest<fuchsia::ui::input::InputMethodEditor> ime) {
keyboard_type_ = std::move(keyboard_type);
action_ = std::move(action);
input_state_ = std::move(input_state);
client_ = client.Bind();
ime_ = std::move(ime);
}
virtual void ShowKeyboard() { keyboard_shown_ = true; }
virtual void HideKeyboard() { keyboard_shown_ = false; }
bool IsKeyboardShown() { return keyboard_shown_; }
bool keyboard_shown_ = false;
fuchsia::ui::input::KeyboardType keyboard_type_;
fuchsia::ui::input::InputMethodAction action_;
fuchsia::ui::input::TextInputState input_state_;
fidl::InterfacePtr<fuchsia::ui::input::InputMethodEditorClient> client_;
fidl::InterfaceRequest<fuchsia::ui::input::InputMethodEditor> ime_;
};
class TextDelegateTest : public ::testing::Test {
protected:
TextDelegateTest()
: loop_(&kAsyncLoopConfigAttachToCurrentThread),
keyboard_service_binding_(&keyboard_service_),
ime_service_binding_(&ime_service_) {
fidl::InterfaceHandle<fuchsia::ui::input3::Keyboard> keyboard_handle;
auto keyboard_request = keyboard_handle.NewRequest();
keyboard_service_binding_.Bind(keyboard_request.TakeChannel());
fidl::InterfaceHandle<fuchsia::ui::input::ImeService> ime_service_handle;
ime_service_binding_.Bind(ime_service_handle.NewRequest().TakeChannel());
auto fake_view_ref_pair = scenic::ViewRefPair::New();
text_delegate_ = std::make_unique<TextDelegate>(
std::move(fake_view_ref_pair.view_ref), std::move(ime_service_handle),
std::move(keyboard_handle),
// Should this be accessed through a weak pointer?
[this](std::unique_ptr<flutter::PlatformMessage> message) {
last_message_ = std::move(message);
});
// TextDelegate has some async initialization that needs to happen.
RunLoopUntilIdle();
}
// Runs the event loop until all scheduled events are spent.
void RunLoopUntilIdle() { loop_.RunUntilIdle(); }
void TearDown() override {
loop_.Quit();
ASSERT_EQ(loop_.ResetQuit(), 0);
}
async::Loop loop_;
FakeKeyboardService keyboard_service_;
fidl::Binding<fuchsia::ui::input3::Keyboard> keyboard_service_binding_;
FakeImeService ime_service_;
fidl::Binding<fuchsia::ui::input::ImeService> ime_service_binding_;
// Unit under test.
std::unique_ptr<TextDelegate> text_delegate_;
std::unique_ptr<flutter::PlatformMessage> last_message_;
};
// Goes through several steps of a text edit protocol. These are hard to test
// in isolation because the text edit protocol depends on the correct method
// invocation sequence. The text editor is initialized with the editing
// parameters, and we verify that the correct input action is parsed out. We
// then exercise showing and hiding the keyboard, as well as a text state
// update.
TEST_F(TextDelegateTest, ActivateIme) {
auto fake_platform_message_response = FakePlatformMessageResponse::Create();
{
// Initialize the editor. Without this initialization, the protocol code
// will crash.
const auto set_client_msg = R"(
{
"method": "TextInput.setClient",
"args": [
7,
{
"inputType": {
"name": "TextInputType.multiline",
"signed":null,
"decimal":null
},
"readOnly": false,
"obscureText": false,
"autocorrect":true,
"smartDashesType":"1",
"smartQuotesType":"1",
"enableSuggestions":true,
"enableInteractiveSelection":true,
"actionLabel":null,
"inputAction":"TextInputAction.newline",
"textCapitalization":"TextCapitalization.none",
"keyboardAppearance":"Brightness.dark",
"enableIMEPersonalizedLearning":true,
"enableDeltaModel":false
}
]
}
)";
auto message = fake_platform_message_response->WithMessage(
kTextInputChannel, set_client_msg);
text_delegate_->HandleFlutterTextInputChannelPlatformMessage(
std::move(message));
RunLoopUntilIdle();
EXPECT_EQ(ime_service_.action_,
fuchsia::ui::input::InputMethodAction::NEWLINE);
EXPECT_FALSE(ime_service_.IsKeyboardShown());
}
{
// Verify that showing keyboard results in the correct platform effect.
const auto set_client_msg = R"(
{
"method": "TextInput.show"
}
)";
auto message = fake_platform_message_response->WithMessage(
kTextInputChannel, set_client_msg);
text_delegate_->HandleFlutterTextInputChannelPlatformMessage(
std::move(message));
RunLoopUntilIdle();
EXPECT_TRUE(ime_service_.IsKeyboardShown());
}
{
// Verify that hiding keyboard results in the correct platform effect.
const auto set_client_msg = R"(
{
"method": "TextInput.hide"
}
)";
auto message = fake_platform_message_response->WithMessage(
kTextInputChannel, set_client_msg);
text_delegate_->HandleFlutterTextInputChannelPlatformMessage(
std::move(message));
RunLoopUntilIdle();
EXPECT_FALSE(ime_service_.IsKeyboardShown());
}
{
// Update the editing state from the Fuchsia platform side.
fuchsia::ui::input::TextInputState state = {
.revision = 42,
.text = "Foo",
.selection = fuchsia::ui::input::TextSelection{},
.composing = fuchsia::ui::input::TextRange{},
};
auto input_event = std::make_unique<fuchsia::ui::input::InputEvent>();
ime_service_.client_->DidUpdateState(std::move(state),
std::move(input_event));
RunLoopUntilIdle();
EXPECT_EQ(
R"({"method":"TextInputClient.updateEditingState","args":[7,{"text":"Foo","selectionBase":0,"selectionExtent":0,"selectionAffinity":"TextAffinity.upstream","selectionIsDirectional":true,"composingBase":-1,"composingExtent":-1}]})",
MessageToString(*last_message_));
}
{
// Notify Flutter that the action key has been pressed.
ime_service_.client_->OnAction(fuchsia::ui::input::InputMethodAction::DONE);
RunLoopUntilIdle();
EXPECT_EQ(
R"({"method":"TextInputClient.performAction","args":[7,"TextInputAction.done"]})",
MessageToString(*last_message_));
}
}
// Hands a few typical |KeyEvent|s to the text delegate. Regular key events are
// handled, "odd" key events are rejected (not handled). "Handling" a key event
// means converting it to an appropriate |PlatformMessage| and forwarding it.
TEST_F(TextDelegateTest, OnAction) {
{
// A sensible key event is converted into a platform message.
fuchsia::ui::input3::KeyEvent key_event;
*key_event.mutable_type() = fuchsia::ui::input3::KeyEventType::PRESSED;
*key_event.mutable_key() = fuchsia::input::Key::A;
key_event.mutable_key_meaning()->set_codepoint('a');
fuchsia::ui::input3::KeyEventStatus status;
keyboard_service_.listener_->OnKeyEvent(
std::move(key_event), [&status](fuchsia::ui::input3::KeyEventStatus s) {
status = std::move(s);
});
RunLoopUntilIdle();
EXPECT_EQ(fuchsia::ui::input3::KeyEventStatus::HANDLED, status);
EXPECT_EQ(
R"({"type":"keydown","keymap":"fuchsia","hidUsage":458756,"codePoint":97,"modifiers":0})",
MessageToString(*last_message_));
}
{
// SYNC event is not handled.
// This is currently expected, though we may need to change that behavior.
fuchsia::ui::input3::KeyEvent key_event;
*key_event.mutable_type() = fuchsia::ui::input3::KeyEventType::SYNC;
fuchsia::ui::input3::KeyEventStatus status;
keyboard_service_.listener_->OnKeyEvent(
std::move(key_event), [&status](fuchsia::ui::input3::KeyEventStatus s) {
status = std::move(s);
});
RunLoopUntilIdle();
EXPECT_EQ(fuchsia::ui::input3::KeyEventStatus::NOT_HANDLED, status);
}
{
// CANCEL event is not handled.
// This is currently expected, though we may need to change that behavior.
fuchsia::ui::input3::KeyEvent key_event;
*key_event.mutable_type() = fuchsia::ui::input3::KeyEventType::CANCEL;
fuchsia::ui::input3::KeyEventStatus status;
keyboard_service_.listener_->OnKeyEvent(
std::move(key_event), [&status](fuchsia::ui::input3::KeyEventStatus s) {
status = std::move(s);
});
RunLoopUntilIdle();
EXPECT_EQ(fuchsia::ui::input3::KeyEventStatus::NOT_HANDLED, status);
}
}
} // namespace flutter_runner::testing