Add FlTextInputPlugin (#18314)

* Add FlTextInputPlugin
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index fff517b..ba413f5 100755
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -1230,6 +1230,8 @@
 FILE: ../../../flutter/shell/platform/linux/fl_standard_method_codec_test.cc
 FILE: ../../../flutter/shell/platform/linux/fl_string_codec.cc
 FILE: ../../../flutter/shell/platform/linux/fl_string_codec_test.cc
+FILE: ../../../flutter/shell/platform/linux/fl_text_input_plugin.cc
+FILE: ../../../flutter/shell/platform/linux/fl_text_input_plugin.h
 FILE: ../../../flutter/shell/platform/linux/fl_value.cc
 FILE: ../../../flutter/shell/platform/linux/fl_value_test.cc
 FILE: ../../../flutter/shell/platform/linux/fl_view.cc
diff --git a/shell/platform/common/cpp/BUILD.gn b/shell/platform/common/cpp/BUILD.gn
index a58bdbe..9f1ad8c 100644
--- a/shell/platform/common/cpp/BUILD.gn
+++ b/shell/platform/common/cpp/BUILD.gn
@@ -30,12 +30,28 @@
   configs += [ ":desktop_library_implementation" ]
 }
 
+source_set("common_cpp_input") {
+  public = [
+    "text_input_model.h",
+  ]
+
+  sources = [
+    "text_input_model.cc",
+  ]
+
+  configs += [ ":desktop_library_implementation" ]
+
+  if (is_win) {
+    # For wstring_conversion. See issue #50053.
+    defines = [ "_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING" ]
+  }
+}
+
 source_set("common_cpp") {
   public = [
     "incoming_message_dispatcher.h",
     "json_message_codec.h",
     "json_method_codec.h",
-    "text_input_model.h",
   ]
 
   # TODO: Refactor flutter_glfw.cc to move the implementations corresponding
@@ -44,7 +60,6 @@
     "incoming_message_dispatcher.cc",
     "json_message_codec.cc",
     "json_method_codec.cc",
-    "text_input_model.cc",
   ]
 
   configs += [ ":desktop_library_implementation" ]
@@ -59,11 +74,6 @@
     ":common_cpp_core",
     "//third_party/rapidjson",
   ]
-
-  if (is_win) {
-    # For wstring_conversion. See issue #50053.
-    defines = [ "_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING" ]
-  }
 }
 
 # The portion of common_cpp that has no dependencies on the public/
@@ -117,6 +127,7 @@
   deps = [
     ":common_cpp",
     ":common_cpp_fixtures",
+    ":common_cpp_input",
     "//flutter/shell/platform/common/cpp/client_wrapper:client_wrapper",
     "//flutter/shell/platform/common/cpp/client_wrapper:client_wrapper_library_stubs",
     "//flutter/testing",
diff --git a/shell/platform/common/cpp/text_input_model.cc b/shell/platform/common/cpp/text_input_model.cc
index 49c11ed..67f0455 100644
--- a/shell/platform/common/cpp/text_input_model.cc
+++ b/shell/platform/common/cpp/text_input_model.cc
@@ -85,6 +85,12 @@
   selection_base_ = selection_extent_;
 }
 
+void TextInputModel::AddText(const std::string& text) {
+  std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>
+      utf16_converter;
+  AddText(utf16_converter.from_bytes(text));
+}
+
 bool TextInputModel::Backspace() {
   if (selection_base_ != selection_extent_) {
     DeleteSelected();
@@ -113,6 +119,46 @@
   return false;
 }
 
+bool TextInputModel::DeleteSurrounding(int offset_from_cursor, int count) {
+  auto start = selection_extent_;
+  if (offset_from_cursor < 0) {
+    for (int i = 0; i < -offset_from_cursor; i++) {
+      // If requested start is before the available text then reduce the
+      // number of characters to delete.
+      if (start == text_.begin()) {
+        count = i;
+        break;
+      }
+      start -= IsTrailingSurrogate(*(start - 1)) ? 2 : 1;
+    }
+  } else {
+    for (int i = 0; i < offset_from_cursor && start != text_.end(); i++) {
+      start += IsLeadingSurrogate(*start) ? 2 : 1;
+    }
+  }
+
+  auto end = start;
+  for (int i = 0; i < count && end != text_.end(); i++) {
+    end += IsLeadingSurrogate(*start) ? 2 : 1;
+  }
+
+  if (start == end) {
+    return false;
+  }
+
+  auto new_base = text_.erase(start, end);
+
+  // Cursor moves only if deleted area is before it.
+  if (offset_from_cursor <= 0) {
+    selection_base_ = new_base;
+  }
+
+  // Clear selection.
+  selection_extent_ = selection_base_;
+
+  return true;
+}
+
 bool TextInputModel::MoveCursorToBeginning() {
   if (selection_base_ == text_.begin() && selection_extent_ == text_.begin())
     return false;
@@ -172,4 +218,13 @@
   return utf8_converter.to_bytes(text_);
 }
 
+int TextInputModel::GetCursorOffset() const {
+  // Measure the length of the current text up to the cursor.
+  // There is probably a much more efficient way of doing this.
+  auto leading_text = text_.substr(0, selection_extent_ - text_.begin());
+  std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>
+      utf8_converter;
+  return utf8_converter.to_bytes(leading_text).size();
+}
+
 }  // namespace flutter
