From f3b4cf3045bae9ec20db254aebd4b9c80ec14186 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 7 Feb 2022 14:58:48 -0800 Subject: [PATCH] Linux TextEditingDeltas (flutter/engine#29215) The Linux platform can report the delta in text editing changes via enableDeltas, useful for custom rich text editors. --- .../platform/linux/fl_text_input_plugin.cc | 127 +++++++++++++++++- 1 file changed, 122 insertions(+), 5 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_text_input_plugin.cc b/engine/src/flutter/shell/platform/linux/fl_text_input_plugin.cc index 3d77b0ea3e8..f1723363226 100644 --- a/engine/src/flutter/shell/platform/linux/fl_text_input_plugin.cc +++ b/engine/src/flutter/shell/platform/linux/fl_text_input_plugin.cc @@ -6,6 +6,7 @@ #include +#include "flutter/shell/platform/common/text_editing_delta.h" #include "flutter/shell/platform/common/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" @@ -21,6 +22,8 @@ static constexpr char kClearClientMethod[] = "TextInput.clearClient"; static constexpr char kHideMethod[] = "TextInput.hide"; static constexpr char kUpdateEditingStateMethod[] = "TextInputClient.updateEditingState"; +static constexpr char kUpdateEditingStateWithDeltasMethod[] = + "TextInputClient.updateEditingStateWithDeltas"; static constexpr char kPerformActionMethod[] = "TextInputClient.performAction"; static constexpr char kSetEditableSizeAndTransform[] = "TextInput.setEditableSizeAndTransform"; @@ -28,6 +31,7 @@ static constexpr char kSetMarkedTextRect[] = "TextInput.setMarkedTextRect"; static constexpr char kInputActionKey[] = "inputAction"; static constexpr char kTextInputTypeKey[] = "inputType"; +static constexpr char kEnableDeltaModel[] = "enableDeltaModel"; static constexpr char kTextInputTypeNameKey[] = "name"; static constexpr char kTextKey[] = "text"; static constexpr char kSelectionBaseKey[] = "selectionBase"; @@ -67,6 +71,12 @@ struct FlTextInputPluginPrivate { // The type of the input method. FlTextInputType input_type; + // Whether to enable that the engine sends text input updates to the framework + // as TextEditingDeltas or as one TextEditingValue. + // For more information on the delta model, see: + // https://master-api.flutter.dev/flutter/services/TextInputConfiguration/enableDeltaModel.html + gboolean enable_delta_model; + // Input method. GtkIMContext* im_context; @@ -158,6 +168,65 @@ static void update_editing_state(FlTextInputPlugin* self) { update_editing_state_response_cb, self); } +// Informs Flutter of text input changes by passing just the delta. +static void update_editing_state_with_delta(FlTextInputPlugin* self, + flutter::TextEditingDelta* delta) { + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + + g_autoptr(FlValue) args = fl_value_new_list(); + fl_value_append_take(args, fl_value_new_int(priv->client_id)); + + g_autoptr(FlValue) deltaValue = fl_value_new_map(); + fl_value_set_string_take(deltaValue, "oldText", + fl_value_new_string(delta->old_text().c_str())); + + fl_value_set_string_take(deltaValue, "deltaText", + fl_value_new_string(delta->delta_text().c_str())); + + fl_value_set_string_take(deltaValue, "deltaStart", + fl_value_new_int(delta->delta_start())); + + fl_value_set_string_take(deltaValue, "deltaEnd", + fl_value_new_int(delta->delta_end())); + + flutter::TextRange selection = priv->text_model->selection(); + fl_value_set_string_take(deltaValue, "selectionBase", + fl_value_new_int(selection.base())); + + fl_value_set_string_take(deltaValue, "selectionExtent", + fl_value_new_int(selection.extent())); + + fl_value_set_string_take(deltaValue, "selectionAffinity", + fl_value_new_string(kTextAffinityDownstream)); + + fl_value_set_string_take(deltaValue, "selectionIsDirectional", + fl_value_new_bool(FALSE)); + + int composing_base = priv->text_model->composing() + ? priv->text_model->composing_range().base() + : -1; + fl_value_set_string_take(deltaValue, "composingBase", + fl_value_new_int(composing_base)); + + int composing_extent = priv->text_model->composing() + ? priv->text_model->composing_range().extent() + : -1; + fl_value_set_string_take(deltaValue, "composingExtent", + fl_value_new_int(composing_extent)); + + g_autoptr(FlValue) deltas = fl_value_new_list(); + fl_value_append(deltas, deltaValue); + g_autoptr(FlValue) value = fl_value_new_map(); + fl_value_set_string_take(value, "deltas", deltas); + + fl_value_append(args, value); + + fl_method_channel_invoke_method( + priv->channel, kUpdateEditingStateWithDeltasMethod, 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, @@ -201,6 +270,7 @@ static void im_preedit_start_cb(FlTextInputPlugin* self) { static void im_preedit_changed_cb(FlTextInputPlugin* self) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); + std::string text_before_change = priv->text_model->GetText(); g_autofree gchar* buf = nullptr; gint cursor_offset = 0; gtk_im_context_get_preedit_string(priv->im_context, &buf, nullptr, @@ -210,18 +280,35 @@ static void im_preedit_changed_cb(FlTextInputPlugin* self) { priv->text_model->SetSelection( flutter::TextRange(cursor_offset, cursor_offset)); - update_editing_state(self); + if (priv->enable_delta_model) { + std::string text(buf); + flutter::TextEditingDelta delta = flutter::TextEditingDelta( + text_before_change, priv->text_model->composing_range(), text); + update_editing_state_with_delta(self, &delta); + } else { + update_editing_state(self); + } } // Signal handler for GtkIMContext::commit static void im_commit_cb(FlTextInputPlugin* self, const gchar* text) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); + std::string text_before_change = priv->text_model->GetText(); + flutter::TextRange selection_before_change = priv->text_model->selection(); + priv->text_model->AddText(text); if (priv->text_model->composing()) { priv->text_model->CommitComposing(); } - update_editing_state(self); + + if (priv->enable_delta_model) { + flutter::TextEditingDelta delta = flutter::TextEditingDelta( + text_before_change, selection_before_change, text); + update_editing_state_with_delta(self, &delta); + } else { + update_editing_state(self); + } } // Signal handler for GtkIMContext::preedit-end @@ -229,7 +316,13 @@ static void im_preedit_end_cb(FlTextInputPlugin* self) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); priv->text_model->EndComposing(); - update_editing_state(self); + if (priv->enable_delta_model) { + flutter::TextEditingDelta delta = flutter::TextEditingDelta( + "", flutter::TextRange(-1, -1), priv->text_model->GetText()); + update_editing_state_with_delta(self, &delta); + } else { + update_editing_state(self); + } } // Signal handler for GtkIMContext::retrieve-surrounding @@ -249,8 +342,17 @@ static gboolean im_delete_surrounding_cb(FlTextInputPlugin* self, gint n_chars) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); + + std::string text_before_change = priv->text_model->GetText(); if (priv->text_model->DeleteSurrounding(offset, n_chars)) { - update_editing_state(self); + if (priv->enable_delta_model) { + flutter::TextEditingDelta delta = flutter::TextEditingDelta( + text_before_change, priv->text_model->composing_range(), + priv->text_model->GetText()); + update_editing_state_with_delta(self, &delta); + } else { + update_editing_state(self); + } } return TRUE; } @@ -274,6 +376,11 @@ static FlMethodResponse* set_client(FlTextInputPlugin* self, FlValue* args) { priv->input_action = g_strdup(fl_value_get_string(input_action_value)); } + FlValue* enable_delta_model_value = + fl_value_lookup_string(config_value, kEnableDeltaModel); + gboolean enable_delta_model = fl_value_get_bool(enable_delta_model_value); + priv->enable_delta_model = enable_delta_model; + // Reset the input type, then set only if appropriate. priv->input_type = FL_TEXT_INPUT_TYPE_TEXT; FlValue* input_type_value = @@ -521,6 +628,9 @@ static gboolean fl_text_input_plugin_filter_keypress_default( return TRUE; } + std::string text_before_change = priv->text_model->GetText(); + flutter::TextRange selection_before_change = priv->text_model->selection(); + // Handle the enter/return key. gboolean do_action = FALSE; // Handle navigation keys. @@ -565,7 +675,14 @@ static gboolean fl_text_input_plugin_filter_keypress_default( } if (changed) { - update_editing_state(self); + if (priv->enable_delta_model) { + flutter::TextEditingDelta delta = flutter::TextEditingDelta( + text_before_change, priv->text_model->composing_range(), + priv->text_model->GetText()); + update_editing_state_with_delta(self, &delta); + } else { + update_editing_state(self); + } } if (do_action) { perform_action(self);