Flutter Engine
The Flutter Engine
text_delegate.cc
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "text_delegate.h"
6
7#include <fuchsia/ui/input/cpp/fidl.h>
8#include <fuchsia/ui/input3/cpp/fidl.h>
9#include <fuchsia/ui/views/cpp/fidl.h>
10#include <lib/fidl/cpp/binding.h>
11
12#include "flutter/fml/logging.h"
13#include "flutter/fml/mapping.h"
14#include "flutter/lib/ui/window/platform_message.h"
15#include "flutter/shell/platform/fuchsia/flutter/keyboard.h"
16#include "flutter/shell/platform/fuchsia/runtime/dart/utils/inlines.h"
17#include "third_party/rapidjson/include/rapidjson/document.h"
18#include "third_party/rapidjson/include/rapidjson/stringbuffer.h"
19#include "third_party/rapidjson/include/rapidjson/writer.h"
20
21#include "logging.h"
22
23namespace flutter_runner {
24
25static constexpr char kInputActionKey[] = "inputAction";
26
27// See: https://api.flutter.dev/flutter/services/TextInputAction.html
28// Only the actions relevant for Fuchsia are listed here.
29static constexpr char kTextInputActionDone[] = "TextInputAction.done";
30static constexpr char kTextInputActionNewline[] = "TextInputAction.newline";
31static constexpr char kTextInputActionGo[] = "TextInputAction.go";
32static constexpr char kTextInputActionNext[] = "TextInputAction.next";
33static constexpr char kTextInputActionPrevious[] = "TextInputAction.previous";
34static constexpr char kTextInputActionNone[] = "TextInputAction.none";
35static constexpr char kTextInputActionSearch[] = "TextInputAction.search";
36static constexpr char kTextInputActionSend[] = "TextInputAction.send";
37static constexpr char kTextInputActionUnspecified[] =
38 "TextInputAction.unspecified";
39
40// Converts Flutter TextInputAction to Fuchsia action enum.
41static fuchsia::ui::input::InputMethodAction IntoInputMethodAction(
42 const std::string action_string) {
43 if (action_string == kTextInputActionNewline) {
44 return fuchsia::ui::input::InputMethodAction::NEWLINE;
45 } else if (action_string == kTextInputActionDone) {
46 return fuchsia::ui::input::InputMethodAction::DONE;
47 } else if (action_string == kTextInputActionGo) {
48 return fuchsia::ui::input::InputMethodAction::GO;
49 } else if (action_string == kTextInputActionNext) {
50 return fuchsia::ui::input::InputMethodAction::NEXT;
51 } else if (action_string == kTextInputActionPrevious) {
52 return fuchsia::ui::input::InputMethodAction::PREVIOUS;
53 } else if (action_string == kTextInputActionNone) {
54 return fuchsia::ui::input::InputMethodAction::NONE;
55 } else if (action_string == kTextInputActionSearch) {
56 return fuchsia::ui::input::InputMethodAction::SEARCH;
57 } else if (action_string == kTextInputActionSend) {
58 return fuchsia::ui::input::InputMethodAction::SEND;
59 } else if (action_string == kTextInputActionUnspecified) {
60 return fuchsia::ui::input::InputMethodAction::UNSPECIFIED;
61 }
62 // If this message comes along it means we should really add the missing 'if'
63 // above.
64 FML_VLOG(1) << "unexpected action_string: " << action_string;
65 // Substituting DONE for an unexpected action string will probably be OK.
66 return fuchsia::ui::input::InputMethodAction::DONE;
67}
68
69// Converts the Fuchsia action enum into Flutter TextInputAction.
70static const std::string IntoTextInputAction(
71 fuchsia::ui::input::InputMethodAction action) {
72 if (action == fuchsia::ui::input::InputMethodAction::NEWLINE) {
74 } else if (action == fuchsia::ui::input::InputMethodAction::DONE) {
76 } else if (action == fuchsia::ui::input::InputMethodAction::GO) {
77 return kTextInputActionGo;
78 } else if (action == fuchsia::ui::input::InputMethodAction::NEXT) {
80 } else if (action == fuchsia::ui::input::InputMethodAction::PREVIOUS) {
82 } else if (action == fuchsia::ui::input::InputMethodAction::NONE) {
84 } else if (action == fuchsia::ui::input::InputMethodAction::SEARCH) {
86 } else if (action == fuchsia::ui::input::InputMethodAction::SEND) {
88 } else if (action == fuchsia::ui::input::InputMethodAction::UNSPECIFIED) {
90 }
91 // If this message comes along it means we should really add the missing 'if'
92 // above.
93 FML_VLOG(1) << "unexpected action: " << static_cast<uint32_t>(action);
94 // Substituting "done" for an unexpected text input action will probably
95 // be OK.
97}
98
99// TODO(fxbug.dev/8868): Terminate engine if Fuchsia system FIDL connections
100// have error.
101template <class T>
102void SetInterfaceErrorHandler(fidl::InterfacePtr<T>& interface,
103 std::string name) {
104 interface.set_error_handler([name](zx_status_t status) {
105 FML_LOG(ERROR) << "Interface error on: " << name << ", status: " << status;
106 });
107}
108template <class T>
109void SetInterfaceErrorHandler(fidl::Binding<T>& binding, std::string name) {
110 binding.set_error_handler([name](zx_status_t status) {
111 FML_LOG(ERROR) << "Binding error on: " << name << ", status: " << status;
112 });
113}
114
116 fuchsia::ui::views::ViewRef view_ref,
117 fuchsia::ui::input::ImeServiceHandle ime_service,
118 fuchsia::ui::input3::KeyboardHandle keyboard,
119 std::function<void(std::unique_ptr<flutter::PlatformMessage>)>
120 dispatch_callback)
121 : dispatch_callback_(dispatch_callback),
122 ime_client_(this),
123 text_sync_service_(ime_service.Bind()),
124 keyboard_listener_binding_(this),
125 keyboard_(keyboard.Bind()) {
126 // Register all error handlers.
127 SetInterfaceErrorHandler(ime_, "Input Method Editor");
128 SetInterfaceErrorHandler(ime_client_, "IME Client");
129 SetInterfaceErrorHandler(text_sync_service_, "Text Sync Service");
130 SetInterfaceErrorHandler(keyboard_listener_binding_, "Keyboard Listener");
131 SetInterfaceErrorHandler(keyboard_, "Keyboard");
132
133 // Configure keyboard listener.
134 keyboard_->AddListener(std::move(view_ref),
135 keyboard_listener_binding_.NewBinding(), [] {});
136}
137
139 ActivateIme(requested_text_action_.value_or(
140 fuchsia::ui::input::InputMethodAction::DONE));
141}
142
143void TextDelegate::ActivateIme(fuchsia::ui::input::InputMethodAction action) {
144 FML_DCHECK(last_text_state_.has_value());
145
146 requested_text_action_ = action;
147 text_sync_service_->GetInputMethodEditor(
148 fuchsia::ui::input::KeyboardType::TEXT, // keyboard type
149 action, // input method action
150 last_text_state_.value(), // initial state
151 ime_client_.NewBinding(), // client
152 ime_.NewRequest() // editor
153 );
154}
155
157 if (ime_) {
158 text_sync_service_->HideKeyboard();
159 ime_ = nullptr;
160 }
161 if (ime_client_.is_bound()) {
162 ime_client_.Unbind();
163 }
164}
165
166// |fuchsia::ui::input::InputMethodEditorClient|
168 fuchsia::ui::input::TextInputState state,
169 std::unique_ptr<fuchsia::ui::input::InputEvent> input_event) {
170 rapidjson::Document document;
171 auto& allocator = document.GetAllocator();
172 rapidjson::Value encoded_state(rapidjson::kObjectType);
173 encoded_state.AddMember("text", state.text, allocator);
174 encoded_state.AddMember("selectionBase", state.selection.base, allocator);
175 encoded_state.AddMember("selectionExtent", state.selection.extent, allocator);
176 switch (state.selection.affinity) {
177 case fuchsia::ui::input::TextAffinity::UPSTREAM:
178 encoded_state.AddMember("selectionAffinity",
179 rapidjson::Value("TextAffinity.upstream"),
180 allocator);
181 break;
182 case fuchsia::ui::input::TextAffinity::DOWNSTREAM:
183 encoded_state.AddMember("selectionAffinity",
184 rapidjson::Value("TextAffinity.downstream"),
185 allocator);
186 break;
187 }
188 encoded_state.AddMember("selectionIsDirectional", true, allocator);
189 encoded_state.AddMember("composingBase", state.composing.start, allocator);
190 encoded_state.AddMember("composingExtent", state.composing.end, allocator);
191
192 rapidjson::Value args(rapidjson::kArrayType);
193 args.PushBack(current_text_input_client_, allocator);
194 args.PushBack(encoded_state, allocator);
195
196 document.SetObject();
197 document.AddMember("method",
198 rapidjson::Value("TextInputClient.updateEditingState"),
199 allocator);
200 document.AddMember("args", args, allocator);
201
202 rapidjson::StringBuffer buffer;
203 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
204 document.Accept(writer);
205
206 const uint8_t* data = reinterpret_cast<const uint8_t*>(buffer.GetString());
207 dispatch_callback_(std::make_unique<flutter::PlatformMessage>(
208 kTextInputChannel, // channel
209 fml::MallocMapping::Copy(data, buffer.GetSize()), // message
210 nullptr) // response
211 );
212 last_text_state_ = std::move(state);
213}
214
215// |fuchsia::ui::input::InputMethodEditorClient|
216void TextDelegate::OnAction(fuchsia::ui::input::InputMethodAction action) {
217 rapidjson::Document document;
218 auto& allocator = document.GetAllocator();
219
220 rapidjson::Value args(rapidjson::kArrayType);
221 args.PushBack(current_text_input_client_, allocator);
222
223 const std::string action_string = IntoTextInputAction(action);
224 args.PushBack(rapidjson::Value{}.SetString(action_string.c_str(),
225 action_string.length()),
226 allocator);
227
228 document.SetObject();
229 document.AddMember(
230 "method", rapidjson::Value("TextInputClient.performAction"), allocator);
231 document.AddMember("args", args, allocator);
232
233 rapidjson::StringBuffer buffer;
234 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
235 document.Accept(writer);
236
237 const uint8_t* data = reinterpret_cast<const uint8_t*>(buffer.GetString());
238 dispatch_callback_(std::make_unique<flutter::PlatformMessage>(
239 kTextInputChannel, // channel
240 fml::MallocMapping::Copy(data, buffer.GetSize()), // message
241 nullptr) // response
242 );
243}
244
245// Channel handler for kTextInputChannel
247 std::unique_ptr<flutter::PlatformMessage> message) {
248 FML_DCHECK(message->channel() == kTextInputChannel);
249 const auto& data = message->data();
250
251 rapidjson::Document document;
252 document.Parse(reinterpret_cast<const char*>(data.GetMapping()),
253 data.GetSize());
254 if (document.HasParseError() || !document.IsObject()) {
255 return false;
256 }
257 auto root = document.GetObject();
258 auto method = root.FindMember("method");
259 if (method == root.MemberEnd() || !method->value.IsString()) {
260 return false;
261 }
262
263 if (method->value == "TextInput.show") {
264 if (ime_) {
265 text_sync_service_->ShowKeyboard();
266 }
267 } else if (method->value == "TextInput.hide") {
268 if (ime_) {
269 text_sync_service_->HideKeyboard();
270 }
271 } else if (method->value == "TextInput.setClient") {
272 // Sample "setClient" message:
273 //
274 // {
275 // "method": "TextInput.setClient",
276 // "args": [
277 // 7,
278 // {
279 // "inputType": {
280 // "name": "TextInputType.multiline",
281 // "signed":null,
282 // "decimal":null
283 // },
284 // "readOnly": false,
285 // "obscureText": false,
286 // "autocorrect":true,
287 // "smartDashesType":"1",
288 // "smartQuotesType":"1",
289 // "enableSuggestions":true,
290 // "enableInteractiveSelection":true,
291 // "actionLabel":null,
292 // "inputAction":"TextInputAction.newline",
293 // "textCapitalization":"TextCapitalization.none",
294 // "keyboardAppearance":"Brightness.dark",
295 // "enableIMEPersonalizedLearning":true,
296 // "enableDeltaModel":false
297 // }
298 // ]
299 // }
300
301 current_text_input_client_ = 0;
303 auto args = root.FindMember("args");
304 if (args == root.MemberEnd() || !args->value.IsArray() ||
305 args->value.Size() != 2)
306 return false;
307 const auto& configuration = args->value[1];
308 if (!configuration.IsObject()) {
309 return false;
310 }
311 // TODO(abarth): Read the keyboard type from the configuration.
312 current_text_input_client_ = args->value[0].GetInt();
313
314 auto initial_text_input_state = fuchsia::ui::input::TextInputState{};
315 initial_text_input_state.text = "";
316 last_text_state_ = std::move(initial_text_input_state);
317
318 const auto configuration_object = configuration.GetObject();
319 if (!configuration_object.HasMember(kInputActionKey)) {
320 return false;
321 }
322 const auto& action_object = configuration_object[kInputActionKey];
323 if (!action_object.IsString()) {
324 return false;
325 }
326 const auto action_string =
327 std::string(action_object.GetString(), action_object.GetStringLength());
328 ActivateIme(IntoInputMethodAction(std::move(action_string)));
329 } else if (method->value == "TextInput.setEditingState") {
330 if (ime_) {
331 auto args_it = root.FindMember("args");
332 if (args_it == root.MemberEnd() || !args_it->value.IsObject()) {
333 return false;
334 }
335 const auto& args = args_it->value;
336 fuchsia::ui::input::TextInputState state;
337 state.text = "";
338 // TODO(abarth): Deserialize state.
339 auto text = args.FindMember("text");
340 if (text != args.MemberEnd() && text->value.IsString()) {
341 state.text = text->value.GetString();
342 }
343 auto selection_base = args.FindMember("selectionBase");
344 if (selection_base != args.MemberEnd() && selection_base->value.IsInt()) {
345 state.selection.base = selection_base->value.GetInt();
346 }
347 auto selection_extent = args.FindMember("selectionExtent");
348 if (selection_extent != args.MemberEnd() &&
349 selection_extent->value.IsInt()) {
350 state.selection.extent = selection_extent->value.GetInt();
351 }
352 auto selection_affinity = args.FindMember("selectionAffinity");
353 if (selection_affinity != args.MemberEnd() &&
354 selection_affinity->value.IsString() &&
355 selection_affinity->value == "TextAffinity.upstream") {
356 state.selection.affinity = fuchsia::ui::input::TextAffinity::UPSTREAM;
357 } else {
358 state.selection.affinity = fuchsia::ui::input::TextAffinity::DOWNSTREAM;
359 }
360 // We ignore selectionIsDirectional because that concept doesn't exist on
361 // Fuchsia.
362 auto composing_base = args.FindMember("composingBase");
363 if (composing_base != args.MemberEnd() && composing_base->value.IsInt()) {
364 state.composing.start = composing_base->value.GetInt();
365 }
366 auto composing_extent = args.FindMember("composingExtent");
367 if (composing_extent != args.MemberEnd() &&
368 composing_extent->value.IsInt()) {
369 state.composing.end = composing_extent->value.GetInt();
370 }
371 ime_->SetState(std::move(state));
372 }
373 } else if (method->value == "TextInput.clearClient") {
374 current_text_input_client_ = 0;
375 last_text_state_ = std::nullopt;
376 requested_text_action_ = std::nullopt;
378 } else if (method->value == "TextInput.setCaretRect" ||
379 method->value == "TextInput.setEditableSizeAndTransform" ||
380 method->value == "TextInput.setMarkedTextRect" ||
381 method->value == "TextInput.setStyle") {
382 // We don't have these methods implemented and they get
383 // sent a lot during text input, so we create an empty case for them
384 // here to avoid "Unknown flutter/textinput method TextInput.*"
385 // log spam.
386 //
387 // TODO(fxb/101619): We should implement these.
388 } else {
389 FML_LOG(ERROR) << "Unknown " << message->channel() << " method "
390 << method->value.GetString();
391 }
392 // Complete with an empty response.
393 return false;
394}
395
396// |fuchsia::ui:input3::KeyboardListener|
398 fuchsia::ui::input3::KeyEvent key_event,
399 fuchsia::ui::input3::KeyboardListener::OnKeyEventCallback callback) {
400 const char* type = nullptr;
401 switch (key_event.type()) {
402 case fuchsia::ui::input3::KeyEventType::PRESSED:
403 type = "keydown";
404 break;
405 case fuchsia::ui::input3::KeyEventType::RELEASED:
406 type = "keyup";
407 break;
408 case fuchsia::ui::input3::KeyEventType::SYNC:
409 // SYNC means the key was pressed while focus was not on this application.
410 // This should possibly behave like PRESSED in the future, though it
411 // doesn't hurt to ignore it today.
412 case fuchsia::ui::input3::KeyEventType::CANCEL:
413 // CANCEL means the key was released while focus was not on this
414 // application.
415 // This should possibly behave like RELEASED in the future to ensure that
416 // a key is not repeated forever if it is pressed while focus is lost.
417 default:
418 break;
419 }
420 if (type == nullptr) {
421 FML_VLOG(1) << "Unknown key event phase.";
422 callback(fuchsia::ui::input3::KeyEventStatus::NOT_HANDLED);
423 return;
424 }
425 keyboard_translator_.ConsumeEvent(std::move(key_event));
426
427 rapidjson::Document document;
428 auto& allocator = document.GetAllocator();
429 document.SetObject();
430 document.AddMember("type", rapidjson::Value(type, strlen(type)), allocator);
431 document.AddMember("keymap", rapidjson::Value("fuchsia"), allocator);
432 document.AddMember("hidUsage", keyboard_translator_.LastHIDUsage(),
433 allocator);
434 document.AddMember("codePoint", keyboard_translator_.LastCodePoint(),
435 allocator);
436 document.AddMember("modifiers", keyboard_translator_.Modifiers(), allocator);
437 rapidjson::StringBuffer buffer;
438 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
439 document.Accept(writer);
440
441 const uint8_t* data = reinterpret_cast<const uint8_t*>(buffer.GetString());
442 dispatch_callback_(std::make_unique<flutter::PlatformMessage>(
443 kKeyEventChannel, // channel
444 fml::MallocMapping::Copy(data, buffer.GetSize()), // data
445 nullptr) // response
446 );
447 callback(fuchsia::ui::input3::KeyEventStatus::HANDLED);
448}
449} // namespace flutter_runner
GLenum type
uint32_t LastCodePoint()
Definition: keyboard.cc:307
bool ConsumeEvent(fuchsia::ui::input3::KeyEvent event)
Definition: keyboard.cc:174
void DidUpdateState(fuchsia::ui::input::TextInputState state, std::unique_ptr< fuchsia::ui::input::InputEvent > event) override
void OnKeyEvent(fuchsia::ui::input3::KeyEvent key_event, fuchsia::ui::input3::KeyboardListener::OnKeyEventCallback callback) override
void OnAction(fuchsia::ui::input::InputMethodAction action) override
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)
bool HandleFlutterTextInputChannelPlatformMessage(std::unique_ptr< flutter::PlatformMessage > message)
Channel handler for kTextInputChannel.
static MallocMapping Copy(const T *begin, const T *end)
Definition: mapping.h:162
AtkStateType state
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
FlKeyEvent uint64_t FlKeyResponderAsyncCallback callback
#define FML_VLOG(verbose_level)
Definition: logging.h:98
#define FML_LOG(severity)
Definition: logging.h:82
#define FML_DCHECK(condition)
Definition: logging.h:103
Dart_NativeFunction function
Definition: fuchsia.cc:51
FlutterKeyEvent key_event
std::u16string text
Win32Message message
static const std::string IntoTextInputAction(fuchsia::ui::input::InputMethodAction action)
static constexpr char kTextInputActionPrevious[]
static constexpr char kTextInputActionUnspecified[]
static constexpr char kTextInputActionGo[]
static constexpr char kTextInputActionDone[]
static constexpr char kTextInputActionNewline[]
static constexpr char kTextInputActionNone[]
static constexpr char kInputActionKey[]
constexpr char kTextInputChannel[]
The channel name used for text editing platofrm messages.
Definition: text_delegate.h:27
static constexpr char kTextInputActionSearch[]
void SetInterfaceErrorHandler(fidl::InterfacePtr< T > &interface, std::string name)
static fuchsia::ui::input::InputMethodAction IntoInputMethodAction(const std::string action_string)
constexpr char kKeyEventChannel[]
The channel name used for key event platform messages.
Definition: text_delegate.h:30
static constexpr char kTextInputActionNext[]
static constexpr char kTextInputActionSend[]
DEF_SWITCHES_START aot vmservice shared library name
Definition: switches.h:32
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir Path to the cache directory This is different from the persistent_cache_path in embedder which is used for Skia shader cache icu native lib Path to the library file that exports the ICU data vm service The hostname IP address on which the Dart VM Service should be served If not defaults to or::depending on whether ipv6 is specified vm service A custom Dart VM Service port The default is to pick a randomly available open port disable vm Disable the Dart VM Service The Dart VM Service is never available in release mode disable vm service Disable mDNS Dart VM Service publication Bind to the IPv6 localhost address for the Dart VM Service Ignored if vm service host is set endless trace buffer
Definition: switches.h:126
static bool Bind(PassBindingsCacheMTL &pass, ShaderStage stage, size_t bind_index, const BufferView &view)
string root
Definition: scale_cpu.py:20
fidl::Binding< fuchsia::ui::input3::Keyboard > keyboard_
FlutterKeyEventType type
The event kind.
Definition: embedder.h:1118
std::shared_ptr< const fml::Mapping > data
Definition: texture_gles.cc:63
#define ERROR(message)
Definition: elf_loader.cc:260