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

Commit df5568d

Browse files
committed
Add multi-step IME support to TextInputModel
This updates the platform-independent TextInputModel to add support for input method (abbreviated IM or IME) composing regions. In contrast to languages such as English, where keyboard input is managed keystroke-by-keystroke, languages such as Japanese require a multi-step input process wherein the user begins a composing sequence, during which point their keystrokes are captured by a system input method and converted into a text sequence. During composing, the user is able to edit the composing range and manage the conversion from keyboard input to text before eventually committing the text to the underlying text input field. To illustrate this, in Japanese, this sequence might look something like the following: 1. User types 'k'. The character 'k' is added to the composing region. Typically, the text 'k' will be inserted inline into the underlying text field but the composing range will be highlighted in some manner, frequently with a highlight or underline. 2. User types 'a'. The composing range is replaced with the phonetic kana character 'か' (ka). The composing range continues to be highlighted. 3. User types 'k'. The character 'k' is appended to the composing range such that the highlighted text is now 'かk' 4. User types 'u'. The trailing 'k' is replaced with the phonetic kana character 'く' (ku) such that the composing range now reads 'かく' The composing range continues to be highlighted. 5. The user presses the space bar to convert the kana characters to kanji. The composing range is replaced with '書く' (kaku: to write). 6. The user presses the space bar again to show other conversions. The user's configured input method (for example, ibus) pops up a completions menu populated with alternatives such as 各 (kaku: every), 描く (kaku: to draw), 核 (kaku: pit of a fruit, nucleus), 角 (kaku: angle), etc. 7. The user uses the arrow keys to navigate the completions menu and select the alternative to input. As they do, the inline composing region in the text field is updated. It continues to be highlighted or underlined. 8. The user hits enter to commit the composing region. The text is committed to the underlying text field and the visual highlighting is removed. 9. If the user presses another key, a new composing sequence begins. If a selection is present when composing begins, it is preserved until the first keypress of input is received, at which point the selection is deleted. If a composing sequence is aborted before the first keypress, the selection is preserved. Creating a new selection (with the mouse, for example) aborts composing and the composing region is automatically committed. A composing range and selection, both with an extent, are not permitted to co-exist. During composing, keyboard navigation via the arrow keys, or home and end (or equivalent shortcuts) is restricted to the composing range, as are deletions via backspace and the delete key. This patch adds two new private convenience methods, `editing_range` and `text_range`. The former returns the range for which editing is currently active -- the composing range, if composing, otherwise the full range of the text. The latter, returns a range from position 0 (inclusive) to `text_.length()` exclusive.
1 parent b715d3f commit df5568d

File tree

3 files changed

+1079
-62
lines changed

3 files changed

+1079
-62
lines changed

shell/platform/common/cpp/text_input_model.cc

Lines changed: 101 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright 2013 The Flutter Authors. All rights reserved.
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
4-
// FLUTTER_NOLINT
54

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

@@ -39,25 +38,88 @@ void TextInputModel::SetText(const std::string& text) {
3938
utf16_converter;
4039
text_ = utf16_converter.from_bytes(text);
4140
selection_ = TextRange(0);
41+
composing_range_ = TextRange(0);
4242
}
4343