diff --git a/shell/platform/common/cpp/text_input_model.h b/shell/platform/common/cpp/text_input_model.h
index 5444283..4494dca 100644
--- a/shell/platform/common/cpp/text_input_model.h
+++ b/shell/platform/common/cpp/text_input_model.h
@@ -33,12 +33,18 @@
   // code point.
   void AddCodePoint(char32_t c);
 
-  // Adds a UTF-16 text.
+  // Adds UTF-16 text.
   //
   // Either appends after the cursor (when selection base and extent are the
   // same), or deletes the selected text, replacing it with the given text.
   void AddText(const std::u16string& text);
 
+  // Adds UTF-8 text.
+  //
+  // Either appends after the cursor (when selection base and extent are the
+  // same), or deletes the selected text, replacing it with the given text.
+  void AddText(const std::string& text);
+
   // Deletes either the selection, or one character ahead of the cursor.
   //
   // Deleting one character ahead of the cursor occurs when the selection base
@@ -47,6 +53,17 @@
   // Returns true if any deletion actually occurred.
   bool Delete();
 
+  // Deletes text near the cursor.
+  //
+  // A section is made starting at @offset code points past the cursor (negative
+  // values go before the cursor). @count code points are removed. The selection
+  // may go outside the bounds of the text and will result in only the part
+  // selection that covers the available text being deleted. The existing
+  // selection is ignored and removed after this operation.
+  //
+  // Returns true if any deletion actually occurred.
+  bool DeleteSurrounding(int offset_from_cursor, int count);
+
   // Deletes either the selection, or one character behind the cursor.
   //
   // Deleting one character behind the cursor occurs when the selection base
@@ -77,9 +94,13 @@
   // Returns true if the cursor could be moved.
   bool MoveCursorToEnd();
 
-  // Get the current text
+  // Gets the current text as UTF-8.
   std::string GetText() const;
 
