Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,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