4444
bool TextInputModel::SetSelection(const TextRange& range) {
45-
if (!text_range().Contains(range)) {
45+
if (composing_ && !range.collapsed()) {
46+
return false;
47+
}
48+
if (!editable_range().Contains(range)) {
4649
return false;
4750
}
4851
selection_ = range;
4952
return true;
5053
}
5154

55+
bool TextInputModel::SetComposingRange(const TextRange& range,
56+
size_t cursor_offset) {
57+
if (!composing_ || !text_range().Contains(range)) {
58+
return false;
59+
}
60+
composing_range_ = range;
61+
selection_ = TextRange(range.start() + cursor_offset);
62+
return true;
63+
}
64+
65+
void TextInputModel::BeginComposing() {
66+
composing_ = true;
67+
composing_range_ = TextRange(selection_.start());
68+
}
69+
70+
void TextInputModel::UpdateComposingText(const std::string& composing_text) {
71+
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>
72+
utf16_converter;
73+
std::u16string text = utf16_converter.from_bytes(composing_text);
74+
75+
// Preserve selection if we get a no-op update to the composing region.
76+
if (text.length() == 0 && composing_range_.collapsed()) {
77+
return;
78+
}
79+
DeleteSelected();
80+
text_.replace(composing_range_.start(), composing_range_.length(), text);
81+
SetComposingLength(text.length());
82+
selection_ = TextRange(composing_range_.end());
83+
}
84+
85+
void TextInputModel::CommitComposing() {
86+
// Preserve selection if no composing text was entered.
87+
if (composing_range_.collapsed()) {
88+
return;
89+
}
90+
composing_range_ = TextRange(composing_range_.end());
91+
selection_ = composing_range_;
92+
}
93+
94+
void TextInputModel::EndComposing() {
95+
composing_ = false;
96+
composing_range_ = TextRange(0);
97+
}
98+
5299
bool TextInputModel::DeleteSelected() {
53100
if (selection_.collapsed()) {
54101
return false;
55102
}
56-
text_.erase(selection_.start(), selection_.length());
57-
selection_ = TextRange(selection_.start());
103+
size_t start = selection_.start();
104+
text_.erase(start, selection_.length());
105+
selection_ = TextRange(start);
106+
if (composing_) {
107+
// This occurs only immediately after composing has begun with a selection.
108+
composing_range_ = selection_;
109+
}
58110
return true;
59111
}
60112

113+
void TextInputModel::SetComposingLength(size_t length) {
114+
if (composing_range_.reversed()) {
115+
size_t extent = composing_range_.extent();
116+
composing_range_ = TextRange(extent + length, extent);
117+
} else {
118+
size_t base = composing_range_.base();
119+
composing_range_ = TextRange(base, base + length);
120+
}
121+
}
122+
61123
void TextInputModel::AddCodePoint(char32_t c) {
62124
if (c <= 0xFFFF) {
63125
AddText(std::u16string({static_cast<char16_t>(c)}));
@@ -74,6 +136,12 @@ void TextInputModel::AddCodePoint(char32_t c) {
74136

75137
void TextInputModel::AddText(const std::u16string& text) {
76138
DeleteSelected();
139+
if (composing_) {
140+
// Delete the current composing text, set the cursor to composing start.
141+
text_.erase(composing_range_.start(), composing_range_.length());
142+
selection_ = TextRange(composing_range_.start());
143+
SetComposingLength(text.length());
144+
}
77145
size_t position = selection_.position();
78146
text_.insert(position, text);
79147
selection_ = TextRange(position + text.length());
@@ -89,12 +157,15 @@ bool TextInputModel::Backspace() {
89157
if (DeleteSelected()) {
90158
return true;
91159
}
92-
// If there's no selection, delete the preceding codepoint.
160+
// There is no selection. Delete the preceding codepoint.
93161
size_t position = selection_.position();
94-
if (position != 0) {
162+
if (position != editable_range().start()) {
95163
int count = IsTrailingSurrogate(text_.at(position - 1)) ? 2 : 1;
96164
text_.erase(position - count, count);
97165
selection_ = TextRange(position - count);
166+
if (composing_) {
167+
SetComposingLength(composing_range_.length() - count);
168+
}
98169
return true;
99170
}
100171
return false;
@@ -104,62 +175,74 @@ bool TextInputModel::Delete() {
104175
if (DeleteSelected()) {
105176
return true;
106177
}
107-
// If there's no selection, delete the preceding codepoint.
178+
// There is no selection. Delete the preceding codepoint.
108179
size_t position = selection_.position();
109-
if (position != text_.length()) {
180+
if (position < editable_range().end()) {
110181
int count = IsLeadingSurrogate(text_.at(position)) ? 2 : 1;
111182
text_.erase(position, count);
183+
if (composing_) {
184+
SetComposingLength(composing_range_.length() - count);
185+
}
112186
return true;
113187
}
114188
return false;
115189
}
116190

117191
bool TextInputModel::DeleteSurrounding(int offset_from_cursor, int count) {
192+
size_t max_pos = editable_range().end();
118193
size_t start = selection_.extent();
119194
if (offset_from_cursor < 0) {
120195
for (int i = 0; i < -offset_from_cursor; i++) {
121196
// If requested start is before the available text then reduce the
122197
// number of characters to delete.
123-
if (start == 0) {
198+
if (start == editable_range().start()) {
124199
count = i;
125200
break;
126201
}
127202
start -= IsTrailingSurrogate(text_.at(start - 1)) ? 2 : 1;
128203
}
129204
} else {
130-
for (int i = 0; i < offset_from_cursor && start != text_.length(); i++) {
205+
for (int i = 0; i < offset_from_cursor && start != max_pos; i++) {
131206
start += IsLeadingSurrogate(text_.at(start)) ? 2 : 1;
132207
}
133208
}
134209

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

140215
if (start == end) {
141216
return false;
142217
}
143218

144-
text_.erase(start, end - start);
219+
auto deleted_length = end - start;
220+
text_.erase(start, deleted_length);
145221

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

225+
// Adjust composing range.
226+
if (composing_) {
227+
SetComposingLength(composing_range_.length() - deleted_length);
228+
}
149229
return true;
150230
}
151231

152232
bool TextInputModel::MoveCursorToBeginning() {
153-
if (selection_.collapsed() && selection_.position() == 0)
233+
size_t min_pos = editable_range().start();
234+
if (selection_.collapsed() && selection_.position() == min_pos) {
154235
return false;
155-
selection_ = TextRange(0);
236+
}
237+
selection_ = TextRange(min_pos);
156238
return true;
157239
}
158240

159241
bool TextInputModel::MoveCursorToEnd() {
160-
size_t max_pos = text_.length();
161-
if (selection_.collapsed() && selection_.position() == max_pos)
242+
size_t max_pos = editable_range().end();
243+
if (selection_.collapsed() && selection_.position() == max_pos) {
162244
return false;
245+
}
163246
selection_ = TextRange(max_pos);
164247
return true;
165248
}
@@ -172,7 +255,7 @@ bool TextInputModel::MoveCursorForward() {
172255
}
173256
// Otherwise, move the cursor forward.
174257
size_t position = selection_.position();
175-
if (position != text_.length()) {
258+
if (position != editable_range().end()) {
176259
int count = IsLeadingSurrogate(text_.at(position)) ? 2 : 1;
177260
selection_ = TextRange(position + count);
178261
return true;
@@ -188,7 +271,7 @@ bool TextInputModel::MoveCursorBack() {
188271
}
189272
// Otherwise, move the cursor backward.
190273
size_t position = selection_.position();
191-
if (position != 0) {
274+
if (position != editable_range().start()) {
192275
int count = IsTrailingSurrogate(text_.at(position - 1)) ? 2 : 1;
193276
selection_ = TextRange(position - count);
194277
return true;

shell/platform/common/cpp/text_input_model.h

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,43 @@ class TextInputModel {
2828
// Attempts to set the text selection.
2929
//
3030
// Returns false if the selection is not within the bounds of the text.
31+
// While in composing mode, the selection is restricted to the composing
32+
// range; otherwise, it is restricted to the length of the text.
3133
bool SetSelection(const TextRange& range);
3234

35+
// Attempts to set the composing range.
36+
//
37+
// Returns false if the range or offset are out of range for the text, or if
38+
// the offset is outside the composing range.
39+
bool SetComposingRange(const TextRange& range, size_t cursor_offset);
40+
41+
// Begins IME composing mode.
42+
//
43+
// Resets the composing base and extent to the selection start. The existing
44+
// selection is preserved in case composing is aborted with no changes. Until
45+
// |EndComposing| is called, any further changes to selection base and extent
46+
// are restricted to the composing range.
47+
void BeginComposing();
48+
49+
// Replaces the composing range with new text.
50+
//
51+
// If a selection of non-zero length exists, it is deleted if the composing
52+
// text is non-empty. The composing range is adjusted to the length of
53+
// |composing_text| and the selection base and offset are set to the end of
54+
// the composing range.
55+
void UpdateComposingText(const std::string& composing_text);
56+
57+
// Commits composing range to the string.
58+
//
59+
// Causes the composing base and extent to be collapsed to the end of the
60+
// range.
61+
void CommitComposing();
62+
63+
// Ends IME composing mode.
64+
//
65+
// Collapses the composing base and offset to 0.
66+
void EndComposing();
67+
3368
// Adds a Unicode code point.
3469
//
3570
// Either appends after the cursor (when selection base and extent are the
@@ -52,48 +87,62 @@ class TextInputModel {
5287
// Deletes either the selection, or one character ahead of the cursor.
5388
//
5489
// Deleting one character ahead of the cursor occurs when the selection base
55-
// and extent are the same.
90+
// and extent are the same. When composing is active, deletions are
91+
// restricted to text between the composing base and extent.
5692
//
5793
// Returns true if any deletion actually occurred.
5894
bool Delete();
5995

6096
// Deletes text near the cursor.
6197
//
62-
// A section is made starting at @offset code points past the cursor (negative
63-
// values go before the cursor). @count code points are removed. The selection
64-
// may go outside the bounds of the text and will result in only the part
65-
// selection that covers the available text being deleted. The existing
66-
// selection is ignored and removed after this operation.
98+
// A section is made starting at |offset_from_cursor| code points past the
99+
// cursor (negative values go before the cursor). |count| code points are
100+
// removed. The selection may go outside the bounds of the available text and
101+
// will result in only the part selection that covers the available text
102+
// being deleted. The existing selection is ignored and removed after this
103+
// operation. When composing is active, deletions are restricted to the
104+
// composing range.
67105
//
68106
// Returns true if any deletion actually occurred.
69107
bool DeleteSurrounding(int offset_from_cursor, int count);
70108

71109
// Deletes either the selection, or one character behind the cursor.
72110
//
73111
// Deleting one character behind the cursor occurs when the selection base
74-
// and extent are the same.
112+
// and extent are the same. When composing is active, deletions are
113+
// restricted to the text between the composing base and extent.
75114
//
76115
// Returns true if any deletion actually occurred.
77116
bool Backspace();
78117

79118
// Attempts to move the cursor backward.
80119
//
81120
// Returns true if the cursor could be moved. If a selection is active, moves
82-
// to the start of the selection.
121+
// to the start of the selection. If composing is active, motion is
122+
// restricted to the composing range.
83123
bool MoveCursorBack();
84124

85125
// Attempts to move the cursor forward.
86126
//
87127
// Returns true if the cursor could be moved. If a selection is active, moves
88-
// to the end of the selection.
128+
// to the end of the selection. If composing is active, motion is restricted
129+
// to the composing range.
89130
bool MoveCursorForward();
90131

91132
// Attempts to move the cursor to the beginning.
92133
//
134+
// If composing is active, the cursor is moved to the beginning of the
135+
// composing range; otherwise, it is moved to the beginning of the text. If
136+
// composing is active, motion is restricted to the composing range.
137+
//
93138
// Returns true if the cursor could be moved.
94139
bool MoveCursorToBeginning();
95140

96-
// Attempts to move the cursor to the back.
141+
// Attempts to move the cursor to the end.
142+
//
143+
// If composing is active, the cursor is moved to the end of the composing
144+
// range; otherwise, it is moved to the end of the text. If composing is
145+
// active, motion is restricted to the composing range.
97146
//
98147
// Returns true if the cursor could be moved.
99148
bool MoveCursorToEnd();
@@ -108,18 +157,39 @@ class TextInputModel {
108157
// The current selection.
109158
TextRange selection() const { return selection_; }
110159

160+
// The composing range.
161+
//
162+
// If not in composing mode, returns a collapsed range at position 0.
163+
TextRange composing_range() const { return composing_range_; }
164+
165+
// Whether multi-step input composing mode is active.
166+
bool composing() const { return composing_; }
167+
111168
private:
112169
// Deletes the current selection, if any.
113170
//
114171
// Returns true if any text is deleted. The selection base and extent are
115172
// reset to the start of the selected range.
116173
bool DeleteSelected();
117174

175+
// Adjusts the composing range to |length|.
176+
void SetComposingLength(size_t length);
177+
178+
// Returns the currently editable text range.
179+
//
180+
// In composing mode, returns the composing range; otherwise, returns a range
181+
// covering the entire text.
182+
TextRange editable_range() const {
183+
return composing_ ? composing_range_ : text_range();
184+
}
185+
118186
// Returns a range covering the entire text.
119187
TextRange text_range() const { return TextRange(0, text_.length()); }
120188

121189
std::u16string text_;
122190
TextRange selection_ = TextRange(0);
191+
TextRange composing_range_ = TextRange(0);
192+
bool composing_ = false;
123193
};
124194

125195
} // namespace flutter

0 commit comments

Comments
 (0)