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

Commit f9eabec

Browse files
authored
Win32: Fix Korean text input (#30805)
Fixes an issue with Korean IMEs wherein a text input state update may be sent to the framework that misleads the framework into assuming that IME composing has ended. When inputting Korean text, characters are built up keystroke by keystroke until the point that either: * the user presses space/enter to terminate composing and commit the character, or; * the user presses a key such that the character currently being composed cannot be modified further, and the IME determines that the user has begun composing the next character. The following is an example sequence of events for the latter case: 1. User presses ㅂ. GCS_COMPSTR event received with ㅂ. Embedder sends state update to framework. 2. User presses ㅏ. GCS_COMPSTR event received with 바. Embedder sends state update to framework. 3. User presses ㄴ. GCS_COMPSTR event received with 반. Embedder sends state update to framework. 4. User presses ㅏ. At this point, the current character being composed (반) cannot be modified in a meaningful way, and the IME determines that the user is typing 바 followed by 나. GCS_RESULTSTR event received with 바, immediately followed by GCS_COMPSTR event with 나. In step 4, we previously sent two events to the framework, one immediately after the other: * GCS_RESULTSTR triggers the text input model to commit the current composing region to the string under edit. This causes the composing region to collapse to an empty range. * GCS_COMPSTR triggers the text input model to insert the new composing character and set the composing region to that character. Conceptually, this is an atomic operation. The fourth keystroke causes the 반 character to be broken into two (바 and ㄴ) and the latter to be modified to 나. From the user's point of view, as well as from the IME's point of view, the user has NOT stopped composing, and the composing region has simply moved on to the next character. Flutter has no concept of whether the user is composing or not other that whether a non-empty composing region exists. As such, sending a state update after the GCS_RESULTSTR event misleads the framework into believing that composing has ended. This triggers a serious bug: Text fields with input formatters applied do not perform input formatting updates while composing is active; instead they wait until composing has ended to apply any formatting. The previous behaviour would thus trigger input formatters to be applied each time the user input caused a new character to be input. This has the add-on negative effect that once formatting has been applied, it sends an update back to the embedder so that the native OS text input state can be updated. However, since the GCS_RESULTSTR event is _immediately_ followed by a GCS_COMPSTR, the state has changed in the meantime, and the embedder is left processing an update (the intermediate state sent after the GCS_RESULTSTR) which is now out of date (i.e. missing the new state from the GCS_COMPSTR event). Since GCS_RESULTR events are always immediately followed by a subsequent GCS_COMPSTR (in the case where composing continues) or a WM_IME_ENDCOMPOSITION (in the case where composing is finished), and because the event handlers for both of those send updated state to the framework, this change eliminates sending the (intermediate) state in response to GCS_COMPSTR events. Issue: flutter/flutter#96209 (full fix) Issue: flutter/flutter#88645 (partial fix)
1 parent bdbd075 commit f9eabec

File tree

2 files changed

+79
-1
lines changed

2 files changed

+79
-1
lines changed

shell/platform/windows/text_input_plugin.cc

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,29 @@ void TextInputPlugin::ComposeCommitHook() {
111111
return;
112112
}
113113
active_model_->CommitComposing();
114-
SendStateUpdate(*active_model_);
114+
115+
// We do not trigger SendStateUpdate here.
116+
//
117+
// Until a WM_IME_ENDCOMPOSING event, the user is still composing from the OS
118+
// point of view. Commit events are always immediately followed by another
119+
// composing event or an end composing event. However, in the brief window
120+
// between the commit event and the following event, the composing region is
121+
// collapsed. Notifying the framework of this intermediate state will trigger
122+
// any framework code designed to execute at the end of composing, such as
123+
// input formatters, which may try to update the text and send a message back
124+
// to the engine with changes.
125+
//
126+
// This is a particular problem with Korean IMEs, which build up one
127+
// character at a time in their composing region until a keypress that makes
128+
// no sense for the in-progress character. At that point, the result
129+
// character is committed and a compose event is immedidately received with
130+
// the new composing region.
131+
//
132+
// In the case where this event is immediately followed by a composing event,
133+
// the state will be sent in ComposeChangeHook.
134+
//
135+
// In the case where this event is immediately followed by an end composing
136+
// event, the state will be sent in ComposeEndHook.
115137
}
116138

117139
void TextInputPlugin::ComposeEndHook() {

shell/platform/windows/text_input_plugin_unittest.cc

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,5 +81,61 @@ TEST(TextInputPluginTest, ClearClientResetsComposing) {
8181
EXPECT_TRUE(delegate.ime_was_reset());
8282
}
8383

84+
// Verify that the embedder sends state update messages to the framework during
85+
// IME composing.
86+
TEST(TextInputPluginTest, VerifyComposingSendStateUpdate) {
87+
bool sent_message = false;
88+
TestBinaryMessenger messenger(
89+
[&sent_message](const std::string& channel, const uint8_t* message,
90+
size_t message_size,
91+
BinaryReply reply) { sent_message = true; });
92+
BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {};
93+
94+
EmptyTextInputPluginDelegate delegate;
95+
TextInputPlugin handler(&messenger, &delegate);
96+
97+
auto& codec = JsonMethodCodec::GetInstance();
98+
99+
// Call TextInput.setClient to initialize the TextInputModel.
100+
auto arguments = std::make_unique<rapidjson::Document>(rapidjson::kArrayType);
101+
auto& allocator = arguments->GetAllocator();
102+
arguments->PushBack(42, allocator);
103+
rapidjson::Value config(rapidjson::kObjectType);
104+
config.AddMember("inputAction", "done", allocator);
105+
config.AddMember("inputType", "text", allocator);
106+
arguments->PushBack(config, allocator);
107+
auto message =
108+
codec.EncodeMethodCall({"TextInput.setClient", std::move(arguments)});
109+
messenger.SimulateEngineMessage("flutter/textinput", message->data(),
110+
message->size(), reply_handler);
111+
112+
// ComposeBeginHook should send state update.
113+
sent_message = false;
114+
handler.ComposeBeginHook();
115+
EXPECT_TRUE(sent_message);
116+
117+
// ComposeChangeHook should send state update.
118+
sent_message = false;
119+
handler.ComposeChangeHook(u"4", 1);
120+
EXPECT_TRUE(sent_message);
121+
122+
// ComposeCommitHook should NOT send state update.
123+
//
124+
// Commit messages are always immediately followed by a change message or an
125+
// end message, both of which will send an update. Sending intermediate state
126+
// with a collapsed composing region will trigger the framework to assume
127+
// composing has ended, which is not the case until a WM_IME_ENDCOMPOSING
128+
// event is received in the main event loop, which will trigger a call to
129+
// ComposeEndHook.
130+
sent_message = false;
131+
handler.ComposeCommitHook();
132+
EXPECT_FALSE(sent_message);
133+
134+
// ComposeEndHook should send state update.
135+
sent_message = false;
136+
handler.ComposeEndHook();
137+
EXPECT_TRUE(sent_message);
138+
}
139+
84140
} // namespace testing
85141
} // namespace flutter

0 commit comments

Comments
 (0)