+  // Gets the cursor position as a byte offset in UTF-8 string returned from
+  // GetText().
+  int GetCursorOffset() const;
+
   // The position of the cursor
   int selection_base() const {
     return static_cast<int>(selection_base_ - text_.begin());
diff --git a/shell/platform/common/cpp/text_input_model_unittests.cc b/shell/platform/common/cpp/text_input_model_unittests.cc
index 7853015..09eac75 100644
--- a/shell/platform/common/cpp/text_input_model_unittests.cc
+++ b/shell/platform/common/cpp/text_input_model_unittests.cc
@@ -103,8 +103,8 @@
 TEST(TextInputModel, AddText) {
   auto model = std::make_unique<TextInputModel>("", "");
   model->AddText(u"ABCDE");
-  model->AddText(u"😄");
-  model->AddText(u"FGHIJ");
+  model->AddText("😄");
+  model->AddText("FGHIJ");
   EXPECT_EQ(model->selection_base(), 12);
   EXPECT_EQ(model->selection_extent(), 12);
   EXPECT_STREQ(model->GetText().c_str(), "ABCDE😄FGHIJ");
@@ -113,7 +113,7 @@
 TEST(TextInputModel, AddTextSelection) {
   auto model = std::make_unique<TextInputModel>("", "");
   EXPECT_TRUE(model->SetEditingState(1, 4, "ABCDE"));
-  model->AddText(u"xy");
+  model->AddText("xy");
   EXPECT_EQ(model->selection_base(), 3);
   EXPECT_EQ(model->selection_extent(), 3);
   EXPECT_STREQ(model->GetText().c_str(), "AxyE");
@@ -173,6 +173,96 @@
   EXPECT_STREQ(model->GetText().c_str(), "AE");
 }
 
+TEST(TextInputModel, DeleteSurroundingAtCursor) {
+  auto model = std::make_unique<TextInputModel>("", "");
+  model->SetEditingState(2, 2, "ABCDE");
+  EXPECT_TRUE(model->DeleteSurrounding(0, 1));
+  EXPECT_EQ(model->selection_base(), 2);
+  EXPECT_EQ(model->selection_extent(), 2);
+  EXPECT_STREQ(model->GetText().c_str(), "ABDE");
+}
+
+TEST(TextInputModel, DeleteSurroundingAtCursorAll) {
+  auto model = std::make_unique<TextInputModel>("", "");
+  model->SetEditingState(2, 2, "ABCDE");
+  EXPECT_TRUE(model->DeleteSurrounding(0, 3));
+  EXPECT_EQ(model->selection_base(), 2);
+  EXPECT_EQ(model->selection_extent(), 2);
+  EXPECT_STREQ(model->GetText().c_str(), "AB");
+}
+
+TEST(TextInputModel, DeleteSurroundingAtCursorGreedy) {
+  auto model = std::make_unique<TextInputModel>("", "");
+  model->SetEditingState(2, 2, "ABCDE");
+  EXPECT_TRUE(model->DeleteSurrounding(0, 4));
+  EXPECT_EQ(model->selection_base(), 2);
+  EXPECT_EQ(model->selection_extent(), 2);
+  EXPECT_STREQ(model->GetText().c_str(), "AB");
+}
+
+TEST(TextInputModel, DeleteSurroundingBeforeCursor) {
+  auto model = std::make_unique<TextInputModel>("", "");
+  model->SetEditingState(2, 2, "ABCDE");
+  EXPECT_TRUE(model->DeleteSurrounding(-1, 1));
+  EXPECT_EQ(model->selection_base(), 1);
+  EXPECT_EQ(model->selection_extent(), 1);
+  EXPECT_STREQ(model->GetText().c_str(), "ACDE");
+}
+
+TEST(TextInputModel, DeleteSurroundingBeforeCursorAll) {
+  auto model = std::make_unique<TextInputModel>("", "");
+  model->SetEditingState(2, 2, "ABCDE");
+  EXPECT_TRUE(model->DeleteSurrounding(-2, 2));
+  EXPECT_EQ(model->selection_base(), 0);
+  EXPECT_EQ(model->selection_extent(), 0);
+  EXPECT_STREQ(model->GetText().c_str(), "CDE");
+}
+
+TEST(TextInputModel, DeleteSurroundingBeforeCursorGreedy) {
+  auto model = std::make_unique<TextInputModel>("", "");
+  model->SetEditingState(2, 2, "ABCDE");
+  EXPECT_TRUE(model->DeleteSurrounding(-3, 3));
+  EXPECT_EQ(model->selection_base(), 0);
+  EXPECT_EQ(model->selection_extent(), 0);
+  EXPECT_STREQ(model->GetText().c_str(), "CDE");
+}
+
+TEST(TextInputModel, DeleteSurroundingAfterCursor) {
+  auto model = std::make_unique<TextInputModel>("", "");
+  model->SetEditingState(2, 2, "ABCDE");
+  EXPECT_TRUE(model->DeleteSurrounding(1, 1));
+  EXPECT_EQ(model->selection_base(), 2);
+  EXPECT_EQ(model->selection_extent(), 2);
+  EXPECT_STREQ(model->GetText().c_str(), "ABCE");
+}
+
+TEST(TextInputModel, DeleteSurroundingAfterCursorAll) {
+  auto model = std::make_unique<TextInputModel>("", "");
+  model->SetEditingState(2, 2, "ABCDE");
+  EXPECT_TRUE(model->DeleteSurrounding(1, 2));
+  EXPECT_EQ(model->selection_base(), 2);
+  EXPECT_EQ(model->selection_extent(), 2);
+  EXPECT_STREQ(model->GetText().c_str(), "ABC");
+}
+
+TEST(TextInputModel, DeleteSurroundingAfterCursorGreedy) {
+  auto model = std::make_unique<TextInputModel>("", "");
+  model->SetEditingState(2, 2, "ABCDE");
+  EXPECT_TRUE(model->DeleteSurrounding(1, 3));
+  EXPECT_EQ(model->selection_base(), 2);
+  EXPECT_EQ(model->selection_extent(), 2);
+  EXPECT_STREQ(model->GetText().c_str(), "ABC");
+}
+
+TEST(TextInputModel, DeleteSurroundingSelection) {
+  auto model = std::make_unique<TextInputModel>("", "");
+  model->SetEditingState(2, 3, "ABCDE");
+  EXPECT_TRUE(model->DeleteSurrounding(0, 1));
+  EXPECT_EQ(model->selection_base(), 3);
+  EXPECT_EQ(model->selection_extent(), 3);
+  EXPECT_STREQ(model->GetText().c_str(), "ABCE");
+}
+
 TEST(TextInputModel, BackspaceStart) {
   auto model = std::make_unique<TextInputModel>("", "");
   EXPECT_TRUE(model->SetEditingState(0, 0, "ABCDE"));
@@ -380,4 +470,25 @@
   EXPECT_STREQ(model->GetText().c_str(), "ABCDE");
 }
 
+TEST(TextInputModel, GetCursorOffset) {
+  auto model = std::make_unique<TextInputModel>("", "");
+  // These characters take 1, 2, 3 and 4 bytes in UTF-8.
+  model->SetEditingState(0, 0, "$¢€𐍈");
+  EXPECT_EQ(model->GetCursorOffset(), 0);
+  EXPECT_TRUE(model->MoveCursorForward());
+  EXPECT_EQ(model->GetCursorOffset(), 1);
+  EXPECT_TRUE(model->MoveCursorForward());
+  EXPECT_EQ(model->GetCursorOffset(), 3);
+  EXPECT_TRUE(model->MoveCursorForward());
+  EXPECT_EQ(model->GetCursorOffset(), 6);
+  EXPECT_TRUE(model->MoveCursorForward());
+  EXPECT_EQ(model->GetCursorOffset(), 10);
+}
+
+TEST(TextInputModel, GetCursorOffsetSelection) {
+  auto model = std::make_unique<TextInputModel>("", "");
+  model->SetEditingState(1, 4, "ABCDE");
+  EXPECT_EQ(model->GetCursorOffset(), 4);
+}
+
 }  // namespace flutter
diff --git a/shell/platform/glfw/BUILD.gn b/shell/platform/glfw/BUILD.gn
index c00df52..7771c67 100644
--- a/shell/platform/glfw/BUILD.gn
+++ b/shell/platform/glfw/BUILD.gn
@@ -53,6 +53,7 @@
     ":flutter_glfw_headers",
     "//build/secondary/third_party/glfw",
     "//flutter/shell/platform/common/cpp:common_cpp",
+    "//flutter/shell/platform/common/cpp:common_cpp_input",
     "//flutter/shell/platform/common/cpp/client_wrapper:client_wrapper",
     "//flutter/shell/platform/embedder:embedder_with_symbol_prefix",
     "//flutter/shell/platform/glfw/client_wrapper:client_wrapper_glfw",
diff --git a/shell/platform/linux/BUILD.gn b/shell/platform/linux/BUILD.gn
index d425088..809d5a2 100644
--- a/shell/platform/linux/BUILD.gn
+++ b/shell/platform/linux/BUILD.gn
@@ -94,6 +94,7 @@
     "fl_standard_message_codec.cc",
     "fl_standard_method_codec.cc",
     "fl_string_codec.cc",
+    "fl_text_input_plugin.cc",
     "fl_value.cc",
     "fl_view.cc",
   ]
