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

Commit e1984eb

Browse files
committed
Add multi-step input method support for Linux
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.
1 parent fbe6859 commit e1984eb

7 files changed

+1458
-41
lines changed

shell/platform/common/cpp/text_input_model.cc

Lines changed: 131 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ bool IsTrailingSurrogate(char32_t code_point) {
2828
return (code_point & 0xFFFFFC00) == 0xDC00;
2929
}
3030

31+
// Returns true if |x| is between |low| and |high| inclusive.
32+
bool InRange(size_t x, size_t low, size_t high) {
33+
return x >= low && x <= high;
34+
}
35+
3136
} // namespace
3237

3338
TextInputModel::TextInputModel() = default;
@@ -40,24 +45,100 @@ void TextInputModel::SetText(const std::string& text) {
4045
text_ = utf16_converter.from_bytes(text);
4146
selection_base_ = 0;
4247
selection_extent_ = 0;
48+
composing_base_ = 0;
49+
composing_extent_ = 0;
4350
}
4451

4552
bool TextInputModel::SetSelection(size_t base, size_t extent) {
46-
auto max_pos = text_.length();
47-
if (base > max_pos || extent > max_pos) {
53+
// Only collapsed selections are allowed while composing.
54+
if (composing_ && base != extent) {
55+
return false;
56+
}
57+
// Verify the selection is within range.
58+
size_t min_pos = composing_ ? composing_start() : 0;
59+
size_t max_pos = composing_ ? composing_end() : text_.length();
60+
if (!InRange(base, min_pos, max_pos) || !InRange(extent, min_pos, max_pos)) {
4861
return false;
4962
}
5063
selection_base_ = base;
5164
selection_extent_ = extent;
5265
return true;
5366
}
5467

55-
void TextInputModel::DeleteSelected() {
56-
text_.erase(selection_start(), selection_end() - selection_start());
57-
selection_base_ = selection_start();
68+
bool TextInputModel::SetComposingRange(size_t base,
69+
size_t extent,
70+
size_t cursor_offset) {
71+
if (!composing_ || base > text_.length() || extent > text_.length()) {
72+
return false;
73+
}
74+
composing_base_ = base;
75+
composing_extent_ = extent;
76+
selection_base_ = composing_start() + cursor_offset;
77+
selection_extent_ = selection_base_;
78+
return true;
79+
}
80+
81+
void TextInputModel::BeginComposing() {
82+
composing_ = true;
83+
composing_base_ = selection_start();
84+
composing_extent_ = composing_base_;
85+
}
86+
87+
void TextInputModel::UpdateComposingText(const std::string& composing_text) {
88+
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>
89+
utf16_converter;
90+
std::u16string text = utf16_converter.from_bytes(composing_text);
91+
92+
// Preserve selection if we get a no-op update to the composing region.
93+
if (text.length() == 0 && composing_base_ == composing_extent_) {
94+
return;
95+
}
96+
if (selection_base_ != selection_extent_) {
97+
DeleteSelected();
98+
}
99+
auto start = composing_start();
100+
text_.replace(start, composing_end() - start, text);
101+
SetComposingLength(text.length());
102+
selection_base_ = composing_end();
58103
selection_extent_ = selection_base_;
59104
}
60105

106+
void TextInputModel::CommitComposing() {
107+
// Preserve selection if no composing text was entered.
108+
if (composing_base_ == composing_extent_) {
109+
return;
110+
}
111+
composing_base_ = composing_extent_;
112+
selection_base_ = composing_extent_;
113+
selection_extent_ = composing_extent_;
114+
}
115+
116+
void TextInputModel::EndComposing() {
117+
composing_ = false;
118+
composing_base_ = 0;
119+
composing_extent_ = 0;
120+
}
121+
122+
void TextInputModel::DeleteSelected() {
123+
auto start = selection_start();
124+
text_.erase(start, selection_end() - start);
125+
selection_base_ = start;
126+
selection_extent_ = start;
127+
if (composing_) {
128+
// This occurs only immediately after composing has begun with a selection.
129+
composing_base_ = start;
130+
composing_extent_ = start;
131+
}
132+
}
133+
134+
void TextInputModel::SetComposingLength(size_t length) {
135+
if (composing_extent_ >= composing_base_) {
136+
composing_extent_ = composing_base_ + length;
137+
} else {
138+
composing_base_ = composing_extent_ + length;
139+
}
140+
}
141+
61142
void TextInputModel::AddCodePoint(char32_t c) {
62143
if (c <= 0xFFFF) {
63144
AddText(std::u16string({static_cast<char16_t>(c)}));
@@ -76,6 +157,14 @@ void TextInputModel::AddText(const std::u16string& text) {
76157
if (selection_base_ != selection_extent_) {
77158
DeleteSelected();
78159
}
160+
if (composing_) {
161+
// Replace the composing text.
162+
auto start = composing_start();
163+
text_.erase(start, composing_end() - start);
164+
selection_base_ = start;
165+
selection_extent_ = start;
166+
SetComposingLength(text.length());
167+
}
79168
text_.insert(selection_extent_, text);
80169
selection_extent_ += text.length();
81170
selection_base_ = selection_extent_;
@@ -94,11 +183,15 @@ bool TextInputModel::Backspace() {
94183
return true;
95184
}
96185
// There's no selection; delete the preceding codepoint.
97-
if (selection_base_ != 0) {
186+
auto min_pos = composing_ ? composing_start() : 0;
187+
if (selection_base_ > min_pos) {
98188
int count = IsTrailingSurrogate(text_.at(selection_base_ - 1)) ? 2 : 1;
99189
text_.erase(selection_base_ - count, count);
100190
selection_base_ -= count;
101191
selection_extent_ = selection_base_;
192+
if (composing_) {
193+
SetComposingLength(composing_end() - composing_start() - count);
194+
}
102195
return true;
103196
}
104197
return false;
@@ -111,43 +204,50 @@ bool TextInputModel::Delete() {
111204
return true;
112205
}
113206
// There's no selection; delete the following codepoint.
114-
if (selection_base_ != text_.length()) {
207+
auto max_pos = composing_ ? composing_end() : text_.length();
208+
if (selection_base_ < max_pos) {
115209
int count = IsLeadingSurrogate(text_.at(selection_base_)) ? 2 : 1;
116210
text_.erase(selection_base_, count);
117211
selection_extent_ = selection_base_;
212+
if (composing_) {
213+
SetComposingLength(composing_end() - composing_start() - count);
214+
}
118215
return true;
119216
}
120217
return false;
121218
}
122219

123220
bool TextInputModel::DeleteSurrounding(int offset_from_cursor, int count) {
221+
auto min_pos = composing_ ? composing_start() : 0;
222+
auto max_pos = composing_ ? composing_end() : text_.length();
124223
auto start = selection_extent_;
125224
if (offset_from_cursor < 0) {
126225
for (int i = 0; i < -offset_from_cursor; i++) {
127226
// If requested start is before the available text then reduce the
128227
// number of characters to delete.
129-
if (start == 0) {
228+
if (start == min_pos) {
130229
count = i;
131230
break;
132231
}
133232
start -= IsTrailingSurrogate(text_.at(start - 1)) ? 2 : 1;
134233
}
135234
} else {
136-
for (int i = 0; i < offset_from_cursor && start != text_.length(); i++) {
235+
for (int i = 0; i < offset_from_cursor && start != max_pos; i++) {
137236
start += IsLeadingSurrogate(text_.at(start)) ? 2 : 1;
138237
}
139238
}
140239

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

146245
if (start == end) {
147246
return false;
148247
}
149248

150-
text_.erase(start, end - start);
249+
auto deleted_length = end - start;
250+
text_.erase(start, deleted_length);
151251

152252
// Cursor moves only if deleted area is before it.
153253
if (offset_from_cursor <= 0) {
@@ -157,22 +257,32 @@ bool TextInputModel::DeleteSurrounding(int offset_from_cursor, int count) {
157257
// Clear selection.
158258
selection_extent_ = selection_base_;
159259

260+
// Adjust composing range.
261+
if (composing_) {
262+
SetComposingLength(composing_end() - composing_start() - deleted_length);
263+
}
160264
return true;
161265
}
162266

163267
bool TextInputModel::MoveCursorToBeginning() {
164-
if (selection_base_ == 0 && selection_extent_ == 0)
268+
// If we're composing, move to the begining of the composing range, else move
269+
// to begging of string.
270+
auto min_pos = composing_ ? composing_start() : 0;
271+
if (selection_base_ == min_pos && selection_extent_ == min_pos) {
165272
return false;
166-
167-
selection_base_ = 0;
168-
selection_extent_ = 0;
273+
}
274+
selection_base_ = min_pos;
275+
selection_extent_ = min_pos;
169276
return true;
170277
}
171278

172279
bool TextInputModel::MoveCursorToEnd() {
173-
auto max_pos = text_.length();
174-
if (selection_base_ == max_pos && selection_extent_ == max_pos)
280+
// If we're composing, move to the end of the composing range, else move to
281+
// end of string.
282+
auto max_pos = composing_ ? composing_end() : text_.length();
283+
if (selection_base_ == max_pos && selection_extent_ == max_pos) {
175284
return false;
285+
}
176286

177287
selection_base_ = max_pos;
178288
selection_extent_ = max_pos;
@@ -187,7 +297,8 @@ bool TextInputModel::MoveCursorForward() {
187297
return true;
188298
}
189299
// If not at the end, move the extent forward.
190-
if (selection_extent_ != text_.length()) {
300+
auto max_pos = composing_ ? composing_end() : text_.length();
301+
if (selection_extent_ != max_pos) {
191302
int count = IsLeadingSurrogate(text_.at(selection_base_)) ? 2 : 1;
192303
selection_base_ += count;
193304
selection_extent_ = selection_base_;
@@ -205,7 +316,8 @@ bool TextInputModel::MoveCursorBack() {
205316
return true;
206317
}
207318
// If not at the start, move the beginning backward.
208-
if (selection_base_ != 0) {
319+
auto min_pos = composing_ ? composing_start() : 0;
320+
if (selection_base_ != min_pos) {
209321
int count = IsTrailingSurrogate(text_.at(selection_base_ - 1)) ? 2 : 1;
210322
selection_base_ -= count;
211323
selection_extent_ = selection_base_;

0 commit comments

Comments
 (0)