Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Add multi-step IME support to TextInputModel #21682

Merged
merged 2 commits into from
Oct 16, 2020
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
109 changes: 91 additions & 18 deletions shell/platform/common/cpp/text_input_model.cc
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// 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.
// FLUTTER_NOLINT

#include "flutter/shell/platform/common/cpp/text_input_model.h"

Expand Down Expand Up @@ -39,22 +38,75 @@ void TextInputModel::SetText(const std::string& text) {
utf16_converter;
text_ = utf16_converter.from_bytes(text);
selection_ = TextRange(0);
composing_range_ = TextRange(0);
}

bool TextInputModel::SetSelection(const TextRange& range) {
if (!text_range().Contains(range)) {
if (composing_ && !range.collapsed()) {
return false;
}
if (!editable_range().Contains(range)) {
return false;
}
selection_ = range;
return true;
}

bool TextInputModel::SetComposingRange(const TextRange& range,
size_t cursor_offset) {
if (!composing_ || !text_range().Contains(range)) {
return false;
}
composing_range_ = range;
selection_ = TextRange(range.start() + cursor_offset);
return true;
}

void TextInputModel::BeginComposing() {
composing_ = true;
composing_range_ = TextRange(selection_.start());
}

void TextInputModel::UpdateComposingText(const std::string& composing_text) {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>
utf16_converter;
std::u16string text = utf16_converter.from_bytes(composing_text);

// Preserve selection if we get a no-op update to the composing region.
if (text.length() == 0 && composing_range_.collapsed()) {
return;
}
DeleteSelected();
text_.replace(composing_range_.start(), composing_range_.length(), text);
composing_range_.set_end(composing_range_.start() + text.length());
selection_ = TextRange(composing_range_.end());
}

void TextInputModel::CommitComposing() {
// Preserve selection if no composing text was entered.
if (composing_range_.collapsed()) {
return;
}
composing_range_ = TextRange(composing_range_.end());
selection_ = composing_range_;
}

void TextInputModel::EndComposing() {
composing_ = false;
composing_range_ = TextRange(0);
}

bool TextInputModel::DeleteSelected() {
if (selection_.collapsed()) {
return false;
}
text_.erase(selection_.start(), selection_.length());
selection_ = TextRange(selection_.start());
size_t start = selection_.start();
text_.erase(start, selection_.length());
selection_ = TextRange(start);
if (composing_) {
// This occurs only immediately after composing has begun with a selection.
composing_range_ = selection_;
}
return true;
}

Expand All @@ -74,6 +126,12 @@ void TextInputModel::AddCodePoint(char32_t c) {

void TextInputModel::AddText(const std::u16string& text) {
DeleteSelected();
if (composing_) {
// Delete the current composing text, set the cursor to composing start.
text_.erase(composing_range_.start(), composing_range_.length());
selection_ = TextRange(composing_range_.start());
composing_range_.set_end(composing_range_.start() + text.length());
}
size_t position = selection_.position();
text_.insert(position, text);
selection_ = TextRange(position + text.length());
Expand All @@ -89,12 +147,15 @@ bool TextInputModel::Backspace() {
if (DeleteSelected()) {
return true;
}
// If there's no selection, delete the preceding codepoint.
// There is no selection. Delete the preceding codepoint.
size_t position = selection_.position();
if (position != 0) {
if (position != editable_range().start()) {
int count = IsTrailingSurrogate(text_.at(position - 1)) ? 2 : 1;
text_.erase(position - count, count);
selection_ = TextRange(position - count);
if (composing_) {
composing_range_.set_end(composing_range_.end() - count);
}
return true;
}
return false;
Expand All @@ -104,62 +165,74 @@ bool TextInputModel::Delete() {
if (DeleteSelected()) {
return true;
}
// If there's no selection, delete the preceding codepoint.
// There is no selection. Delete the preceding codepoint.
size_t position = selection_.position();
if (position != text_.length()) {
if (position < editable_range().end()) {
int count = IsLeadingSurrogate(text_.at(position)) ? 2 : 1;
text_.erase(position, count);
if (composing_) {
composing_range_.set_end(composing_range_.end() - count);
}
return true;
}
return false;
}

bool TextInputModel::DeleteSurrounding(int offset_from_cursor, int count) {
size_t max_pos = editable_range().end();
size_t 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 == 0) {
if (start == editable_range().start()) {
count = i;
break;
}
start -= IsTrailingSurrogate(text_.at(start - 1)) ? 2 : 1;
}
} else {
for (int i = 0; i < offset_from_cursor && start != text_.length(); i++) {
for (int i = 0; i < offset_from_cursor && start != max_pos; i++) {
start += IsLeadingSurrogate(text_.at(start)) ? 2 : 1;
}
}

auto end = start;
for (int i = 0; i < count && end != text_.length(); i++) {
for (int i = 0; i < count && end != max_pos; i++) {
end += IsLeadingSurrogate(text_.at(start)) ? 2 : 1;
}

if (start == end) {
return false;
}

text_.erase(start, end - start);
auto deleted_length = end - start;
text_.erase(start, deleted_length);

// Cursor moves only if deleted area is before it.
selection_ = TextRange(offset_from_cursor <= 0 ? start : selection_.start());

// Adjust composing range.
if (composing_) {
composing_range_.set_end(composing_range_.end() - deleted_length);
}
return true;
}

bool TextInputModel::MoveCursorToBeginning() {
if (selection_.collapsed() && selection_.position() == 0)
size_t min_pos = editable_range().start();
if (selection_.collapsed() && selection_.position() == min_pos) {
return false;
selection_ = TextRange(0);
}
selection_ = TextRange(min_pos);
return true;
}

bool TextInputModel::MoveCursorToEnd() {
size_t max_pos = text_.length();
if (selection_.collapsed() && selection_.position() == max_pos)
size_t max_pos = editable_range().end();
if (selection_.collapsed() && selection_.position() == max_pos) {
return false;
}
selection_ = TextRange(max_pos);
return true;
}
Expand All @@ -172,7 +245,7 @@ bool TextInputModel::MoveCursorForward() {
}
// Otherwise, move the cursor forward.
size_t position = selection_.position();
if (position != text_.length()) {
if (position != editable_range().end()) {
int count = IsLeadingSurrogate(text_.at(position)) ? 2 : 1;
selection_ = TextRange(position + count);
return true;
Expand All @@ -188,7 +261,7 @@ bool TextInputModel::MoveCursorBack() {
}
// Otherwise, move the cursor backward.
size_t position = selection_.position();
if (position != 0) {
if (position != editable_range().start()) {
int count = IsTrailingSurrogate(text_.at(position - 1)) ? 2 : 1;
selection_ = TextRange(position - count);
return true;
Expand Down
87 changes: 77 additions & 10 deletions shell/platform/common/cpp/text_input_model.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,43 @@ class TextInputModel {
// Attempts to set the text selection.
//
// Returns false if the selection is not within the bounds of the text.
// While in composing mode, the selection is restricted to the composing
// range; otherwise, it is restricted to the length of the text.
bool SetSelection(const TextRange& range);

// Attempts to set the composing range.
//
// Returns false if the range or offset are out of range for the text, or if
// the offset is outside the composing range.
bool SetComposingRange(const TextRange& range, size_t cursor_offset);

// Begins IME composing mode.
//
// Resets the composing base and extent to the selection start. The existing
// selection is preserved in case composing is aborted with no changes. Until
// |EndComposing| is called, any further changes to selection base and extent
// are restricted to the composing range.
void BeginComposing();

// Replaces the composing range with new text.
//
// If a selection of non-zero length exists, it is deleted if the composing
// text is non-empty. The composing range is adjusted to the length of
// |composing_text| and the selection base and offset are set to the end of
// the composing range.
void UpdateComposingText(const std::string& composing_text);

// Commits composing range to the string.
//
// Causes the composing base and extent to be collapsed to the end of the
// range.
void CommitComposing();

// Ends IME composing mode.
//
// Collapses the composing base and offset to 0.
void EndComposing();

// Adds a Unicode code point.
//
// Either appends after the cursor (when selection base and extent are the
Expand All @@ -52,48 +87,62 @@ class TextInputModel {
// Deletes either the selection, or one character ahead of the cursor.
//
// Deleting one character ahead of the cursor occurs when the selection base
// and extent are the same.
// and extent are the same. When composing is active, deletions are
// restricted to text between the composing base and extent.
//
// 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.
// A section is made starting at |offset_from_cursor| 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 available 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. When composing is active, deletions are restricted to the
// composing range.
//
// 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
// and extent are the same.
// and extent are the same. When composing is active, deletions are
// restricted to the text between the composing base and extent.
//
// Returns true if any deletion actually occurred.
bool Backspace();

// Attempts to move the cursor backward.
//
// Returns true if the cursor could be moved. If a selection is active, moves
// to the start of the selection.
// to the start of the selection. If composing is active, motion is
// restricted to the composing range.
bool MoveCursorBack();

// Attempts to move the cursor forward.
//
// Returns true if the cursor could be moved. If a selection is active, moves
// to the end of the selection.
// to the end of the selection. If composing is active, motion is restricted
// to the composing range.
bool MoveCursorForward();

// Attempts to move the cursor to the beginning.
//
// If composing is active, the cursor is moved to the beginning of the
// composing range; otherwise, it is moved to the beginning of the text. If
// composing is active, motion is restricted to the composing range.
//
// Returns true if the cursor could be moved.
bool MoveCursorToBeginning();

// Attempts to move the cursor to the back.
// Attempts to move the cursor to the end.
//
// If composing is active, the cursor is moved to the end of the composing
// range; otherwise, it is moved to the end of the text. If composing is
// active, motion is restricted to the composing range.
//
// Returns true if the cursor could be moved.
bool MoveCursorToEnd();
Expand All @@ -108,18 +157,36 @@ class TextInputModel {
// The current selection.
TextRange selection() const { return selection_; }

// The composing range.
//
// If not in composing mode, returns a collapsed range at position 0.
TextRange composing_range() const { return composing_range_; }

// Whether multi-step input composing mode is active.
bool composing() const { return composing_; }

private:
// Deletes the current selection, if any.
//
// Returns true if any text is deleted. The selection base and extent are
// reset to the start of the selected range.
bool DeleteSelected();

// Returns the currently editable text range.
//
// In composing mode, returns the composing range; otherwise, returns a range
// covering the entire text.
TextRange editable_range() const {
return composing_ ? composing_range_ : text_range();
}

// Returns a range covering the entire text.
TextRange text_range() const { return TextRange(0, text_.length()); }

std::u16string text_;
TextRange selection_ = TextRange(0);
TextRange composing_range_ = TextRange(0);
bool composing_ = false;
};

} // namespace flutter
Expand Down
Loading