@@ -108,6 +109,7 @@
   defines = [ "FLUTTER_LINUX_COMPILATION" ]
 
   deps = [
+    "//flutter/shell/platform/common/cpp:common_cpp_input",
     "//flutter/shell/platform/embedder:embedder_with_symbol_prefix",
     "//third_party/rapidjson",
   ]
diff --git a/shell/platform/linux/fl_text_input_plugin.cc b/shell/platform/linux/fl_text_input_plugin.cc
new file mode 100644
index 0000000..51127bc
--- /dev/null
+++ b/shell/platform/linux/fl_text_input_plugin.cc
@@ -0,0 +1,290 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "flutter/shell/platform/linux/fl_text_input_plugin.h"
+
+#include "flutter/shell/platform/common/cpp/text_input_model.h"
+#include "flutter/shell/platform/linux/public/flutter_linux/fl_json_method_codec.h"
+#include "flutter/shell/platform/linux/public/flutter_linux/fl_method_channel.h"
+
+#include <gtk/gtk.h>
+
+static constexpr char kChannelName[] = "flutter/textinput";
+
+static constexpr char kSetClientMethod[] = "TextInput.setClient";
+static constexpr char kShowMethod[] = "TextInput.show";
+static constexpr char kSetEditingStateMethod[] = "TextInput.setEditingState";
+static constexpr char kClearClientMethod[] = "TextInput.clearClient";
+static constexpr char kHideMethod[] = "TextInput.hide";
+static constexpr char kUpdateEditingStateMethod[] =
+    "TextInputClient.updateEditingState";
+static constexpr char kPerformActionMethod[] = "TextInputClient.performAction";
+
+static constexpr char kInputActionKey[] = "inputAction";
+static constexpr char kTextKey[] = "text";
+static constexpr char kSelectionBaseKey[] = "selectionBase";
+static constexpr char kSelectionExtentKey[] = "selectionExtent";
+static constexpr char kSelectionAffinityKey[] = "selectionAffinity";
+static constexpr char kSelectionIsDirectionalKey[] = "selectionIsDirectional";
+static constexpr char kComposingBaseKey[] = "composingBase";
+static constexpr char kComposingExtentKey[] = "composingExtent";
+
+static constexpr char kTextAffinityDownstream[] = "TextAffinity.downstream";
+
+static constexpr int64_t kClientIdUnset = -1;
+
+struct _FlTextInputPlugin {
+  GObject parent_instance;
+
+  FlMethodChannel* channel;
+
+  // Client ID provided by Flutter to report events with.
+  int64_t client_id;
+
+  // Input action to perform when enter pressed.
+  gchar* input_action;
+
+  // Input method.
+  GtkIMContext* im_context;
+
+  flutter::TextInputModel* text_model;
+};
+
+G_DEFINE_TYPE(FlTextInputPlugin, fl_text_input_plugin, G_TYPE_OBJECT)
+
+// Completes method call and returns TRUE if the call was successful.
+static gboolean finish_method(GObject* object,
+                              GAsyncResult* result,
+                              GError** error) {
+  g_autoptr(FlMethodResponse) response = fl_method_channel_invoke_method_finish(
+      FL_METHOD_CHANNEL(object), result, error);
+  if (response == nullptr)
+    return FALSE;
+  return fl_method_response_get_result(response, error) != nullptr;
+}
+
+// Called when a response is received from TextInputClient.updateEditingState()
+static void update_editing_state_response_cb(GObject* object,
+                                             GAsyncResult* result,
+                                             gpointer user_data) {
+  g_autoptr(GError) error = nullptr;
+  if (!finish_method(object, result, &error)) {
+    g_warning("Failed to call %s: %s", kUpdateEditingStateMethod,
+              error->message);
+  }
+}
+
+// Informs Flutter of text input changes.
+static void update_editing_state(FlTextInputPlugin* self) {
+  g_autoptr(FlValue) args = fl_value_new_list();
+  fl_value_append_take(args, fl_value_new_int(self->client_id));
+  g_autoptr(FlValue) value = fl_value_new_map();
+
+  fl_value_set_string_take(
+      value, kTextKey,
+      fl_value_new_string(self->text_model->GetText().c_str()));
+  fl_value_set_string_take(
+      value, kSelectionBaseKey,
+      fl_value_new_int(self->text_model->selection_base()));
+  fl_value_set_string_take(
+      value, kSelectionExtentKey,
+      fl_value_new_int(self->text_model->selection_extent()));
+
+  // The following keys are not implemented and set to default values.
+  fl_value_set_string_take(value, kSelectionAffinityKey,
+                           fl_value_new_string(kTextAffinityDownstream));
+  fl_value_set_string_take(value, kSelectionIsDirectionalKey,
+                           fl_value_new_bool(FALSE));
+  fl_value_set_string_take(value, kComposingBaseKey, fl_value_new_int(-1));
+  fl_value_set_string_take(value, kComposingExtentKey, fl_value_new_int(-1));
+
+  fl_value_append(args, value);
+
+  fl_method_channel_invoke_method(self->channel, kUpdateEditingStateMethod,
+                                  args, nullptr,
+                                  update_editing_state_response_cb, self);
+}
+
+// Called when a response is received from TextInputClient.performAction()
+static void perform_action_response_cb(GObject* object,
+                                       GAsyncResult* result,
+                                       gpointer user_data) {
+  g_autoptr(GError) error = nullptr;
+  if (!finish_method(object, result, &error))
+    g_warning("Failed to call %s: %s", kPerformActionMethod, error->message);
+}
+
+// Inform Flutter that the input has been activated.
+static void perform_action(FlTextInputPlugin* self) {
+  g_return_if_fail(FL_IS_TEXT_INPUT_PLUGIN(self));
+  g_return_if_fail(self->client_id != 0);
+  g_return_if_fail(self->input_action != nullptr);
+
+  g_autoptr(FlValue) args = fl_value_new_list();
+  fl_value_append_take(args, fl_value_new_int(self->client_id));
+  fl_value_append_take(args, fl_value_new_string(self->input_action));
+
+  fl_method_channel_invoke_method(self->channel, kPerformActionMethod, args,
+                                  nullptr, perform_action_response_cb, self);
+}
+
+// Signal handler for GtkIMContext::commit
+static void im_commit_cb(FlTextInputPlugin* self, const gchar* text) {
+  self->text_model->AddText(text);
+  update_editing_state(self);
+}
+
+// Signal handler for GtkIMContext::retrieve-surrounding
+static gboolean im_retrieve_surrounding_cb(FlTextInputPlugin* self) {
+  auto text = self->text_model->GetText();
+  size_t cursor_offset = self->text_model->GetCursorOffset();
+  gtk_im_context_set_surrounding(self->im_context, text.c_str(), -1,
+                                 cursor_offset);
+  return TRUE;
+}
+
+// Signal handler for GtkIMContext::delete-surrounding
+static gboolean im_delete_surrounding_cb(FlTextInputPlugin* self,
+                                         gint offset,
+                                         gint n_chars) {
+  if (self->text_model->DeleteSurrounding(offset, n_chars))
+    update_editing_state(self);
+  return TRUE;
+}
+
+// Called when a method call is received from Flutter.
+static void method_call_cb(FlMethodChannel* channel,
+                           FlMethodCall* method_call,
+                           gpointer user_data) {
+  FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(user_data);
+
+  const gchar* method = fl_method_call_get_name(method_call);
+  FlValue* args = fl_method_call_get_args(method_call);
+
+  if (strcmp(method, kSetClientMethod) == 0) {
+    self->client_id = fl_value_get_int(fl_value_get_list_value(args, 0));
+    FlValue* config_value = fl_value_get_list_value(args, 1);
+    g_free(self->input_action);
+    FlValue* input_action_value =
+        fl_value_lookup_string(config_value, kInputActionKey);
+    if (fl_value_get_type(input_action_value) == FL_VALUE_TYPE_STRING)
+      self->input_action = g_strdup(fl_value_get_string(input_action_value));
+    fl_method_call_respond_success(method_call, nullptr, nullptr);
+  } else if (strcmp(method, kShowMethod) == 0) {
+    gtk_im_context_focus_in(self->im_context);
+    fl_method_call_respond_success(method_call, nullptr, nullptr);
+  } else if (strcmp(method, kSetEditingStateMethod) == 0) {
+    const gchar* text =
+        fl_value_get_string(fl_value_lookup_string(args, kTextKey));
+    int64_t selection_base =
+        fl_value_get_int(fl_value_lookup_string(args, kSelectionBaseKey));
+    int64_t selection_extent =
+        fl_value_get_int(fl_value_lookup_string(args, kSelectionExtentKey));
+
+    self->text_model->SetEditingState(selection_base, selection_extent, text);
+
+    fl_method_call_respond_success(method_call, nullptr, nullptr);
+  } else if (strcmp(method, kClearClientMethod) == 0) {
+    self->client_id = kClientIdUnset;
+    fl_method_call_respond_success(method_call, nullptr, nullptr);
+  } else if (strcmp(method, kHideMethod) == 0) {
+    gtk_im_context_focus_out(self->im_context);
+    fl_method_call_respond_success(method_call, nullptr, nullptr);
+  } else
+    fl_method_call_respond_not_implemented(method_call, nullptr);
+}
+
+static void fl_text_input_plugin_dispose(GObject* object) {
+  FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(object);
+
+  g_clear_object(&self->channel);
+  g_clear_pointer(&self->input_action, g_free);
+  g_clear_object(&self->im_context);
+  if (self->text_model != nullptr) {
+    delete self->text_model;
+    self->text_model = nullptr;
+  }
+
+  G_OBJECT_CLASS(fl_text_input_plugin_parent_class)->dispose(object);
+}
+
+static void fl_text_input_plugin_class_init(FlTextInputPluginClass* klass) {
+  G_OBJECT_CLASS(klass)->dispose = fl_text_input_plugin_dispose;
+}
+
+static void fl_text_input_plugin_init(FlTextInputPlugin* self) {
+  self->client_id = kClientIdUnset;
+  self->im_context = gtk_im_multicontext_new();
+  g_signal_connect_object(self->im_context, "commit", G_CALLBACK(im_commit_cb),
+                          self, G_CONNECT_SWAPPED);
+  g_signal_connect_object(self->im_context, "retrieve-surrounding",
+                          G_CALLBACK(im_retrieve_surrounding_cb), self,
+                          G_CONNECT_SWAPPED);
+  g_signal_connect_object(self->im_context, "delete-surrounding",
+                          G_CALLBACK(im_delete_surrounding_cb), self,
+                          G_CONNECT_SWAPPED);
+  self->text_model = new flutter::TextInputModel("", "");
+}
+
+FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger) {
+  g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr);
+
+  FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(
+      g_object_new(fl_text_input_plugin_get_type(), nullptr));
+
+  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
+  self->channel =
+      fl_method_channel_new(messenger, kChannelName, FL_METHOD_CODEC(codec));
+  fl_method_channel_set_method_call_handler(self->channel, method_call_cb, self,
+                                            nullptr);
+
+  return self;
+}
+
+gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self,
+                                              GdkEventKey* event) {
+  g_return_val_if_fail(FL_IS_TEXT_INPUT_PLUGIN(self), FALSE);
+  if (gtk_im_context_filter_keypress(self->im_context, event))
+    return TRUE;
+
+  // Handle navigation keys.
+  gboolean changed = FALSE;
+  if (event->type == GDK_KEY_PRESS) {
+    switch (event->keyval) {
+      case GDK_KEY_BackSpace:
+        changed = self->text_model->Backspace();
+        break;
+      case GDK_KEY_Delete:
+      case GDK_KEY_KP_Delete:
+        // Already handled inside Flutter.
+        break;
+      case GDK_KEY_End:
+      case GDK_KEY_KP_End:
+        changed = self->text_model->MoveCursorToEnd();
+        break;
+      case GDK_KEY_Return:
+      case GDK_KEY_KP_Enter:
+      case GDK_KEY_ISO_Enter:
+        perform_action(self);
+        break;
+      case GDK_KEY_Home:
+      case GDK_KEY_KP_Home:
+        changed = self->text_model->MoveCursorToBeginning();
+        break;
+      case GDK_KEY_Left:
+      case GDK_KEY_KP_Left:
+        // Already handled inside Flutter.
+        break;
+      case GDK_KEY_Right:
+      case GDK_KEY_KP_Right:
+        // Already handled inside Flutter.
+        break;
+    }
+  }
+
+  if (changed)
+    update_editing_state(self);
+
+  return FALSE;
+}
diff --git a/shell/platform/linux/fl_text_input_plugin.h b/shell/platform/linux/fl_text_input_plugin.h
new file mode 100644
index 0000000..270c46c
--- /dev/null
+++ b/shell/platform/linux/fl_text_input_plugin.h
@@ -0,0 +1,52 @@
+// 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_TEXT_INPUT_LINUX_FL_TEXT_INPUT_PLUGIN_H_
+#define FLUTTER_SHELL_TEXT_INPUT_LINUX_FL_TEXT_INPUT_PLUGIN_H_
+
+#include <gdk/gdk.h>
+
+#include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h"
+
+G_BEGIN_DECLS
+
+G_DECLARE_FINAL_TYPE(FlTextInputPlugin,
+                     fl_text_input_plugin,
+                     FL,
+                     TEXT_INPUT_PLUGIN,
+                     GObject);
+
+/**
+ * FlTextInputPlugin:
+ *
+ * #FlTextInputPlugin is a text_input channel that implements the shell side
+ * of TextInputPlugins.textInput from the Flutter services library.
+ */
+
+/**
+ * fl_text_input_plugin_new:
+ * @messenger: an #FlBinaryMessenger.
+ *
+ * Creates a new plugin that implements TextInputPlugins.textInput from the
+ * Flutter services library.
+ *
+ * Returns: a new #FlTextInputPlugin.
+ */
+FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger);
+
+/**
+ * fl_text_input_plugin_filter_keypress
+ * @self: an #FlTextInputPlugin.
+ * @event: a #GdkEventKey
+ *
+ * Process a Gdk key event.
+ *
+ * Returns: %TRUE if the event was used.
+ */
+gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self,
+                                              GdkEventKey* event);
+
+G_END_DECLS
+
+#endif  // FLUTTER_SHELL_TEXT_INPUT_LINUX_FL_TEXT_INPUT_PLUGIN_H_
diff --git a/shell/platform/linux/fl_view.cc b/shell/platform/linux/fl_view.cc
index b3d3bb6..85e3517 100644
--- a/shell/platform/linux/fl_view.cc
+++ b/shell/platform/linux/fl_view.cc
@@ -8,6 +8,7 @@
 #include "flutter/shell/platform/linux/fl_key_event_plugin.h"
 #include "flutter/shell/platform/linux/fl_plugin_registrar_private.h"
 #include "flutter/shell/platform/linux/fl_renderer_x11.h"
