Skip to content

Commit

Permalink
Add FlTextInputPlugin (flutter#18314)
Browse files Browse the repository at this point in the history
* Add FlTextInputPlugin
  • Loading branch information
robert-ancell authored May 28, 2020
1 parent 368fe27 commit 2bd95d9
Show file tree
Hide file tree
Showing 11 changed files with 568 additions and 12 deletions.
2 changes: 2 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,8 @@ FILE: ../../../flutter/shell/platform/linux/fl_standard_method_codec.cc
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
Expand Down
25 changes: 18 additions & 7 deletions shell/platform/common/cpp/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,28 @@ source_set("common_cpp_library_headers") {
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
Expand All @@ -44,7 +60,6 @@ source_set("common_cpp") {
"incoming_message_dispatcher.cc",
"json_message_codec.cc",
"json_method_codec.cc",
"text_input_model.cc",
]

configs += [ ":desktop_library_implementation" ]
Expand All @@ -59,11 +74,6 @@ source_set("common_cpp") {
":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/
Expand Down Expand Up @@ -117,6 +127,7 @@ executable("common_cpp_unittests") {
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",
Expand Down
55 changes: 55 additions & 0 deletions shell/platform/common/cpp/text_input_model.cc
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ void TextInputModel::AddText(const std::u16string& text) {
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();
Expand Down Expand Up @@ -113,6 +119,46 @@ bool TextInputModel::Delete() {
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;
Expand Down Expand Up @@ -172,4 +218,13 @@ std::string TextInputModel::GetText() const {
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
25 changes: 23 additions & 2 deletions shell/platform/common/cpp/text_input_model.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,18 @@ class TextInputModel {
// 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
Expand All @@ -47,6 +53,17 @@ class TextInputModel {
// 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
Expand Down Expand Up @@ -77,9 +94,13 @@ class TextInputModel {
// 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());
Expand Down
117 changes: 114 additions & 3 deletions shell/platform/common/cpp/text_input_model_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ TEST(TextInputModel, AddCodePointSelectionWideCharacter) {
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");
Expand All @@ -113,7 +113,7 @@ TEST(TextInputModel, AddText) {
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");
Expand Down Expand Up @@ -173,6 +173,96 @@ TEST(TextInputModel, DeleteSelection) {
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"));
Expand Down Expand Up @@ -380,4 +470,25 @@ TEST(TextInputModel, MoveCursorToEndSelection) {
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
1 change: 1 addition & 0 deletions shell/platform/glfw/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ source_set("flutter_glfw") {
":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",
Expand Down
2 changes: 2 additions & 0 deletions shell/platform/linux/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ source_set("flutter_linux") {
"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",
]
Expand All @@ -108,6 +109,7 @@ source_set("flutter_linux") {
defines = [ "FLUTTER_LINUX_COMPILATION" ]

deps = [
"//flutter/shell/platform/common/cpp:common_cpp_input",
"//flutter/shell/platform/embedder:embedder_with_symbol_prefix",
"//third_party/rapidjson",
]
Expand Down
Loading

0 comments on commit 2bd95d9

Please sign in to comment.