+#include "flutter/shell/platform/linux/fl_text_input_plugin.h"
 #include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h"
 #include "flutter/shell/platform/linux/public/flutter_linux/fl_plugin_registry.h"
 
@@ -32,6 +33,7 @@
 
   // Flutter system channel handlers.
   FlKeyEventPlugin* key_event_plugin;
+  FlTextInputPlugin* text_input_plugin;
 };
 
 enum { PROP_FLUTTER_PROJECT = 1, PROP_LAST };
@@ -114,6 +116,7 @@
   // Create system channel handlers
   FlBinaryMessenger* messenger = fl_engine_get_binary_messenger(self->engine);
   self->key_event_plugin = fl_key_event_plugin_new(messenger);
+  self->text_input_plugin = fl_text_input_plugin_new(messenger);
 }
 
 static void fl_view_set_property(GObject* object,
@@ -156,6 +159,7 @@
   g_clear_object(&self->renderer);
   g_clear_object(&self->engine);
   g_clear_object(&self->key_event_plugin);
+  g_clear_object(&self->text_input_plugin);
 
   G_OBJECT_CLASS(fl_view_parent_class)->dispose(object);
 }
@@ -256,6 +260,9 @@
 static gboolean fl_view_key_press_event(GtkWidget* widget, GdkEventKey* event) {
   FlView* self = FL_VIEW(widget);
 
+  if (fl_text_input_plugin_filter_keypress(self->text_input_plugin, event))
+    return TRUE;
+
   fl_key_event_plugin_send_key_event(self->key_event_plugin, event);
 
   return TRUE;
@@ -266,6 +273,9 @@
                                           GdkEventKey* event) {
   FlView* self = FL_VIEW(widget);
 
+  if (fl_text_input_plugin_filter_keypress(self->text_input_plugin, event))
+    return TRUE;
+
   fl_key_event_plugin_send_key_event(self->key_event_plugin, event);
 
   return TRUE;
diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn
index ce8c6c0..53d335c 100644
--- a/shell/platform/windows/BUILD.gn
+++ b/shell/platform/windows/BUILD.gn
@@ -72,6 +72,7 @@
   deps = [
     ":flutter_windows_headers",
     "//flutter/shell/platform/common/cpp:common_cpp",
+    "//flutter/shell/platform/common/cpp:common_cpp_input",
     "//flutter/shell/platform/common/cpp/client_wrapper:client_wrapper",
     "//flutter/shell/platform/embedder:embedder_with_symbol_prefix",
     "//flutter/shell/platform/windows/client_wrapper:client_wrapper_windows",